初始化流程

首先需要理解compiler和compilation

Webpack中两大核心的类Compiler和Compilation。Compiler是每次Webpack全部的生命周期对象,而Compilation是Webpack中每次构建过程中的生命周期。Compilation是通过Compiler创建的实例。两个类都有自己的生命周期,即是自己的hook,通过添加hook事件,可以拿到各自生命周期的关键性数据和对象。Compilation有个很重要的对象是Stats对象,通过这个对象可以拿到Webpack打包后所有的module、chunk和assets信息,通过分析stats对象可以得到很多信息。

调用webpack函数,传入自定义的配置项,经过webpack加工后返回一个compiler对象。返回的complier对象可以调用它的run方法,传入一个回调函数,回调函数中接收错误信息和构建完成的信息。

一、调用webpack函数

  • 首先创建一个complier实例,传入的参数是自定义选项中的项目的绝对路径
  • 将传入的自定义选项绑定给complier实例上的options属性上
  • 采用插件的方式,将fs的读写模块绑定给complier对象
  • 配置文件是否传入了plugins插件,如果传入挂载所有配置文件的plugins插件
  • 根据配置文件中的选项执行
  • 返回complier对象

    1. const webpack = function(options) {
    2. let compiler = new Compiler(options.context);
    3. compiler.options = options
    4. new NodeEnvironmentPlugin().apply(complier); // 让complier具备文件读写的能力
    5. if (options.plugins && Array.isArray(options.plugins)) {
    6. for (const plugin of options.plugins) {
    7. plugin.apply(complier);
    8. }
    9. }
    10. new WebpackOptionsApply().process(options, complier);
    11. return compiler;
    12. }

    以上就是webpack主文件的大致内容,下面我们依次来拆解每一步流程。

    二、注册文件系统

    注册的文件系统就是让compiler对象有读写文件的能力,就是将fs模块的功能进行绑定
    1. class NodeEnvironmentPlugin {
    2. applay(compiler) {
    3. compiler.inputFileSystem = fs;
    4. compiler.outputFileSystem = fs;
    5. }
    6. }

    三、根据参数进行Webpack注册

    根据webpack参数进行注册,调用process方法进打包入口的确定以及make钩子函数的注册
  • 调用process函数,传入配置参数和compiler对象

  • 注册一个入口选项的插件,传入一个compiler对象,内部会注册一个entryOption钩子函数
  • entryOption这个钩子函数内会创建一个类,执行apply方法后注册一个make钩子函数
  • 注册的make钩子函数中,会调用compilation的addEntry方法从入口开始进行打包
  • 调用entryOption这个钩子函数

    1. class SingleEntryPlugin { // 单入口的注册方法
    2. constructor(context, entry, name) {
    3. this.context = context;
    4. this.entry = entry;
    5. this.name = name;
    6. }
    7. apply(compiler) {
    8. // 注册make钩子函数
    9. compiler.hooks.make.tapAsync('SingleEntryOption', (compilation, callback) => {
    10. // 拿到项目的绝对路径,入口名称,打包后文件的名字
    11. const { context, entry, name } = this;
    12. // 调用compilation的添加入口的方法
    13. compilation.addEntry(context, entry, name, callback);
    14. })
    15. }
    16. }
    17. class EntryOptionsPlugin { // 根据入口文件进行注册
    18. apply(compiler) {
    19. // 注册entryOption这个钩子函数
    20. compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
    21. if (typeof entry === 'string') { // 如果是单入口
    22. // 创建一个类型注册make钩子函数
    23. new SingleEntryPlugin(context, entry, 'main').apply(compiler);
    24. }
    25. })
    26. }
    27. }
    28. class WebpackOptionsApply { // 根据参数进行webpack相关内容注册
    29. process(options, compiler) {
    30. new EntryOptionPlugin().apply(compiler); // 注册entryOption这个钩子函数
    31. // 调用entryOption这个钩子函数
    32. compiler.hooks.entryOption.call(options.context, options.entry);
    33. }
    34. }

    四、compiler构建执行流程

    1、初始化complier对象

    初始化complier对象,传入配置文件中的项目的绝对路径
  • 接收构建对象时传入的配置文件中的上下文绝对路径

  • 初始化生命周期的hooks钩子,hooks钩子中的生命周期是基于tapable的,后续文章中会详细介绍

    1. class Complier {
    2. constructor(context) {
    3. this.context = context;
    4. this.hooks = {
    5. // 当编译完成之后就会触发这个钩子函数执行
    6. done: new AsyncSeriesHook(['stats']),
    7. // 确定完成配置文件中入口参数执行的钩子,需要传入项目的绝对路径和入口文件的路径
    8. entryOption: new SyncBailHook(['context', 'entry']),
    9. beforeRun: new AsyncSeriesHook(['complier']), // compiler运行前
    10. run: new AsyncSeriesHook(['complier']), // compiler运行时
    11. beforeCompile: new AsyncSeriesHook(['params']), // compiler编译前
    12. compile: new AsyncSeriesHook(['params']), // 编译时
    13. // make构建,会创建一个compilation和thisCompilation实例
    14. make: new AsyncParalleHook(['compilation']),
    15. // 开始一个新的编译
    16. thisCompilation: new SyncHook(['compilation', parmas]),
    17. // 创建完成一个compilation实例
    18. compilation: new SyncHook(['compilation', 'params']),
    19. afterCompile: new AsyncSeriesHook(['compilation']), // 完成编译
    20. emit: new AsyncSeriesHook(['compilation']) // 发射写入文件
    21. }
    22. }
    23. }

    2、执行compiler的run方法

    执行compiler的run方法开始编译
  • run方法首先执行beforeRun这个钩子函数,在这个钩子函数内部执行run这个钩子函数

  • run钩子执行内部调用compiler对象的compiler方法,传入一个onCompiled方法
  • onCompiled方法内部会执行写入文件的方法

    1. run(callback) {
    2. // 正在编译的函数
    3. const onCompiled = (err, compilation) => {
    4. // 执行写入文件的方法
    5. this.emitAssets(comiplation, err => {
    6. let stats = new Stats(compilation); // 创建一个stats实例,传入compilation对象
    7. // 执行编译完成的方法,将stats信息和错误信息抛出
    8. this.hooks.done.callAsync(stats, err => {
    9. callback(err, stats)
    10. })
    11. })
    12. }
    13. // 执行compiler运行前的钩子函数
    14. this.hook.beforeRun.callAsync(this, () => {
    15. // 执行compiler运行时的钩子函数
    16. this.hook.run.callAsync(this, () => {
    17. // 编译开始的方法,传入正在编译的函数
    18. this.compiler(onCompiled);
    19. })
    20. })
    21. }

    3、执行compiler方法

    执行compiler对象的compiler方法

    compiler方法接收一个函数

  • 构建compilation参数,创建compilation之前要创建一个普通工厂函数

  • 执行正在编译的钩子函数
  • 创建一个compilation对象,执行thisCompilation和compilation钩子函数
  • 创建完成compilation对象后,执行make这个钩子函数
  • make钩子函数执行内部,会去执行compilation对象的seal封装代码块的函数
  • seal方法应该传入一个回调函数,回调函数内部首先执行afterCompile的钩子函数
  • afterCompiled钩子函数中执行run方法中传入函数

    1. newCompilationParams() { // 创建compilation参数
    2. return {
    3. // 在创建compilation之前创建一个普通工厂函数
    4. normalModuleFactory: new NormalModuleFactory();
    5. }
    6. }
    7. newCompilation(params) { // 创建依次新的编译
    8. const compilation = new Compilation(this); // 创建compilation对象
    9. // 执行thisCompilation钩子函数
    10. this.hooks.thisCompilation.call(compilation, params);
    11. this.hooks.compilation.call(compilation, params);// 执行compilation钩子函数
    12. return compilation;
    13. }
    14. compiler(onCompiled) { // 编译执行函数
    15. const params = new newCompilationParams();
    16. this.hooks.compile.call(params); // 执行开始编译的钩子函数
    17. const compilation = this.newCompilation(params); // 创建compilation对象
    18. // 执行make这个钩子函数
    19. this.hooks.make.callAsync(compilation, err => {
    20. // 执行compilation对象的seal封装代码块函数
    21. compilation.seal(() => {
    22. // 执行编译完成后的钩子函数
    23. this.hooks.afterCompile.callAsync(compilation, err => {
    24. onCompiled(err, compilation);
    25. })
    26. })
    27. })
    28. }

    4、执行onCompiled方法文件写入

    当conpiler方法中执行afterCompile钩子函数时,内部会执行一个onCompiled方法,会将compilation编译对象传入,最后根据conpilation对象进行磁盘文件写入。

  • 执行emit这个钩子函数,内部使用mkdir这个包,创建目录,执行回调函数

  • 回调函数中,拿到所有compilation的构建资源,拿到输出的目录
  • 遍历所有构建完成的资源,拿到文件资源,确定好写入的文件名称,调用写入系统写入磁盘中
  • 最后构建完成
    1. emitAssets(compilation, callback) { // 写入文件的方法
    2. const emitFiles = () => {
    3. const assets = compilation.assets;
    4. const ouputPath = this.options.output.path;
    5. for (let file of assets) {
    6. const source = assets[file];
    7. const targetPath = path.posix.join(ouputPath, file);
    8. this.outputFileSystem.writeFileSync(targetPath, targetPath, 'utf8');
    9. }
    10. callback();
    11. }
    12. this.hooks.emit.callAsync(compilation, () => {
    13. mkdir(this.options.output.path, emitFiles);
    14. })
    15. }

    五、创建compilation对象

    创建compilation类
    ```javascript const async = require(‘neo-async’);

class Compilation { constructor(compiler) { this.compiler = compiler; this.options = compiler.options; this.context = compiler.context; this.entris = []; // 入口的数组,放着所有入口的模块 this.modules = []; // 模块的数组,这里放着所有的模块 this._modules = {}; // key是模块的id,值是模块对象 this.chunks = []; // 这里是所有的代码块 this.files = []; // 这里是本次编译产出的所有文件名称 this.assets = {}; // 这里是所有的资源 this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.hooks = { // 当构建完成一个模块后会触发这个钩子 successModules: new SyncHook([‘module’]), seal: new SyncHook(), // 封装代码块 beforeChunks: new SyncHook(), // 生成代码块之前 afterChunk: new SyncHook() // 生成代码块之后 } } }

  1. <a name="lIWEk"></a>
  2. #### 1、执行addEntry方法
  3. <a name="ksIvn"></a>
  4. ##### 在确定入口的时候注册了一个make钩子函数,当compiler执行compiler时候会触发make钩子执行,在注册make钩子函数中,会调用compilation的addEntry方法进行模块的解析
  5. - 创建一个普通模块工厂对象,调用create方法生成模块信息
  6. - 把模块的路径作为模块的id保存
  7. - 把生成的模块放入入口信息数组、模块信息数组中
  8. - 执行模块的buildModule方法,开始执行编译
  9. - 当编译执行完成之后,看是否有依赖的模块,如果有依赖的模块进行递归解析
  10. - 内部有一个机制,就是一个入口依赖的文件,解析后所有模块的name都和入口文件的name相同
  11. ```javascript
  12. const parser = (source) => {
  13. return babylon.parese(source, {
  14. sourceType: 'modules',
  15. plugins: ['dynamicImport']
  16. })
  17. }
  18. addEntry(context, entry, name, callback) {
  19. // 增加编译模块链
  20. this._addModuleChain(context, entry, name, (err, module) => {
  21. callback(err, module);
  22. });
  23. }
  24. _addModuleChain(context, rawRequest, name, callback) {
  25. this.createModule({
  26. name,
  27. context,
  28. rawRequest,
  29. resource: path.posix.join(context, rawRequest),
  30. parser
  31. }, entryModule => this.entries.push(entryModule), callback);
  32. }
  33. createModule(data, addEntryCallback, callback) {
  34. // 创建一个模块工厂
  35. const normalModuleFactory = new NormalModuleFactory();
  36. const module = normalModuleFactory.create(data);
  37. module.moduleId = './' + path.posix.relative(this.context, module.resource);
  38. addEntryCallback && addEntryCallback(module); // 添加一个入口文件
  39. this.modules.push(module); // 添加一个模块
  40. this._module[module.moduleId] = module; // 保存对应信息
  41. this.buildModule(module, (err, module) => {
  42. if (module.dependencies.length > 0) {
  43. this.processModuleDependencies(module, err => {
  44. callback(err, module);
  45. })
  46. } else {
  47. callback(err, module);
  48. }
  49. })
  50. }
  51. processModuleDependencies(module, callback) {
  52. // 获取当前模块的依赖模块
  53. const dependencies = module.dependencies;
  54. // 遍历依赖模块,全部开始编译,当所有模块全部编译完成后才会调用callback
  55. async.forEach(dependencies, (dependency, done) => {
  56. const { name, context, resource, moduleId } = dependency;
  57. this.createModule({
  58. name,
  59. context,
  60. rawRequest: './' + path.posix.relative(context, resource),
  61. resource,
  62. parser,
  63. moduleId
  64. }, null, done);
  65. }, callback);
  66. }
  67. buildModule(module, callback) {
  68. // 这一步就是真正的执行编译的过程
  69. module.build(this, err => {
  70. // 意味着一个module已经完成编译
  71. this.hooks.successModule.call(module);
  72. callback(err, module);
  73. })
  74. }

