webpack4打包流程分析,实现一个简易的webpack打包器

本文详细解析了webpack的打包流程,从启动打包程序开始,逐步讲解了webpack如何通过Compiler实例、配置参数、插件系统以及编译工作等步骤完成代码的打包。在理解webpack核心逻辑的基础上,模拟实现了一个简易的webpack打包器,涵盖了从读取源代码、转换AST、编译模块到写入文件的整个过程。
摘要由CSDN通过智能技术生成

webpack打包的流程大致可以归纳成:

  1. options:通过config文件传递进来的配置参数
  2. webpack:生成Compiler实例及其他webpack初始化逻辑
  3. compiler:编译的核心桥梁,根据配置文件,通过执行webpack(options),生成的并返回一个Compiler实例compiler(自我理解为一个编译工厂)
    2.1 在生成的Compiler实例上将options配置参数挂载到compiler.options,以方便将来获取配置参数
    2.2 通过new NodeEnvironmentPlugin()让compiler得到读写文件的能力
    2.3 遍历options配置中的plugins,如果有plugins,则通过tapable机制注册一个的监听事件,以便将来某个时刻触发监听来执行插件
    2.4 通过new WebpackOptionsApply()的process方法来注册所有内置插件的监听事件(第4大点)
  4. 通过new WebpackOptionsApply().process(options, compiler)注册内置插件
    4.1 新建一个EntryOptionPlugin实例并注册entryOption监听事件
    4.2 执行entryOption事件来注册make监听事件
  5. 执行compiler的run方法来打包生成代码文件,以及返回最终的打包对象数据
    5.1 生成一个Compilation实例compilation(负责具体的代码编译)
    5.2 执行make事件来读取文件源代码转换成ast语法树,随后替换其中的关键字为webpack关键字,再转换成编译后的源代码
    5.3 遍历已被编译的源代码是否有依赖模块,如果有再重复执行5.2步骤来递归编译
    5.4 通过compilation实例方法将编译后的代码写入模版文件
    5.5 通过compiler实例将代码写入到指定文件夹下的指定文件,并返回打包后的数据信息

话不多说,下面我们直接从webpack的入口文件开始来一步步拆解模拟一个webpack的简易打包器

1. 启动打包程序

start.js

const webpack = require('./lib/myWebpack/webpack')
const options = require('./webpack.config')

// 生成compiler编译工厂
const compiler = webpack(options)

// 执行打包程序
compiler.run((err, stats) => {
  console.log(err)
  console.log(stats)
})

2. 生成Compiler实例及其他webpack初始化逻辑

webpack.js

const Compiler = require('./Compiler')
const NodeEnvironmentPlugin = require('./NodeEnvironmentPlugin')
const WebpackOptionsApply = require('./WebpackOptionsApply')

const webpack = options => {
  // 实例化一个compiler对象(编译工厂)
  const compiler = new Compiler(options.context)

  // 将options配置参数挂载到compiler.options,以方便将来获取配置参数
  compiler.options = options

  // 让compiler获得读写文件的能力
  new NodeEnvironmentPlugin().apply(compiler)

  // 遍历options配置中的plugins注册监听事件,以便将来某个时刻触发监听来执行插件
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      plugin.apply(compiler)
    }
  }
  
  // 注册内置插件
  new WebpackOptionsApply().process(options, compiler)

  return compiler
}

module.exports = webpack

3. 生成Compiler实例

// 实例化一个compiler对象(编译工厂)
const compiler = new Compiler(options.context)

通过上文的分析,Compiler必须要具备一个run方法来开始编译,并且需具备一些钩子来让用户注册自定义插件和挂载内置插件。由于Compiler只是一个“工厂”,只负责流程管理,所以我们还需要创造一个工人compilation来负责代码的编译工作,当compilation把代码编译完成后,通知Compiler来写入文件。

const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')

