likes
comments
collection
share

从超简单demo入手了解webpack打包原理

作者站长头像
站长
· 阅读数 39

本文从一个简单demo来探究文件到输出bundle 的过程;来了解webpack4.46.0打包主流程;

首先看我们的demo:

// 入口 index.js
import { add } from "./add.js";
function test() {
  const tmp = "something";
  return tmp + add;
}
const r = test();
// add.js
export const add = (a, b) => a + b; 

打包配置:

const path = require("path");
module.exports = {
  entry: {
    main: "./src/index.js",
  },
  output: {
    path: path.join(__dirname, "./dist"),
    filename: "index.js",
  },
  mode: "none",
};

再来看我们的打包输出(简化了下,只保留了大概结构):

(function (modules) {
  // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
    xxxxx...
  }

  __webpack_require__.xxxx = ...
  __webpack_require__.xxxx = ...
  ....
  // __webpack_public_path__
  __webpack_require__.p = "";

  // Load entry module and return exports
  return __webpack_require__((__webpack_require__.s = 0));
})(
  [
    function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      var _add_js__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(1);

      function test() {
        const tmp = "something";
        return tmp + _add_js__WEBPACK_IMPORTED_MODULE_0__["add"];
      }
      const r = test();
    },
   function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      __webpack_require__.d(
        __webpack_exports__,
        "add",
        function () {
          return add;
        }
      );
      const add = (a, b) => a + b;
    },
  ]
);

打包输出的文件包含了webpack的运行时代码,执行的入口(webpack_require((webpack_require.s = 0)),打包后的module;

我们主要看一下这之间经历了什么,以及module中的代码是如何替换的,例如

import {add} from './add.js' => var add_js_WEBPACK_IMPORTED_MODULE_0_ = _webpack_require_(1);

add => add_js_WEBPACK_IMPORTED_MODULE_0_['add'];

webpack 启动阶段:

主要是将配置的options与默认的options合并;然后根据option中的字段注册相应的plugin;

启动代码在这里: node_modules/webpack/bin/webpack.js; node_modules/webpack-cli/bin/cli.js;

根据option注册插件在这里: node_modules/webpack/lib/WebpackOptionsApply.js;

这里注册了很多插件,在complier的不同阶段会触发它们订阅的回调;

最后通过webpack-cli中调用complier.run开始构建;

compiler.run((err, stats) => {
    if (compiler.close) {
        compiler.close(err2 => {
                compilerCallback(err || err2, stats);
        });
    } else {
        compilerCallback(err, stats);
    }
});

先后触发complier的beforeRun,run,beforeCompile,compile,compilation,thisCompilation 生成comliation;进入make阶段:

make阶段:

complier的make事件被SingleEntryPlugin注册,它会将入口文件(我们配置中写的‘./src/index.js’),加入到构建中,开始模块解析和构建过程;

const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);

addEntry 中我们会触发complilation的addEntry事件; addEntry 里继续调用complation的_addModuleChain方法,在这里我们获取moduleFactory ;开始module create; 因为dep是SingleEntryDependancy的实例,根据SingleEntry 在 compliation事件中 的注册;

compilation.dependencyFactories.set(SingleEntryDependency,normalModuleFactory);

所以我们获取的moduleFactory为normalModuleFactory, 在 normalModuleFactory.create中会依次触发beforeResolve,factory,resolver,afterResolve, 这个过程解析出文件的路径和loader的路径;

继续触发createModule和module事件,创建module:

createdModule = new NormalModule(result);
// result
context: context, // prcess.pwd();
request: loaders
        .map(loaderToIdent)
        .concat([resource])
        .join("!"),
dependencies: data.dependencies, // SingleEntryDepedancy
userRequest, // /Users/userName/Documents/exercise/webpack-source-code/src/index.js
rawRequest: request, // ./src/index.js
loaders,
resource, // /Users/userName/Documents/exercise/webpack-source-code/src/index.js
matchResource,
resourceResolveData,
settings,
type, // 'javascript/auto'
parser: this.getParser(type, settings.parser), //node_modules/webpack/lib/JavascriptModulesPlugin.js 里注册了parser, generator
generator: this.getGenerator(type, settings.generator),
resolveOptions

回到compilation,将module加入到compilation 的 modules 里;然后module与singleEntryModule 建立起关系:

