webpack 流程概览

1,127 阅读10分钟

准备

tapable

Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 tapableWebpack 中最核心的,负责编译的 Compiler 和负责创建 bundlesCompilation 都是 tapable 的实例。 tapable 暴露了许多钩子类,这些类可以用来为插件创建钩子。

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
} = require("tapable"); 

enhanced-resolve

github.com/webpack/enh…

enhanced-resolve 是一个异步的高度可配置的解析文件路径的库。使用 enhanced-resolvewebpack 能解析三种文件路径:绝对路径、相对路径和模块路径。Webpack 可以根据配置中的参数和默认配置生成 enhanced-resolve 的配置,然后利用 enhanced-resolve 库来解析各种路径。

acorn & ast estree

github.com/acornjs/aco…

github.com/estree/estr…

acorn 是一个完全使用 javascript 实现的,小型且快速的 javascript 解析器。将待解析的代码传给 acorn.parse 即可输出遵循 Estree 规范的 astEstree是一种 json 风格的 ast ,现在流行的 bableeslint的实现也是基于 Estree。 比如 let code = 1 + 1; 解析成 ast 后是:

{
  "type": "Program",
  "start": 0,
  "end": 11,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 5,
      "end": 10,
      "expression": {
        "type": "BinaryExpression",
        "start": 5,
        "end": 10,
        "left": {
          "type": "Literal",
          "start": 5,
          "end": 6,
          "value": 1,
          "raw": "1"
        },
        "operator": "+",
        "right": {
          "type": "Literal",
          "start": 9,
          "end": 10,
          "value": 1,
          "raw": "1"
        }
      }
    }
  ],
  "sourceType": "script"
}

loader-runner

github.com/webpack/loa…

juejin.cn/post/684490…

webpack 用来控制执行 loader 的库。

neo-async & async

github.com/suguru03/ne…

github.com/caolan/asyn…

async文档

Async 库是一个异步调用的工具库,提供了大量控制异步流程的方法。neo-asyncasync 的一个增强库,在性能上做了优化。webpack 中有很多利用 neo-async 库并行执行多个方法的场景。

demo

本例所用 webpack 版本号是 4.44.2

// a.js (webpack config 入口文件)
import add from './b.js'
add(1, 2)
import('./c').then(del => del(1, 2))

-----

// b.js
import mod from './d.js'
export default function add(n1, n2) {
  return n1 + n2
}
mod(100, 11)

-----

// c.js
import mod from './d.js'
mod(100, 11)
import('./b.js').then(add => add(1, 2))
export default function del(n1, n2) {
  return n1 - n2
}

-----

// d.js
export default function mod(n1, n2) {
  return n1 % n2
}

webpack 相关的配置:

{
  mode: "development",
  entry: {
    app: "./debug/src/a.js"
  },
  devtool: "none",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].[chunkhash].js"
  },
  module: {
    rules: [
      // 前置
      { enforce: "pre", test: /\.js$/, use: "babel-loader" },
      // 正则匹配
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"]
          }
        }
      },
      // 后置
      { enforce: "post", test: /\.js$/, use: "babel-loader" }
    ]
  }
};

其中a.jswebpack config 当中配置的entry入口文件,a.js依赖 b.js/c.js,而b.js依赖d.jsc.js依赖d.js/b.js

最终通过webpack编译后,将会生成2个chunk文件,其中:

  • app.hash.js 包含了 webpack runtime 代码和 a.js/b.js/d.js 的模块代码;
  • 0.bundle.hash.js包含了异步加载的c.js的代码。

编译过程

编译前的准备

首先执行

const compiler = webpack(config);

创建一个编译流程,并传入上文中的配置。在 webpack 函数中,首先执行

const webpackOptionsValidationErrors = validateSchema(
  webpackOptionsSchema,
  options
);

对传入的配置做一个类型的校验,然后执行

if (Array.isArray(options)) {
  compiler = new MultiCompiler(
    Array.from(options).map(options => webpack(options))
  );
} else if (typeof options === "object") {
  options = new WebpackOptionsDefaulter().process(options);
  compiler = new Compiler(options.context);
  // ......
}