class Compiler extends Tapable {
  constructor (context) {
    super()
    this.context = context

    // 生成一系列钩子
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']),
      thisCompilation: new SyncHook(['compilation', 'params']),
      compilation: new SyncHook(['compilation', 'params']),
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),
      emit: new AsyncSeriesHook(['compilation'])
    }
  }

  emitAssets(compilation, callback) {
    // TODO: 写入打包文件
  }

  run (callback) {
    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        // TODO:this.compile(callback) 执行编译逻辑,callback为编译完成后的回调
      })
    })
  }

}

module.exports = Compiler

接下来将编译逻辑compile方法补全,按源码的逻辑,此时会生成一个compilation工人,同时会调用一些钩子事件,最重要的就是make钩子的调用进入make流程,在make流程完成后执行回调处理onCompiled编译完成之后的流程

compile (callback) {
  // 生成beforeCompile,compile钩子用到的参数
  const params = this.newCompilationParams()
  this.hooks.beforeCompile.callAsync(params, err => {
    this.hooks.compile.call(params);

    // 生成compilation
    const compilation = this.newCompilation(params)
    
    // 执行make钩子
    this.hooks.make.callAsync(compilation, err => {
      // TODO:make钩子执行完成的回调
    })
  })
}

newCompilationParams () {
  const params = {
    normalModuleFactory: new NormalModuleFactory()
  }
  return params
}

newCompilation (params) {
  const compilation = new Compilation(this)
  this.hooks.thisCompilation.call(compilation, params)
  this.hooks.compilation.call(compilation, params)
  return compilation
}

此时的Compile.js代码

const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')

class Compiler extends Tapable {
  constructor (context) {
    super()
    this.context = context

    // 生成一系列钩子
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']),
      thisCompilation: new SyncHook(['compilation', 'params']),
      compilation: new SyncHook(['compilation', 'params']),
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),
      emit: new AsyncSeriesHook(['compilation'])
    }
  }

  run (callback) {
    const onCompiled = () => {}
    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        this.compile(onCompiled)
      })
    })
  }

  compile (callback) {
    // 生成beforeCompile,compile钩子用到的参数
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params);
  
      // 生成compilation
      const compilation = this.newCompilation(params)
      
      // 执行make钩子
      this.hooks.make.callAsync(compilation, err => {
        // TODO:make钩子执行完成的回调, 开始处理chunk
      })
    })
  }
  
  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params
  }
  
  newCompilation (params) {
    const compilation = new Compilation(this)
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }

}

module.exports = Compiler

4. NodeEnvironmentPlugin,赋予compiler实例读写能力

// 让compiler获得读写文件的能力
new NodeEnvironmentPlugin().apply(compiler)

我们让NodeEnvironmentPlugin类继承fs模块的能力,由于属于插件,根据webpack的思想,需要有一个apply方法来调用实现

NodeEnvironmentPlugin.js

const fs = require('fs')

class NodeEnvironmentPlugin {
  constructor(options) {
    this.options = options || {}
  }
  
  apply(compiler) {
    complier.inputFileSystem = fs
    complier.outputFileSystem = fs
  }
}

module.exports = NodeEnvironmentPlugin

5. 注册自定义插件apply方法,注册钩子事件

// 遍历options配置中的plugins注册监听事件,以便将来某个时刻触发监听来执行插件
if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    plugin.apply(compiler)
  }
}

6. 注册webpack内置插件

// 注册内置插件
new WebpackOptionsApply().process(options, compiler)

通过注册内置插件来注册compiler实例上的entryOption,make钩子监听事件。

WebpackOptionsApply.js

const EntryOptionPlugin = require('./EntryOptionPlugin')

class WebpackOptionsApply {
  process (options, compiler) {
    // 入口插件注册
    new EntryOptionPlugin().apply(compiler)
    compiler.hooks.entryOption.call(options.context, options.entry)
  }
}

module.exports = WebpackOptionsApply

EntryOptionPlugin.js

const SingleEntryPlugin = require("./SingleEntryPlugin")

const itemToPlugin = function (context, item, name) {
  return new SingleEntryPlugin(context, item, name)
}