dependency.module = module;
module.addReason(null, dependency);

到这里只是解析了入口模块的路径并将它加到了compilation的modules中;下面进入module.build 过程,该过程触发buildModule, failedModule/succeedModule 事件

下面这段逻辑执行后,module的_source等字段才填充上;

buildModule(){
    ...
    module.build();
}
// NormalModule.js
build () {
    ...
    this._source = null;
    this.buildMeta = {};
    this.buildInfo = {};
    ...
    return this.doBuild(...someParams..., () => {
        ...
        this.parser.parse(this._ast || this._source.source(), {curent: this, module: this, ...}, () => {...})
        ...
    });
}
doBuild () {
    const loaderContext = this.createLoaderContext(...);
    runLoaders({resource: this.resource, loaders: this.loaders, context: loaderContext,readSource: fs.readFile.bind(fs)}, (err, result) => {
        if (result) {
            this.buildInfo.xx = result.xx;
            ...
        }
        ...
        const source = result.result[0];
        ...
        this._source = this.createSource(this.binary ? asBuffer(source) : asString(source),...);
        ...
        this._ast = ...someCode..
    })
}

在this.parser.parse 里 会对资源进行解析,为 module添加依赖; 具体逻辑在node_modules/webpack/lib/Parser.js 中;

最终对于index.js, 会给它添加五个依赖(这些依赖的originModule是a.js, module会在解析完添加);

  • HarmonyCompatibilityDependency HarmonyDetectionParserPlugin 添加,因为顶层语句中有ImportDeclaration;
  • HarmonyInitDependency 理由同上
const initDep = new HarmonyInitDependency(module);
initDep.loc = {
    start: {
        line: -1,
        column: 0
    },
    end: {
        line: -1,
        column: 0
    },
    index: -2
};
module.addDependency(initDep);
  • ConstDependency
  • HarmonyImportSideEffectDependency 上面两个添加是遇到import 语句;
const sideEffectDep = new HarmonyImportSideEffectDependency(
    source,
    parser.state.module,
    parser.state.lastHarmonyImportOrder,
    parser.state.harmonyParserScope
);
sideEffectDep.loc = statement.loc; //{start: {line: 1, column: 0}, end: {line: 1, column: 31}
parser.state.module.addDependency(sideEffectDep);
  • HarmonyImportSpecifierDependency
const dep = new HarmonyImportSpecifierDependency(
    settings.source,
    parser.state.module,
    settings.sourceOrder,
    parser.state.harmonyParserScope,
    settings.id,
    name,
    expr.range,
    this.strictExportPresence
);
dep.shorthand = parser.scope.inShorthand;
dep.directImport = true;
dep.loc = expr.loc; // {start: {line: 5, column: 15}, end: {line: 5, column: 18}}
parser.state.module.addDependency(dep);

这里的逻辑是这样的:

首先在遇到importSpecifier节点的时候,会把当前作用域(当前为顶层作用域)的变量引用删除掉,然后设置它的别名为‘import var';

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

在遍历其它表达式时,遇到rename为import var 的时候,添加HarmonyImportSpecifierDependency依赖;

这里顺便说下parser的prewalkStatements;walkStatements;

一个是扫描当前作用域的变量,做变量收集和一些预处理,一个做最终处理;

这样就得到了index.js模块的依赖,webpack会根据它们在源码中应用的位置进行下排序,后调用callback进入到afterBuild 函数, 有依赖,执行compilation.processModuleDependencies中:

这里面主要干了两件事:

  • 先对依赖根据factory分组,然后组内根据资源id进行分组;此处会过滤掉没有资源id的依赖;

  • 对上一步继续处理,解构成<Factory, <Dependancy[]>>的结构;

    提一下module的几个属性: dependencies: 同步方式引入的模块; blocks: import() 方式引入的模块: variables: webpack 内置的变量 加入的依赖;

进入到addModuleDependencies里,对上一步的数据结构进行遍历处理,开始新一轮的resolve,createModule, 本例中会创建 add.js 的module;然后加入到compilation中;

把add.js module跟 引入 它的module(index.js) 以及 填充index 中 该资源的依赖的module字段;