如果传入的配置是一个数组,那么会创建多个编译流程。这里先讨论传入的配置是一个对象的情况。之后执行

options = new WebpackOptionsDefaulter().process(options);

将系统默认的配置、配置文件合并。 然后使用合并后的配置,执行

compiler = new Compiler(options.context);

创建 compiler 对象。Compiler 类定义了整个构建的流程,是 Tapable 的扩展类,挂载了一堆钩子 done/runemit/seal 等等。创建了 compiler 对象之后,执行

if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
      plugin.call(compiler, compiler);
    } else {
      plugin.apply(compiler);
    }
  }
}

将配置中的插件注册进来,可以看到注册插件的时候传入了 compiler 实例, compiler 包含整个构建流程的全部钩子,这样插件就可以在 compiler 实例中的各个钩子中注册事件,来进行处理。其中 compiler 的部分钩子会传入 compilation 对象参数,该对象又包含资源构建的很多钩子。

注册完插件后执行

compiler.options = new WebpackOptionsApply().process(options, compiler);

根据 options 的配置不同,注册激活一些默认自带的插件。至此 compiler 对象就创建完成了。

执行

compiler.run()

开始编译流程。在 compiler.compile 方法中,首先执行

const params = this.newCompilationParams();

初始化 compilation 对象的参数

newCompilationParams() {
  const params = {
    normalModuleFactory: this.createNormalModuleFactory(),
    contextModuleFactory: this.createContextModuleFactory(),
    compilationDependencies: new Set()
  };
  return params;
}

NormalModuleFactory 类用于创建一个 normalModule 实例, 一个 normalModule 实例就是一个 module 。在 new NormalModuleFactory 的时候执行了

this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));

这个方法格式化了 loader 的配置,为后面 module 应用 loader 做了准备。

然后执行了

const compilation = this.newCompilation(params);

该方法实例化了一个 compilation 对象,也是扩展于 tapable 类,同样也挂载了一堆钩子。一个 compilation 对象代表了一次资源的构建。

至此编译前的准备已完成。接着调用了 compiler.hook.make 钩子,开始构建模块。

构建模块

compilation.addEntry 方法开始解析入口模块。

resolve路径

从调用 moduleFactory.create 开始,第一步是解析路径。 触发 normalModuleFactory.hooks.resolver 钩子,执行

const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);

得到两个 resolver 对象,这里的 resolver 是前文提到的 enhanced-resolve 的实例,用来异步解析绝对路径。然后解析 inline loader

const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
const noAutoLoaders =
    noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
let elements = requestWithoutMatchResource
    .replace(/^-?!+/, "")
    .replace(/!!+/g, "!")
    .split("!");

之后通过 asyncLib.parallel 并行解析 inline loader 和当前模块的绝对路径。

asyncLib.parallel(
  [
    callback =>
        this.resolveRequestArray(
            contextInfo,
            context,
            elements,  // inline loader
            loaderResolver,
            callback
        ),
    callback => {
        // ......

        normalResolver.resolve(
            contextInfo,
            context,
            resource,
            {},
            (err, resource, resourceResolveData) => {
                if (err) return callback(err);
                callback(null, {
                    resourceResolveData,
                    resource
                });
            }
        );
    }
  ],
  (err, results) => {
  	// ......
  }
)

在回调里拿到解析完成的 inline loader 和当前文件的绝对路径,然后继续解析配置里的 laoder ,执行

const result = this.ruleSet.exec({
  resource: resourcePath,
  realResource:
    matchResource !== undefined
      ? resource.replace(/\?.*/, "")
      : resourcePath,
  resourceQuery,
  issuer: contextInfo.issuer,
  compiler: contextInfo.compiler
});

这里的 this.ruleSet 就是在 new NormalModuleFactory 时在 constructor 里创建的 RuleSet 对象。在 new RuleSet 对象的时候,已经将配置里的 loader 相关配置格式化成了

{
  resource: function(),
  resourceQuery: function(),
  compiler: function(),
  issuer: function(),
  use: [
      {
          loader: string,
          options: string,
          <any>: <any>
      }
  ],
  rules: [<rule>],
  oneOf: [<rule>],
  <any>: <any>,
}