class EntryOptionPlugin {
  apply (compiler) {
    compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
      // 注册make钩子
      itemToPlugin(context, entry, "main").apply(compiler)
    })
  }
}

module.exports = EntryOptionPlugin

SingleEntryPlugin.js

class SingleEntryPlugin {
  constructor(context, entry, name) {
    this.context = context
    this.entry = entry
    this.name = name
  }

  apply (compiler) {
    compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
      const { context, entry, name } = this
      compilation.addEntry(context, entry, name, callback)
    })
  }
}

module.exports = SingleEntryPlugin

7. compilation编译工实现

首先实现一个ast解析器,将来用来把代码转换成ast语法树
Parser.js

const babylon = require('babylon')
const { Tapable } = require('tapable')

class Parser extends Tapable {
  parse (source) {
    return babylon.parse(source, {
      sourceType: 'module',
      plugins: ['dynamicImport']  // 当前插件可以支持 import() 动态导入的语法
    })
  }
}

module.exports = Parser

通过make钩子,我们得知compilation实例上需要有一个addEntry方法作为入口

const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树

const parser = new Parser()

class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
    this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
    this.entries = []  // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = []  // 存放当前次打包过程中所产出的 chunk
    this.assets = []
    this.files = []
    this.hooks = {
      succeedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  /**
   * 完成模块编译操作
   * @param {*} context 当前项目的根
   * @param {*} entry 当前的入口的相对路径
   * @param {*} name chunkName main 
   * @param {*} callback 回调
   */
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module)
    })
  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      parser,
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule)
    }, callback)
  }

  /**
   * 定义一个创建模块的方法,达到复用的目的
   * @param {*} data 创建模块时所需要的一些属性值 
   * @param {*} doAddEntry 可选参数,在加载入口模块的时候,将入口模块的id 写入 this.entries 
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    // 代码工厂,解析ast语法树,编译最终代码形态
    const module = normalModuleFactory.create(data)
    const afterBuild = () => {
      // TODO
    }
    this.buildModule(module, afterBuild)

    // 当我们完成了本次的 build 操作之后将 module 进行保存
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }

  /**
   * 完成具体的 build 行为
   * @param {*} module 当前需要被编译的模块
   * @param {*} callback 
   */
  buildModule(module, callback) {
    module.build(this, (err) => {
      // 如果代码走到这里就意味着当前 Module 的编译完成了
      this.hooks.succeedModule.call(module)
      callback(err, module)
    })
  }
}

module.exports = Compilation

compilation createModule需要实现一个模块代码工厂类,将Parser后的ast代码替换关键字后,编译成源代码
NormalModuleFactory.js

const NormalModule = require("./NormalModule")

class NormalModuleFactory {
  create (data) {
    return new NormalModule(data)
  }
}

module.exports = NormalModuleFactory

NormalModule.js

const path = require('path')
const types = require('@babel/types')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default

class NormalModule {
  constructor (data) {
    this.context = data.context
    this.name = data.name
    this.moduleId = data.moduleId
    this.rawRequest = data.rawRequest
    this.parser = data.parser
    this.resource = data.resource
    this._source  // 存放某个模块的源代码
    this._ast // 存放某个模板源代码对应的 ast 
    this.dependencies = [] // 定义一个空数组用于保存被依赖加载的模块信息
  }