2、执行编译过程

在执行addEntry方法确定入口的时候,创建了一个普通模块工厂,这个类内部会执行构建的方法
  • 创建一个普通模块工厂,调用create方法,返回一个普通模块的实例。
  • 创建一个普通模块实例,调用build方法进行构建.
  • 根据文件路径读取文件内的内容,将文件内容赋值给当前实例。
  • 调用parser方法将文件内容转成抽象语法树(ast),遍历抽象语法树。
  • 将节点require或者import节点进行转换,并且收集内部依赖的模块放入依赖队列中后,编译完成。

    class NormalModuleFactory {
      create(data) {
        return new NormalModule(data);
    }
    }
    class NormalModule {
      constructor({ name, context, rawRequest, resource, parser, moduleId }) {
        this.name = name; // 打包出来的名字
      this.context = context; // 跟路径
      this.rawRequest = rawRequest; // 请求文件路径
      this.resource = resource; // 资源路径
      this.parser = parser; // 转成ast的babel方法
      this.moduleId = moduleId; // 模块的id
      this._source; // 模块对应的源代码
      this.ast; // 抽象语法树
      this.dependencies = []; // 当前模块的依赖信息
      this.moduleId = moduleId || ('./' + path.posix.relative(context, resource))
    }
    build(compilation, callback) {
      this.doBuild(compilation, () => {
          this.ast = this.parser.parse(this._source);
        traverse(this._ast, {
            CallExpression: (nodePath) => {
            let node = nodePath.node; // 获取节点
            if (node.callee.name === 'require' || node.callee.name === 'important') { // 如果方法名是require方法的话
              node.callee.name = '__webpack_require__'; // 对方法名称进行修改,从require改成__webpack_require__
              console.log(node.callee.name)
              const moduleName = node.arguments[0].value; // 模块的名称
              const extName = moduleName.split(path.posix.sep).pop().indexOf('.') === -1 ? '.js' : ''; // 进行后缀补全,可能的扩展名
              // 获取依赖模块的绝对路径
              const depResource = path.posix.join(path.posix.dirname(this.resource), moduleName + extName);
              // 获取模块的ID,
              const depModuleId = './' + path.posix.relative(this.context, depResource);
              node.arguments = [types.stringLiteral(depModuleId)]; // 把require模块路径从./title改成./src/title.js
              const chunk = {
                name: this.name,
                context: this.context, // 根目录
                rawRequest: moduleName, // 模块的原始路径
                moduleId: depModuleId, // 模块id,相对于根目录的相对路径,以./开头
                resource: depResource // 依赖模块的绝对路径
              };
              this.dependencies.push(chunk)
            }
          }
        })
        this._source = generate(this._ast);
        callback();
      })
    
    }
    doBuild(compilation, callback) {
        compilation.inputFileSystem.readFile(this.resource, 'utf8', (err, source) => {
          this._source = source;
        callback();
      })
    }
    }
    

    3、封装模块

    在compiler执行中,当make执行完成之后会执行compilation实例上的seal方法对模块进行封装。
  • 首先执行seal、beforeChunk两个钩子函数函数。

  • 遍历入口队列,拿到每一个入口,用入口生成一个chunk,将这个chunk放入chunks队列。
  • 找到和当前模块name相同的模块,将模块放入当前chunk的modules队列中。
  • 执行afterChunk这个钩子函数,遍历chunks队列,根据chunkName和modules的依赖关系,根据ejs模版生成资源。
    class Chunk {
    constructor(entryModule) {
      this.entryModule = entryModule; // 此代码块的入口模块
      this.name = entryModule.name; // 代码块的名字
      this.files = []; // 这个代码块生成了那些文件
      this.modules = []; // 这个代码块包含那些模块
    }
    }
    seal(callback) {
    this.hooks.seal.call();
    this.hooks.beforeChunk.call();
    for (const entryModule of this.entris) {
        const chunk = new Chunk(entryModule);
      this.chunks.push(chunk);
      chunk.modules = this.modules.find(a => a.name === chunk.name);
    }
    this.hooks.afterChunk.call();
    this.createAssets()
    callback()
    }
    createAssets() {
      for(let i = 0; i < this.chunks.length; i++) {
        const chunk = chunks[i];
      const file = chunk.name + '.js'; // 拿到了文件名字
      chunk.files.push(file);
      const source = mainRender({
        entryModuleId: chunk.entryModule.moduleId, // 模块的名字
        modules: chunk.modules // 代码块对应的模块数组
      })
      this.assets[file] = source;
      this.files.push(file);
    }
    }
    

    4、普通模块的ejs模版

    根据ejs模版生成
    (function (modules) {
    var installedModules = {};
    function __webpack_require__(moduleId) {
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }
      var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      };
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
      module.l = true;
      return module.exports;
    }
    return __webpack_require__("<%-entryModuleId%>");
    })
    ({
    <%
    for(let module of modules)
    {%>
    "<%-module.moduleId%>":
    (function (module, exports, __webpack_require__) {
      <%-module._source%>
    }),
      <%}
        %> 
    });
    

    5、将模版内容写入文件

    compilation的seal方法是在compiler中执行的,当执行完成之后会执行afterCompile这个钩子函数,会调用emitAssets方法把转换完成的文件写入硬盘中。执行过程到第四步中的第4个过程。