格式的对象,其中 resource 是条件函数,执行这个函数可以筛选出当前模块相对应的 loaderuseloader 和配置。比如 demo 中的配置:

module: {
  rules: [
    // 前置
    { enforce: "pre", test: /\.js$/, use: "babel-loader" },
    // 正则匹配
    {
      test: /\.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env"]
        }
      }
    },
    // 后置
    { enforce: "post", test: /\.js$/, use: "babel-loader" }
  ]
}

格式化后是 在执行 this.ruleSet.exec 后就可以得出当前模块对应的 loader ,如 demo 中入口文件的 a.js ,解析后匹配的结果就是:

在得到匹配的 loader 结果后,将根据前置、普通、后置分类,然后并行解析三组 loader 的路径


asyncLib.parallel(
  [
    this.resolveRequestArray.bind(
      this,
      contextInfo,
      this.context,
      useLoadersPost,  // 后置 loader
      loaderResolver
    ),
    this.resolveRequestArray.bind(
      this,
      contextInfo,
      this.context,
      useLoaders,  // 普通 loader
      loaderResolver
    ),
    this.resolveRequestArray.bind(
      this,
      contextInfo,
      this.context,
      useLoadersPre,  // 前置 loader
      loaderResolver
    )
  ],
  (err, results) => {
    // ......
  }
)

得到路径后,将行内、前置、普通、后置 loader 排序合并,规则是:

  • 正常情况 loader 执行顺序: pre -> normal -> inline -> post
  • 资源路径前使用 xxx!=! 装饰: pre -> inline -> normal -> post
  • 资源路径前使用 -! 装饰: inline -> post
  • 资源路径前使用 ! 装饰: pre -> inline -> post
  • 资源路径前使用 !! 装饰: inline

这样就得到了与模块相匹配的正确顺序的 loader

至此 resolve 过程结束。

编译模块

路径解析完之后,执行

createdModule = new NormalModule(result);

创建一个 module ,每一个 module 都是一个 NormalModule 对象。从 NormalModule.doBuild 方法开始编译模块。首先执行

const loaderContext = this.createLoaderContext(
  resolver,
  options,
  compilation,
  fs
);

生成一个给所有 loader 使用的上下文对象,然后执行

runLoaders(
  {
    resource: this.resource,
    loaders: this.loaders,
    context: loaderContext,
    readResource: fs.readFile.bind(fs)
  },
  (err, result) => {
  	// ......
  }
)

module 传递给 loader 处理。这里的 runloader 是上文中 loader-runner 库中的方法,作用是按顺序执行 loader 。 在回调里拿到经过 loader 处理后的模块,然后执行

const result = this.parser.parse(
  this._ast || this._source.source(),
  {
    current: this,
    module: this,
    compilation: compilation,
    options: options
  },
  (err, result) => {
    if (err) {
      handleParseError(err);
    } else {
      handleParseResult(result);
    }
  }
);

开始解析 module 。在 parse 方法中,首先执行

ast = Parser.parse(source, {
  sourceType: this.sourceType,
  onComment: comments
});

module 解析成遵循 Estree 规范的 ast ,这里解析 ast 是使用的 acorn 库。以 a.js 为例,转化成 ast 后如图:

接下来就是对这个树进行遍历了,执行

if (this.hooks.program.call(ast, comments) === undefined) {
    this.detectMode(ast.body);
    this.prewalkStatements(ast.body);
    this.blockPrewalkStatements(ast.body);
    this.walkStatements(ast.body);
}

这几行代码逐条处理 ast ,解析模块中的依赖关系。

this.hooks.program.call(ast, comments)这个hook中,会触发HarmonyDetectionParserPluginUseStrictPlugin 这两个插件的回调。

  • HarmonyDetectionParserPlugin 中,如果代码中有 import 或者 export 或者类型为 javascript/esm,那么会增加 HarmonyCompatibilityDependencyHarmonyInitDependency 依赖。
  • UseStrictPlugin 用来检测文件是否有 use strict,如果有,则增加一个 ConstDependency 依赖。