  build(compilation, callback) {
    /**
     * 01 从文件中读取到将来需要被加载的 module 内容
     * 02 如果当前不是 js 模块则需要 Loader 进行处理,最终返回 js 模块 
     * 03 上述的操作完成之后就可以将 js 代码转为 ast 语法树
     * 04 当前 js 模块内部可能又引用了很多其它的模块,因此我们需要递归完成 
     * 05 前面的完成之后,我们只需要重复执行即可
     */
    this.doBuild(compilation, err => {
      // 将源代码转成ast语法树
      this._ast = this.parser.parse(this._source)

      // 对语法树进行修改,最后再将 ast 转回成 code 代码 
      traverse(this._ast, {
        CallExpression: nodePath => {
          let node = nodePath.node

          // 定位 require 所在的节点
          if (node.callee.name === 'require') {
            // 获取原始请求路径
            let modulePath = node.arguments[0].value
            // 取出当前被加载的模块名称
            let moduleName = modulePath.split(path.posix.sep).pop()
            // 当前我们的打包器只处理 js
            let extName = moduleName.indexOf('.') == -1 ? '.js' : ''
            moduleName += extName
            // 最终我们想要读取当前js里的内容,所以我们需要个绝对路径
            let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
            // 将当前模块的id定义为./src/**形式
            let depModuleId = './' + path.posix.relative(this.context, depResource)

            // 记录当前被依赖模块的信息,方便后面递归加载
            this.dependencies.push({
              name: this.name, // TODO: 将来需要修改 
              context: this.context,
              rawRequest: moduleName,
              moduleId: depModuleId,
              resource: depResource
            })

            // 替换内容
            node.callee.name = '__webpack_require__'
            node.arguments = [types.stringLiteral(depModuleId)]
          }
        }
      })

      // 将修改后的 ast 转回成 code 
      const { code } = generator(this._ast)
      this._source = code
      callback(err)
    })
  }

  doBuild(compilation, callback) {
    this.getSource(compilation, (err, source) => {
      this._source = source
      callback()
    })
  }

  getSource(compilation, callback) {
    compilation.inputFileSystem.readFile(this.resource, 'utf8', callback)
  }
}

module.exports = NormalModule

接下来回到Compilation中完成afterBuild之后的工作,判读module中是否有依赖,如果有继续递归执行createModule,直到没有依赖为止,最后执行addEntry的回调函数(即make钩子执行完成的回调)

const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树

const parser = new Parser()

class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
    this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
    this.entries = []  // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = []  // 存放当前次打包过程中所产出的 chunk
    this.assets = []
    this.files = []
    this.hooks = {
      succeedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  /**
   * 完成模块编译操作
   * @param {*} context 当前项目的根
   * @param {*} entry 当前的入口的相对路径
   * @param {*} name chunkName main 
   * @param {*} callback 回调
   */
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module)
    })
  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      parser,
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule)
    }, callback)
  }

  /**
   * 定义一个创建模块的方法,达到复用的目的
   * @param {*} data 创建模块时所需要的一些属性值 
   * @param {*} doAddEntry 可选参数,在加载入口模块的时候,将入口模块的id 写入 this.entries 
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    // 代码工厂,解析ast语法树,编译最终代码形态
    const module = normalModuleFactory.create(data)
    const afterBuild = (err, module) => {
      // 判断当前次module加载完成之后是否需要处理依赖加载
      if (module.dependencies.length > 0) {
        this.processDependencies(module, (err) => {
          callback(err, module)
        })
      } else {
        callback(err, module)
      }
    }
    this.buildModule(module, afterBuild)

    // 当我们完成了本次的 build 操作之后将 module 进行保存
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }

  /**
   * 完成具体的 build 行为
   * @param {*} module 当前需要被编译的模块
   * @param {*} callback 
   */
   buildModule(module, callback) {
    module.build(this, (err) => {
      // 如果代码走到这里就意味着当前 Module 的编译完成了
      this.hooks.succeedModule.call(module)
      callback(err, module)
    })
  }
}

module.exports = Compilation

8. make钩子执行完成后的操作

在Compilation的afterBuild执行完成后,会触发make钩子的回调函数,通过Compilation开始处理chunk,将刚才被编译过的code写入模板文件中,最后执行写入目录操作,到此Compiler的任务就完成了。现在先回到Compiler里面,补全Compiler代码

Compiler.js

const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable')
const path = require('path')
const mkdirp = require('mkdirp')
const Stats = require('./Stats')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')