const iterationDependencies = depend => {
    for (let index = 0; index < depend.length; index++) {
        const dep = depend[index];
        dep.module = dependentModule; // 填充依赖的module字段
        dependentModule.addReason(module, dep); // 跟引入它的module建立关系
    }
};
dependentModule = addModuleResult.module;
iterationDependencies(dependencies);

然后继续开始b.js的build,获取它的依赖,本次的依赖经过processModuleDependencies的处理为空数组,至此结束make阶段;

seal 阶段

具体代码在compilation.seal 中, 这里面触发了一堆事件,具体可以查阅代码,总之,这里面会构建chunk,将module分配到chunk中,为module设置id,为chunk设置id, 为module,chunk设置hash,生成文件内容;

我们主要看下文件的生成过程:

这是输出代码的各部分构成,方便后续理解:

从超简单demo入手了解webpack打包原理

先看createChunkAssets: 首先调用mainTemplate.getRenderList,触发renderManifest,触发JavascriptModulesPlugin的监听,返回如下:

[{
    render: () =>
        compilation.mainTemplate.render(
            hash,
            chunk,
            moduleTemplates.javascript, // javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"),
            dependencyTemplates
        ),
    filenameTemplate, // 'index.js'
    pathOptions: {
        noChunkHash: !useChunkHash,
        contentHashType: "javascript",
        chunk
    },
    identifier: `chunk${chunk.id}`,// chunk.id = 0;
    hash: useChunkHash ? chunk.hash : fullHash //13e9c3686eb70120279b158de175
}]

上面出现的三个模版: MainTemplate 主要用于生成webpack运行时的框架代码。 RunTimeTemplate 是 Webpack 编译器中用于生成运行时代码的模板。我们会用到importStatement的方法 ModuleTemplate 用于生成每个模块的代码。

获取到mainfest,webpack会遍历调用每一项的render方法,获取source;进入到mainTemplate.render 方法中

  1. 首先会renderBootstrap,生成webpack_require函数部分和它的静态属性;
  2. 将结果作为载荷触发 render钩子,执行mainTemplate的订阅函数,在这里面生成bootStrap 的外层IIFE, 参数部分的生成需要mainTemplate的modules事件订阅函数返回;
  3. 执行JavascriptModulesPlugin的订阅,调用Template.renderChunkModules, 里面循环调用ModuleTemplate的render方法

里面分为了四部分:

1.调用NormalModule.source

里面调用JavascriptGenerator.generate 为资源生成 ReplaceSource,这些replace是由模块的依赖来生成的;

// JavascriptGenerator
const source = new ReplaceSource(originalSource);

首先HarmonyCompatibilityDependency,为source加‘_webpack_require_.r(_webpack_exports_);\n’, 定义__esModule;

HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate {
    apply(dep, source, runtime) {
        const usedExports = dep.originModule.usedExports;
        if (usedExports !== false && !Array.isArray(usedExports)) {
                // 定义 module esmodule = true;
                const content = runtime.defineEsModuleFlagStatement({
                        exportsArgument: dep.originModule.exportsArgument
                });
                source.insert(-10, content);
        }
    }
}

第二个依赖:HarmonyInitDependency,处理import语句,它会遍历该模块的依赖,找到HarmonyImportDepandancy的实例,也就是(HarmonyImportSideEffectDependency,HarmonyImportSpecifierDependency),然后依次执行它们的template.harmonyInit的方法:

harmonyInit(dep, source, runtime, dependencyTemplates) {
    let sourceInfo = importEmittedMap.get(source); // importEmittedMap单例
    if (!sourceInfo) { // HarmonyImportSideEffectDependency 执行时为空,后面HarmonyImportSpecifierDependency 执行时有值;
        importEmittedMap.set(
            source,
            (sourceInfo = {
                    emittedImports: new Map()
            })
        );
    }
    const key = dep._module || dep.request;
    if (key && sourceInfo.emittedImports.get(key)) return;
    sourceInfo.emittedImports.set(key, true);
    const content = dep.getImportStatement(false, runtime); // 调用HarmonyImportDepandancy的getImportStatement
    source.insert(-1, content);
}
//HarmonyImportDepandancy.getImportStatement
getImportStatement(update, runtime) {
    return runtime.importStatement({
        update,
        module: this._module,
        importVar: this.getImportVar(),
        request: this.request,
        originModule: this.originModule
    });
}
//HarmonyImportDepandancy.getImportVar
getImportVar() {
    let importVarMap = this.parserScope.importVarMap; // 这个地方两个依赖共用,SlideEffer/Specifier
    if (!importVarMap) this.parserScope.importVarMap = importVarMap = new Map();
    let importVar = importVarMap.get(this._module);
    if (importVar) return importVar;
    importVar = `${Template.toIdentifier(
            `${this.userRequest}`
    )}__WEBPACK_IMPORTED_MODULE_${importVarMap.size}__`; // _add_js__WEBPACK_MODULE_0_
    importVarMap.set(this._module, importVar);
    return importVar;
}