detectStrictMode中,检测当前执行块是否有 use strict ,如果有则执行

this.scope.isStrict = true

prewalkStatements 函数负责处理变量。遍历每个节点,根据 statement.type 的不同,调用不同的处理函数。比如 a.js 的第一个节点是 importDeclaration ,处理这个节点,首先触发 HarmonyImportDependencyParserPlugin 插件,增加 ConstDependencyHarmonyImportSideEffectDependency 依赖。然后触发 HarmonyImportDependencyParserPlugin 插件, 执行

parser.scope.renames.set(name, "imported var");

设置 add 的值为 imported var

walkStatements 这一步则会深入函数内部,对于函数内部的内容调用

this.detectMode(expression.body.body);
this.prewalkStatement(expression.body);
this.walkStatement(expression.body);

循环处理函数中的依赖关系。比如第三句 import('./c').then(del => del(1, 2)) ,这是一个异步加载依赖的语句,也是一个函数调用语句,在 walkStatements 这一句处理这一句时,会进入到 ImportParserPlugin 这个插件中,生成一个 ImportDependenciesBlock 类型的依赖,并加入到 module.blocks 中。

// ImportParserPlugin
const depBlock = new ImportDependenciesBlock(
    param.string,
    expr.range,
    Object.assign(groupOptions, {
        name: chunkName
    }),
    parser.state.module,
    expr.loc,
    parser.state.module
);
// parser.state.current 为当前处理的 module 
parser.state.current.addBlock(depBlock);

当所有 ast 被解析完之后,就得到了当前模块所有的依赖关系,普通依赖保存在 module.dependencies 中,异步依赖保存在 module.blocks 中。对于 a.js 来说,得到的依赖是:

然后执行

this._initBuildHash(compilation);

生成 hash 值,保存在 this._buildHash 中,到此一个模块就编译完成了。

回到回调中,执行 afterBuild 回调,然后调用

compilation.processModuleDependencies(module, err => {
  if (err) return callback(err);
  callback(null, module);
});

根据解析出来的依赖关系,循环编译依赖的模块。

至此所有模块编译完成。最终所有的文件就都转化为了 module ,并且会得到 module 和依赖的 dependencies ,后续生成 chunk 和生成打包文件代码时会使用到。

生成chunk

回到 compiler.hook.make 的回调,执行 compilation.seal 函数。 seal 方法中触发了大量的 hook ,为侵入 webpack 构建流程提供了海量钩子。

然后开始生成 chunk 。在这个过程当中首先遍历 config 当中配置的入口 module,为每个入口 module 创建一个空 chunk。之后实例化一个entryPoint,这个 entryPoint 是一个包含 runtimeChunkchunkGroup ,每个 chunkGroup 可以包含多的 chunk

然后调用

GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);

建立起 modulechunkentrypoint 的关系。

然后调用

buildChunkGraph(
  this,
  /** @type {Entrypoint[]} */ (this.chunkGroups.slice())
);

建立起 chunk 与其他依赖之间的关系。在 buildChunkGraph 方法中,调用了三个方法

// PART ONE
visitModules(
  compilation,
  inputChunkGroups,
  chunkGroupInfoMap,
  blockConnections,
  blocksWithNestedBlocks,
  allCreatedChunkGroups
);

// PART TWO
connectChunkGroups(
  blocksWithNestedBlocks,
  blockConnections,
  chunkGroupInfoMap
);

// Cleaup work
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);

visitModules 中,首先执行

const blockInfoMap = extraceBlockInfoMap(compilation);

-----------------------------------

const extraceBlockInfoMap = compilation => {
  // ......

  for (const module of compilation.modules) {  // 循环处理每个模块
    blockQueue = [module];
    currentModule = module;
    while (blockQueue.length > 0) {
      block = blockQueue.pop();
      blockInfoModules = new Set();
      blockInfoBlocks = [];

      // ......

      // 对于同步模块,缓存进blockInfoModules数组
      if (block.dependencies) {
        for (const dep of block.dependencies) iteratorDependency(dep);
      }

     // 对于异步模块,缓存进blockInfoBlocks和blockQueue数组
      if (block.blocks) {
        for (const b of block.blocks) iteratorBlockPrepare(b);
      }

      const blockInfo = {
        modules: blockInfoModules,
        blocks: blockInfoBlocks
      };
      blockInfoMap.set(block, blockInfo); // 将模块关系缓存进blockInfoMap
    }
  }

  return blockInfoMap;
};