class Compiler extends Tapable {
  constructor (context) {
    super()
    this.context = context

    // 生成一系列钩子
    this.hooks = {
      done: new AsyncSeriesHook(['stats']),
      entryOption: new SyncBailHook(['context', 'entry']),
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']),
      thisCompilation: new SyncHook(['compilation', 'params']),
      compilation: new SyncHook(['compilation', 'params']),
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      make: new AsyncParallelHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),
      emit: new AsyncSeriesHook(['compilation'])
    }
  }

  // 创建打包目录,在目录创建完成之后执行文件的写操作
  emitAssets(compilation, callback) {
    // 01 定义一个工具方法用于执行文件的生成操作
    const emitFlies = (err) => {
      const assets = compilation.assets
      let outputPath = this.options.output.path

      for (let file in assets) {
        let source = assets[file]
        let targetPath = path.posix.join(outputPath, file)
        this.outputFileSystem.writeFileSync(targetPath, source, 'utf8')
      }

      callback(err)
    }

    // 创建目录之后启动文件写入
    this.hooks.emit.callAsync(compilation, (err) => {
      mkdirp.sync(this.options.output.path)
      emitFlies()
    })

  }

  run (callback) {
    
    // 输出stats打包后的数据信息
    const finalCallback = function (err, stats) {
      callback(err, stats)
    }

    const onCompiled = (err, compilation) => {

      // 调用emitAssets将处理好的 chunk 写入到指定的文件然后输出至打包目录 
      this.emitAssets(compilation, (err) => {
        let stats = new Stats(compilation)
        finalCallback(err, stats)
      })
    }

    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        this.compile(onCompiled)
      })
    })
  }

  compile (callback) {
    // 生成beforeCompile,compile钩子用到的参数
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params);
  
      // 生成compilation
      const compilation = this.newCompilation(params)
      
      // 执行make钩子
      this.hooks.make.callAsync(compilation, err => {

        // 开始处理 chunk 
        compilation.seal((err) => {
          this.hooks.afterCompile.callAsync(compilation, (err) => {
            callback(err, compilation)
          })
        })
      })
    })
  }
  
  newCompilationParams () {
    const params = {
      normalModuleFactory: new NormalModuleFactory()
    }
    return params
  }
  
  newCompilation (params) {
    const compilation = new Compilation(this)
    this.hooks.thisCompilation.call(compilation, params)
    this.hooks.compilation.call(compilation, params)
    return compilation
  }

}

module.exports = Compiler

Stats.js

class Stats {
  constructor (compilation) {
    this.entries = compilation.entries
    this.modules = compilation.modules
    this.chunks = compilation.chunks
    this.files = compilation.files
  }

  toJson() {
    return this
  }
}

module.exports = Stats

9. 补全Compilation中处理chunk的逻辑

处理chunk指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并

Chunk.js

class Chunk {
  constructor (entryModule) {
    this.entryModule = entryModule
    this.name = entryModule.name
    this.files = []  // 记录每个 chunk的文件信息
    this.modules = [] // 记录每个 chunk 里的所包含的 module
  }
}

module.exports = Chunk

Compilation.js

const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树
const ejs = require('ejs')
const Chunk = require('./Chunk')
const path = require('path')
const async = require('neo-async')

const parser = new Parser()
const normalModuleFactory = new NormalModuleFactory()