总结:

1、基本流程

Webpack的基本流程可以分成三个阶段:
  • 准备阶段:主要任务是创建Compiler和Compilation对象。
  • 编译阶段:这个阶段是完成modules的解析,并且生成chunks。module解析主要是创建实例、loaders应用和依赖的收集,chunks生成主要是找到每个chunks所包含的modules。
  • 产出阶段:这个阶段主要是根据chunks生成最终的文件。

    在产出阶段中,主要有三个步骤:模版hash的生成、模版渲染chunk、生成文件。细化到具体的代码层次,大概可以分为:
  • 初始化参数:包含从配置文件和shell中读取和合并参数,然后得到最终的参数,shell中的参数由于配置文件的参数。

  • 使用上一个得到的参数实例化一个Compiler类,注册所有的插件,给对应的webpack构建生命周期绑定对应的Hook。
  • 开始编译:执行Compiler类的run方法执行编译,compiler.run方法调用compiler.compile,在compile内实例化一个Compilation类。

    Compilation是构建打包的事情,主要内容有:
  • 查找入口:根据entry入口配置,找到全部入口文件。

  • 编译模块:根据文件类型和loader配置,使用对应的loader对文件进行转换处理。
  • 解析文件的AST语法树,找出文件的依赖关系,递归编译依赖的模块。
    递归完后得到每个文件的最终结果,根据entry配置生成chunk,输入所有chunk到对应的output路径中。
    Webpack的工作流程中,Tapable始终贯穿其中,Tapable各种钩子组成了Webpack完整的生命周期。
    总结:Webpack打包流程从配置文件的读取开始,分别经过准备阶段、modules产出阶段、chunks产出阶段、bundle产出阶段。在各自阶段,分别有不同的角色参与,整个Webpack打包流程是通过compiler控制的,而每次打包的过程是通过compilation来控制的。在普通模式下,webpack的compiler和compilation是一一对应的关系;watch模式下,Webpack的Compiler会因为文件变化而产生多次打包流程,所以Compiler和Compilation是一对多的关系。

    2、流程图如下:

    image.png

    参考:

    webpack文档
    了不起的 Webpack 构建流程学习指南
    webpack 工程化实践总结之webpack 核心模块、Compiler 和 Compilation、基本流程和 HMR