在这个方法中,对所有 module 进行一次遍历,在遍历 module 的过程中,会对这个 moduledependencies 依赖进行处理,获取这个 module 的依赖模块,同时还会处理这个 moduleblocks(异步加载的模块)。遍历的过程结束后会建立起基本的 module graph,包含普通的 module 及异步 module(block),最终存储到一个 map(blockInfoMap) 中,代表着模块间的依赖关系。对于 demo,可得到关系图:

此时只是得到了空的 chunk,和各个单独的模块之间的依赖关系,chunkmodule 之间尚未关联起来。接下来就是在 chunk 中添加关联的依赖。比如 a.js ,关键的步骤有:

  1. 处理 a.js ,拿到 a.js 中的依赖关系 const blockInfo = blockInfoMap.get(block);
  2. 对于依赖的普通模块 b.js,如果当前 chunk 中没有,则加入 chunk 中,并缓存起来;对于异步模块 c.js ,则为 c.js 新建一个 chunk,并添加进循环中;
  3. 在当前 chunk 中,循环处理 b.js 中的依赖;
  4. 在新 chunk 中,循环处理 c.js 中的依赖。

当循环处理完所有 module 时,chunk 图也生成了,对于 demo 来说,生成的 chunk 图如下

然后执行

connectChunkGroups(
  blocksWithNestedBlocks,
  blockConnections,
  chunkGroupInfoMap
);

建立起 chunkGroup 之间的父子关系。chunk2 中没有 module ,自然跟其他chunk没有关系。

然后执行

cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);

清除空的没有父子关系的 chunkGroup

最终生成的 chunk 图为:

生成文件

生成hash

chunk 生成后,调用 seal 钩子中的

this.createHash();

生成 hash 值。这个方法主要做了两件事,为 module 生成 hash,和为 chunk 生成 hash

module hash 的生成代码如下:

createHash() {
   //......
  const modules = this.modules;
  for (let i = 0; i < modules.length; i++) {
    const module = modules[i];
    const moduleHash = createHash(hashFunction);
    module.updateHash(moduleHash);
    module.hash = moduleHash.digest(hashDigest);
    module.renderedHash = module.hash.substr(0, hashDigestLength);
  }
  //......
}

其中关键的 updateHash 方法,封装在每个 module 类的实现中。对于 normalModule 来说,这个方法是:

updateHash(hash) {
  hash.update(this._buildHash);
  super.updateHash(hash);
}

其中 _buildHash 是模块编译完成时生成的 module hashsuper.update 代码是

updateHash(hash) {
    hash.update(`${this.id}`);
    hash.update(JSON.stringify(this.usedExports));
    super.updateHash(hash);
}

可以看到 module id 和被使用到的 exports 信息也更新进了 hash 中。super.update 代码是

updateHash(hash) {
  for (const dep of this.dependencies) dep.updateHash(hash);
  for (const block of this.blocks) block.updateHash(hash);
  for (const variable of this.variables) variable.updateHash(hash);
}

各个依赖具体有哪些信息要写入 hash ,由 xxxDependency.jsupdateHash 方法决定。 可以看到,一个 modulehash 包含了:

  • 每个 module 中自己特有的需要写入 hash 中的信息;
  • module id 和被使用到的 exports 信息;
  • 依赖的信息。

chunk hash 的生成代码如下:

createHash() {
  //......
  for (let i = 0; i < chunks.length; i++) {
    const chunk = chunks[i];
    const chunkHash = createHash(hashFunction);
    try {
      // ......
      chunk.updateHash(chunkHash);
      const template = chunk.hasRuntime()
        ? this.mainTemplate
        : this.chunkTemplate;
      template.updateHashForChunk(
        chunkHash,
        chunk,
        this.moduleTemplates.javascript,
        this.dependencyTemplates
      );
      this.hooks.chunkHash.call(chunk, chunkHash);
      chunk.hash = /** @type {string} */ (chunkHash.digest(hashDigest));
      hash.update(chunk.hash);
      chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
      this.hooks.contentHash.call(chunk);
    } catch (err) {
      this.errors.push(new ChunkRenderError(chunk, "", err));
    }
  }
  // ......
}

第一步是执行

chunk.updateHash(chunkHash);

------------------------------------------

updateHash(hash) {
  hash.update(`${this.id} `);
  hash.update(this.ids ? this.ids.join(",") : "");
  hash.update(`${this.name || ""} `);
  for (const m of this._modules) {
    hash.update(m.hash);
  }
}

ididsname 和其包含的所有 modulehash 信息写入。

然后写入生成 chunk 的模板信息:

const template = chunk.hasRuntime()
                  ? this.mainTemplate
                  : this.chunkTemplate;
template.updateHashForChunk(
  chunkHash,
  chunk,
  this.moduleTemplates.javascript,
  this.dependencyTemplates
);

webpacktemplate 分为两种:mainTemplate 最终会生成包含 runtime 的代码,和 chunkTemplate。我们主要看 mainTemplateupdateHashForChunk 方法

updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
  this.updateHash(hash);
  this.hooks.hashForChunk.call(hash, chunk);
  // ......
}

----------------------------------------------------

updateHash(hash) {
  hash.update("maintemplate");
  hash.update("3");
  this.hooks.hash.call(hash);
}

这里会将 template 类型 maintemplate 写入,然后触发的 hash 钩子和 hashForChunk 钩子会将一些文件的输出信息写入。

将相关信息都存入 hashbuffer 之后,调用 digest 方法生成最终的 hash,然后从中截取出需要的长度,chunkhash 就得到了。

this.fullHash = /** @type {string} */ (hash.digest(hashDigest));
this.hash = this.fullHash.substr(0, hashDigestLength);

生成文件

hash 值生成之后,会调用

this.createChunkAssets(); 

来决定最终输出到每个 chunk 当中对应的文本内容是什么。首先根据 chunk 是否包含有 webpack runtime 代码来决定使用的渲染模板是mainTemplate 还是 chunkTemplate。其中 mainTemplate 除了生成普通 module 的代码之外,还包含了 runtime 代码的生成工作,chunkTemplate 主要用于普通 chunk 的代码生成。

const template = chunk.hasRuntime()
                  ? this.mainTemplate
                  : this.chunkTemplate;

然后通过 getRenderManifest 获取到 render 需要的内容。

const manifest = template.getRenderManifest({
  chunk,
  hash: this.hash,
  fullHash: this.fullHash,
  outputOptions,
  moduleTemplates: this.moduleTemplates,
  dependencyTemplates: this.dependencyTemplates
}); 

然后执行

source = fileManifest.render();

用于生成代码。 以 demo 的入口 chunk 为例,因为入口 chunk 包含了 runtime 代码,所以使用mainTemplate ,且 mainTemplate 的代码生成过程中包含了生成普通 module 代码,所以我们以 mainTemplate 为例来说明代码生成过程:

// mainTemplate manifest 的 render 方法
render: () =>
  compilation.mainTemplate.render(
    hash,
    chunk,
    moduleTemplates.javascript,
    dependencyTemplates
  ),
          
--------------

render(hash, chunk, moduleTemplate, dependencyTemplates) {
  // 生成runtime代码
  const buf = this.renderBootstrap(
    hash,
    chunk,
    moduleTemplate,
    dependencyTemplates
  );
  // 注册在 MainTemplate 里的方法,包装了 runtime 代码,调用 this.hooks.modules.call 生成 module 代码
  let source = this.hooks.render.call(
    new OriginalSource(
      Template.prefix(buf, " \t") + "\n",
      "webpack/bootstrap"
    ),
    chunk,
    hash,
    moduleTemplate,
    dependencyTemplates
  );
  // ......
  return new ConcatSource(source, ";");
}