class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
    this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
    this.entries = []  // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = []  // 存放当前次打包过程中所产出的 chunk
    this.assets = []
    this.files = []
    this.hooks = {
      succeedModule: new SyncHook(['module']),
      seal: new SyncHook(),
      beforeChunks: new SyncHook(),
      afterChunks: new SyncHook()
    }
  }

  /**
   * 完成模块编译操作
   * @param {*} context 当前项目的根
   * @param {*} entry 当前的入口的相对路径
   * @param {*} name chunkName main 
   * @param {*} callback 回调
   */
  addEntry (context, entry, name, callback) {
    this._addModuleChain(context, entry, name, (err, module) => {
      callback(err, module)
    })
  }

  _addModuleChain (context, entry, name, callback) {
    this.createModule({
      parser,
      name,
      context,
      rawRequest: entry,
      resource: path.posix.join(context, entry),
      moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
    }, (entryModule) => {
      this.entries.push(entryModule)
    }, callback)
  }

  /**
   * 定义一个创建模块的方法,达到复用的目的
   * @param {*} data 创建模块时所需要的一些属性值 
   * @param {*} doAddEntry 可选参数,在加载入口模块的时候,将入口模块的id 写入 this.entries 
   * @param {*} callback 
   */
  createModule (data, doAddEntry, callback) {
    // 代码工厂,解析ast语法树,编译最终代码形态
    const module = normalModuleFactory.create(data)
    const afterBuild = (err, module) => {
      // 判断当前次module加载完成之后是否需要处理依赖加载
      if (module.dependencies.length > 0) {
        this.processDependencies(module, (err) => {
          callback(err, module)
        })
      } else {
        callback(err, module)
      }
    }
    this.buildModule(module, afterBuild)

    // 当我们完成了本次的 build 操作之后将 module 进行保存
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }

  /**
   * 完成具体的 build 行为
   * @param {*} module 当前需要被编译的模块
   * @param {*} callback 
   */
   buildModule (module, callback) {
    module.build(this, (err) => {
      // 如果代码走到这里就意味着当前 Module 的编译完成了
      this.hooks.succeedModule.call(module)
      callback(err, module)
    })
  }

  processDependencies(module, callback) {
    // 01 当前的函数核心功能就是实现一个被依赖模块的递归加载
    // 02 加载模块的思想都是创建一个模块,然后想办法将被加载模块的内容拿进来
    // 03 当前我们不知道 module 需要依赖几个模块,此时我们需要想办法让所有的被依赖的模块都加载完成之后再执行 callback【 neo-async 】
    const dependencies = module.dependencies

    async.forEach(dependencies, (dependency, done) => {
      this.createModule({
        parser,
        name: dependency.name,
        context: dependency.context,
        rawRequest: dependency.rawRequest,
        moduleId: dependency.moduleId,
        resource: dependency.resource
      }, null, done)
    }, callback)
  }

  // 处理Chunk
  seal (callback) {
    this.hooks.seal.call()
    this.hooks.beforeChunks.call()

    // 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
    for (const entryModule of this.entries) {
      // 核心:创建模块加载已有模块的内容,同时记录模块信息 
      const chunk = new Chunk(entryModule)
      // 保存 chunk 信息
      this.chunks.push(chunk)
      // 给 chunk 属性赋值 
      chunk.modules = this.modules.filter(module => module.name === chunk.name)
    }

    this.hooks.afterChunks.call(this.chunks)
    // 根据模板渲染代码
    this.createChunkAssets()
    // 执行回调写入文件
    callback()
  }

  createChunkAssets () {
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i]
      const fileName = chunk.name + '.js'
      chunk.files.push(fileName)

      // 获取模板文件的路径
      const tempPath = path.posix.join(__dirname, 'temp/main.ejs')
      // 读取模块文件中的内容
      const tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
      // 获取渲染函数
      const tempRender = ejs.compile(tempCode)
      // 按ejs的语法渲染数据
      const source = tempRender({
        entryModuleId: chunk.entryModule.moduleId,
        modules: chunk.modules
      })

      // 输出文件
      this.emitAssets(fileName, source)
    }
  }

  emitAssets (fileName, source) {
    this.assets[fileName] = source
    this.files.push(fileName)
  }
}

module.exports = Compilation

至此webpack的简要打包流程及一个简易的webpack打包器就完成了,一句话总结起来就是:
根据配置文件,递归拿到文件的源代码,替换源代码中的关键字,然后合并代码,最后根据模板输出到指定目录

附文结构

  • Chunk.js
  • Compilation.js
  • Compiler.js
  • EntryOptionPlugin.js
  • NodeEnvironmentPlugin.js
  • NormalModule.js
  • NormalModuleFactory.js
  • Parser.js
  • SingleEntryPlugin.js
  • Stats.js
  • webpack.js
  • WebpackOptionsApply.js
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值