// RunTimeTemplate.importStatement 处理导入语句
importStatement({ update, module, request, importVar, originModule }) {
    if (!module) {
        return this.missingModuleStatement({
                request
        });
    }
    // 获取moduleId
    const moduleId = this.moduleId({
        module,
        request
    });
    const optDeclaration = update ? "" : "var ";

    const exportsType = module.buildMeta && module.buildMeta.exportsType;
    let content = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`;

    if (!exportsType && !originModule.buildMeta.strictHarmonyModule) {
            content += `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/__webpack_require__.n(${importVar});\n`;
    }
    if (exportsType === "named") {
            if (Array.isArray(module.buildMeta.providedExports)) {
                    content += `${optDeclaration}${importVar}_namespace = /*#__PURE__*/__webpack_require__.t(${moduleId}, 1);\n`;
            } else {
                    content += `${optDeclaration}${importVar}_namespace = /*#__PURE__*/__webpack_require__.t(${moduleId});\n`;
            }
    }
    return content;
}

经过一系列操作,生成'/* harmony import */ var add_js__WEBPACK_IMPORTED_MODULE_0_ = _webpack_require_(1);'处理了import语句;

第三个依赖:ConstDependency,加入了一个空的Replacement:

ConstDependency.Template = class ConstDependencyTemplate {
    apply(dep, source) {
        if (typeof dep.range === "number") {
            source.insert(dep.range, dep.expression);
            return;
        }
        source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
    }
};

第四个依赖:HarmonyImportSideEffectDependency, 啥也没干,空函数

apply(dep, source, runtime) {
        // no-op
}

第五个依赖:HarmonyImportSpecifierDependency 替换表达式中的引用

apply(dep, source, runtime) {
    super.apply(dep, source, runtime); // super 啥也没干
    const content = this.getContent(dep, runtime);
    source.replace(dep.range[0], dep.range[1] - 1, content);
}
getContent(dep, runtime) {
    const exportExpr = runtime.exportFromImport({
        module: dep._module,
        request: dep.request,
        exportName: dep._id,
        originModule: dep.originModule,
        asiSafe: dep.shorthand,
        isCall: dep.call,
        callContext: !dep.directImport,
        importVar: dep.getImportVar(), // 第二个依赖搞完设置了;
    });
    return dep.shorthand ? `${dep.name}: ${exportExpr}` : exportExpr;
}
// RuntimeTemplate
exportFromImport({module,request,exportName,originModule,asiSafe,isCall,callContext,importVar}) {
    if (!module) {
       ...
    }
    const exportsType = module.buildMeta && module.buildMeta.exportsType;// ast 解析时相关节点触发的插件填充的,这里是HarmonyDetectionParserPlugin填充的

    if (!exportsType) {
       ...
    }

    if (exportsType === "named") {
        if (exportName === "default") {
            return importVar;
        } else if (!exportName) {
            return `${importVar}_namespace`;
        }
    }

    if (exportName) {
        const used = module.isUsed(exportName);
        if (!used) {
            const comment = Template.toNormalComment(`unused export ${exportName}`);
            return `${comment} undefined`;
        }
        const comment = used !== exportName ? Template.toNormalComment(exportName) + " " : "";
        const access = `${importVar}[${comment}${JSON.stringify(used)}]`;
        if (isCall) { //  是否时函数调用
            if (callContext === false && asiSafe) {
                    return `(0,${access})`;
            } else if (callContext === false) {
                    return `Object(${access})`;
            }
        }
        return access; //  _add_js__WEBPACK_IMPORTED_MODULE_0__["add"]
    } else {
        return importVar;
    }
}
2.moduleTemplate.hooks.content

该事件在本例中没有相关订阅事件

3.moduleTemplate.hooks.module

该事件在本例中没有相关订阅事件,订阅该事件的plugin有EvalDevToolModuleTemplatePlugin.js,EvalSourceMapDevToolModuleTemplatePlugin.js

4.moduleTemplate.hooks.render

将模块代码用外层函数包裹

订阅函数:

// FunctionModuleTemplatePlugin
(moduleSource, module) => {
    const source = new ConcatSource();
    const args = [module.moduleArgument]; // ['module']
    // TODO remove HACK checking type for javascript
    if (module.type && module.type.startsWith("javascript")) { // type: 'javascript/auto'
    args.push(module.exportsArgument); // __webpack_exports__
    if (module.hasDependencies(d => d.requireWebpackRequire !== false)) { //true
       args.push("__webpack_require__");
    }
    } else if (module.type && module.type.startsWith("json")) {
        // no additional arguments needed
    } else {
        args.push(module.exportsArgument, "__webpack_require__");
    }
    // 将module,__webpack_exports__,__webpack_require__ 作为模块的参数
    source.add("/***/ (function(" + args.join(", ") + ") {\n\n"); 
    if (module.buildInfo.strict) source.add('"use strict";\n');
    source.add(moduleSource);
    source.add("\n\n/***/ })");
    return source;
}
5.moduleTemplate.hooks.package

是否对打包的模块标注;我们没设置,所以不走;

至此,我们就得到了模块的source, 现在模块的replace操作还没有执行,只是保存在了source中,方便后续有流程改的话可以拿到信息;

最终替换的代码在最终输出文件的地方,也就是Complier的emitAssets中,source()调用会执行replace;

let content;
if (typeof source.buffer === "function") {
        content = source.buffer();
} else {
    const bufferOrString = source.source(); // 生成最终内容
    if (Buffer.isBuffer(bufferOrString)) {
        content = bufferOrString;
    } else {
        content = Buffer.from(bufferOrString, "utf8");
    }
}

// Create a replacement resource which only allows to ask for size
// This allows to GC all memory allocated by the Source
// (expect when the Source is stored in any other cache)
cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
        size: content.length
});

// Write the file to output file system
							this.outputFileSystem.writeFile(targetPath, content, errorCallback);

我们看一下index.js 最后的replacements(在source()调用中经过了排序, end大的在前,其次start大的靠前,最后看insertIndex):

  {
    start: 93,
    end: 95,
    content: '_add_js__WEBPACK_IMPORTED_MODULE_0__["add"]',
    insertIndex: 3,
  },
  { start: 0, end: 30, content: "", insertIndex: 2 },
  {
    start: -1,
    end: -2,
    content:
      "/* harmony import */ var _add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);\n",
    insertIndex: 1,
  },
  {
    start: -10,
    end: -11,
    content: "__webpack_require__.r(__webpack_exports__);\n",
    insertIndex: 0,
  },

每个replace执行如下操作,就是将替换部分切掉,然后填充content;

result=[str]; // 原来的文件内容
function(repl) {
    var remSource = result.pop();
    var splitted1 = this._splitString(remSource, Math.floor(repl.end + 1));
    var splitted2 = this._splitString(splitted1[0], Math.floor(repl.start));
    result.push(splitted1[1], repl.content, splitted2[0]);
}
_splitString(str, position) {
    return position <= 0 ? ["", str] : [str.substr(0, position), str.substr(position)];
}
// 例如第一个replacement 切成
[
'd;\n}\nconst r = test();\n'
'_add_js__WEBPACK_IMPORTED_MODULE_0__["add"]'
'import { add } from "./add.js";\n\nfunction test() {\n  const tmp = "something";\n  return tmp + '
]

// 最后拼接起来得到最终结果;
let resultStr = "";
for(let i = result.length - 1; i >= 0; --i) {
    resultStr += result[i];
}

这里有个细节,数组里第一项是内容的最后一个字符串,也就是倒过来了,可以想下这么做的原因🐶;(这也是为什么要对replacement排序的原因)

同理,add.js的代码跟index.js基本相同;至此我们就了解了webpack打包的整个流程,这里我省略了很多分支,只探讨了主干部分,希望对想探究webpack原理的朋友有所帮助。

转载自:https://juejin.cn/post/7202514143340232741
评论
请登录