调用完 hooks.render 后,即得到了包含 runtime bootstrap 代码的 chunk 代码,最终返回一个 ConcatSource 类型实例。 最终的代码会被保存在这个 ConcatSource 类的 children 中。

重点来看一下 module 的代码是怎么生成的。在 this.hooks.render.call 中,执行this.hooks.modules.call 生成 module 代码

this.hooks.render.tap(
  "MainTemplate",
  (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
    const source = new ConcatSource();
    // ......
    source.add(
      this.hooks.modules.call(
        new RawSource(""),
        chunk,
        hash,
        moduleTemplate,
        dependencyTemplates
      )
    );
    source.add(")");
    return source;
  }
);

这个钩子是在 JavascriptModulesPlugin 中注册的,执行 Template.renderChunkModules 方法:

compilation.mainTemplate.hooks.modules.tap(
  "JavascriptModulesPlugin",
  (source, chunk, hash, moduleTemplate, dependencyTemplates) => {
    return Template.renderChunkModules(
        chunk,
        m => typeof m.source === "function",
        moduleTemplate,
        dependencyTemplates,
        "/******/ "
    );
  }
);

--------------------------------------------------------

static renderChunkModules(
  chunk,
  filterFn,
  moduleTemplate,
  dependencyTemplates,
  prefix = ""
) {
  // ......
  const allModules = modules.map(module => {
    return {
      id: module.id,
      source: moduleTemplate.render(module, dependencyTemplates, {
          chunk
      })
    };
  });
  // ......
  return source;
}

---------------------------------

render(module, dependencyTemplates, options) {
  try {
    const moduleSource = module.source(
      dependencyTemplates,
      this.runtimeTemplate,
      this.type
    );
    const moduleSourcePostContent = this.hooks.content.call(
      moduleSource,
      module,
      options,
      dependencyTemplates
    );
    const moduleSourcePostModule = this.hooks.module.call(
      moduleSourcePostContent,
      module,
      options,
      dependencyTemplates
    );
    const moduleSourcePostRender = this.hooks.render.call(
      moduleSourcePostModule,
      module,
      options,
      dependencyTemplates
    );
    return this.hooks.package.call(
      moduleSourcePostRender,
      module,
      options,
      dependencyTemplates
    );
  } catch (e) {
    e.message = `${module.identifier()}\n${e.message}`;
    throw e;
  }
}

module.source 中,执行 this.generator.generate方法:

source(dependencyTemplates, runtimeTemplate, type = "javascript") {
  // ......

  const source = this.generator.generate(
    this,
    dependencyTemplates,
    runtimeTemplate,
    type
  );

  // ......
  return cachedSource;
}

这里的 generator 是在 NormalModuleFactory 创建 NormalModule 的过程时创建的。generate 中执行 this.sourceBlock

generate(module, dependencyTemplates, runtimeTemplate) {
  // ......
  this.sourceBlock(
    module,
    module,
    [],
    dependencyTemplates,
    source,
    runtimeTemplate
  );

  return source;
}

sourceBlock 中,首先循环处理了 dependency

sourceBlock(
  module,
  block,
  availableVars,
  dependencyTemplates,
  source,
  runtimeTemplate
) {
  for (const dependency of block.dependencies) {
    this.sourceDependency(
      dependency,
      dependencyTemplates,
      source,
      runtimeTemplate
    );
  }
  // ......
}

------------------------------

sourceDependency(dependency, dependencyTemplates, source, runtimeTemplate) {
  const template = dependencyTemplates.get(dependency.constructor);
  // ......
  template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
}

也就是说要挨个执行 dependency 中的 apply 方法。对 a.js 来说,第一个依赖是 HarmonyCompatibilityDependency ,它的 apply 方法是

apply(dep, source, runtime) {
  const usedExports = dep.originModule.usedExports;
  if (usedExports !== false && !Array.isArray(usedExports)) {
    const content = runtime.defineEsModuleFlagStatement({
        exportsArgument: dep.originModule.exportsArgument
    });
    source.insert(-10, content);
  }
}

也就是在代码中插入 __webpack_require__.r(__webpack_exports__);__webpack_require__.r 方法会为 __webpack_exports__ 对象增加一个 __esModule 属性,将其标识为一个 es module

然后是 HarmonyInitDependency 依赖,它的 apply 方法会遍历所有的 dependency ,并执行 dependencytemplate.harmonyInit 方法。在这个过程中 a.js 代码

import add from './b.js'

所对应的 HarmonyImportSideEffectDependencyHarmonyImportSpecifierDependency 中的 template.harmonyInit 方法将会在这时执行,然后得到下面这句

/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ "./debug/src/b.js");

然后是 ConstDependencyHarmonyImportDependency,在这里啥也没干。

然后是 HarmonyImportSpecifierDependency ,得到

Object(_b_js__WEBPACK_IMPORTED_MODULE_0__["default"])

对应

add()

这一句中的 add。 至此 dependency 就处理完了。

然后在 sourceBlock 中循环处理了异步模块:

sourceBlock(
    module,
    block,
    availableVars,
    dependencyTemplates,
    source,
    runtimeTemplate
) {
	// ......
  for (const childBlock of block.blocks) {
    this.sourceBlock(
      module,
      childBlock,
      availableVars.concat(vars),
      dependencyTemplates,
      source,
      runtimeTemplate
    );
  }
  // ......
}

可以看到处理异步模块就是把 blocks 中的依赖拿出来,再调用 sourceBlock 循环处理,这里就不赘述了。

当模块解析完毕,将得到一个 ReplaceSource 对象,这个类包含一个 replacements 的数组,里面存放了对源码转化的操作,数组中每个元素结构如下:

[替换源码的起始位置,替换源码的终止位置,替换的最终内容,优先级]

至此,针对源码的操作已经得到了。 然后执行

const moduleSourcePostRender = this.hooks.render.call(
  moduleSourcePostModule,
  module,
  options,
  dependencyTemplates
);

将模块代码包装成函数,这个钩子函数主要的工作就是完成对上面已经完成的 module 代码进行一层包裹,包裹的内容主要是 webpack 自身的一套模块加载系统,包括模块导入,导出等,每个 module 代码最终生成的形式为:

/***/ (function(module, __webpack_exports__, __webpack_require__) {

  // module 最终生成的代码被包裹在这个函数内部

/***/ })

然后执行

return this.hooks.package.call(
  moduleSourcePostRender,
  module,
  options,
  dependencyTemplates
);

这个 hook 的作用是添加注释。

至此,一个 module 的包装好的代码就完成了。

回到 renderChunkModules ,得到所有 module 的之后,先判断是否有边界,如果有边界,则将 module 代码组装成数组的形式,否则组装成对象的形式,最后得到:

{

  /***/ "./debug/src/a.js":
  /*!********************************!*\
    !*** (webpack)/debug/src/a.js ***!
    \********************************/
  /*! no exports provided */
  /***/ (function(module, __webpack_exports__, __webpack_require__) {

    // 模块内容......

  /***/ }),

  /***/ "./debug/src/b.js":
  /*!********************************!*\
    !*** (webpack)/debug/src/b.js ***!
    \********************************/
  /*! exports provided: default */
  /***/ (function(module, __webpack_exports__, __webpack_require__) {

    // 模块内容......

  /***/ }),

  /***/ "./debug/src/d.js":
  /*!********************************!*\
    !*** (webpack)/debug/src/d.js ***!
    \********************************/
  /*! exports provided: default */
  /***/ (function(module, __webpack_exports__, __webpack_require__) {

  // 模块内容......

  /***/ })

/******/ }

至此,所有代码已完成。

输出文件

经历了上面所有的阶段之后,执行 emitAssets 将生成的 source 保存在 assets 中。然后回到 compiler ,调用

this.outputFileSystem.mkdirp(outputPath, emitFiles);

emitFiles 方法中将文件拼接起来,得到文件的路径、文件名,并将文件写到输出目录里。

参考文献

Webpack 核心模块 tapable 解析

Webpack 核心库 Tapable 的使用与原理解析

webpack与rollup背后的acorn

webpack 4 源码主流程分析

webpack系列之一总览

Webpack源码分析