Skip to content

import()分包原理

使用方式

例如:

javascript
import('./A.js').then((res) => { console.log(res) })

ast解析

使用acorn解析时会转换为ExpressionStatement,进一步分析为MemberExpression下的ImportExpression

img

hooks.importCall

解析完成后进行walkStatement,此时会触发hooks.importCall钩子。在webpack/lib/dependencies/ImportParserPlugin.js文件中:

javascript
parser.hooks.importCall.tap("ImportParserPlugin", expr => {

  let chunkName = null;

  // ...省略
  const depBlock = new AsyncDependenciesBlock(
    {
      ...groupOptions,
      name: chunkName
    },
    expr.loc,
    param.string
  );
  const dep = new ImportDependency(param.string, expr.range, exports);
  dep.loc = expr.loc;
  depBlock.addDependency(dep);
  parser.state.current.addBlock(depBlock);

  return true;

});

最终会创建一个AsyncDependenciesBlock,并通过addBlock添加,最终添加到module.blocks属性上。异步组件moduleparentBlock指向的是AsyncDependenciesBlock

seal 阶段

seal阶段中解析下一层引用modules时,调用webpack/buildChunkGraph.js文件中的extractBlockModules方法:

javascript
const queue = [module];
while (queue.length > 0) {
  const block = queue.pop();
  const arr = [];
  arrays.push(arr);
  blockModulesMap.set(block, arr);
  for (const b of block.blocks) {
    queue.push(b);
  }
}

此时会遍历blocks形成如下结构数组结构:不同的block对应于不同的module

img

接着在processBlock结尾处,会对module.block进行遍历:

javascript
for (const b of block.blocks) {
  iteratorBlock(b);
}

if (block.blocks.length > 0 && module !== block) {
  blocksWithNestedBlocks.add(block);
}

此时如果没有对应的chunkGroup,则会进行创建:

javascript
cgi = namedChunkGroups.get(chunkName);
if (!cgi) {
  c = compilation.addChunkInGroup(
    b.groupOptions || b.chunkName,
    module,
    b.loc,
    b.request
  );
}

因此最终compilation.chunkscompilation.chunkGroups的值中将不止一个chunk/chunkGroup。最终也会根据chunk生成单独的文件。

emit阶段

这里以这一段代码为例:

javascript
// index.js 文件
import('./moduleB.js').then((res) => {
  console.log(res)
})

// moduleB.js 文件
export function B() {
  console.log('==> module B');
}

此时会生成两个文件:

img

分包后的代码会独自生成一个文件,而在main文件中,打包后的import()语句则会变为:

javascript
// 整理后的代码大致如下:
__webpack_require__.e("src_moduleB_js")
  .then(__webpack_require__.bind(__webpack_require__,"./src/moduleB.js"))
 .then((res) => {console.log(res)}));

接下来看下代码的具体执行过程。

代码执行初始化

installedChunks

先找到/* webpack/runtime/jsonp chunk loading */这个注释的闭包函数,首先会定义一个installedChunks变量

javascript
var installedChunks = {
  "main": 0
};

该变量是一个对象,keychunkkey,值为0代表已经加载完成,否则值为[resolve, reject, Promise]形式。

webpack_require.f.j

__webpack_require__.f.j.f属性上定义了一个j函数,在后面会用到。

webpackJsonpCallback

javascript
// 定义webpackJsonpCallback
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
  // ...
}

// 定义一个全局变量,该变量在其他chunk里同样能访问
var chunkLoadingGlobal = self["webpackChunkstudy_webpack"] = self["webpackChunkstudy_webpack"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 1. 将数组的 原生push 当做参数传入
// 2. 重写chunkLoadingGlobal.push 方法
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));

当初始化完成后,开始加载index.js文件,此时会执行__webpack_require__.e("src_moduleB_js")

引入模块(webpack_require.e)

javascript
(() => {
  __webpack_require__.f = {};
  __webpack_require__.e = (chunkId) => {
    return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
      __webpack_require__.f[key](chunkId, promises);
      return promises;
    }, []));
  };
})();

该函数相当于遍历.f对象,并执行各个函数,前面初始化时已经定义了.j函数。因此会执行__webpack_require__.f.j函数。

封装Promise(webpack_require.f.j/l)

javascript
__webpack_require__.f.j = (chunkId, promises) => {
  var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
  if (installedChunkData !== 0) {
    if (installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      if (true) { 
        var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
        promises.push(installedChunkData[2] = promise);
        __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);

      } else installedChunks[chunkId] = 0;
    }
  }
};

该函数会先判断installedChunks中该chunk对应的值是否为0,如果为0,说明已经加载好了,只需要把原来保存的promise返回即可。

否则的话需要创建一个promise对象,按照[resolve, reject, Promise]的形式添加到promises中,最后调用__webpack_require__.l正式加载模块。

webpack_require.l

__webpack_require__.l精简代码如下:

javascript
__webpack_require__.l = (url, done, key, chunkId) => {
  if (!script) {
    needAttach = true;
    script = document.createElement('script');
    script.charset = 'utf-8';
    script.setAttribute("data-webpack", dataWebpackPrefix + key);
    script.src = url;
  }
  var onScriptComplete = (prev, event) => {
    script.parentNode && script.parentNode.removeChild(script);
  }
  needAttach && document.head.appendChild(script);
};

该方法会创建一个script标签,加入到head中,记载完成后就会移除该script。重要的就是这个加载过程了。

异步模块执行

来到moduleB.js打包后的文件中:

javascript
(self["webpackChunkstudy_webpack"] = self["webpackChunkstudy_webpack"] || []).push([["src_moduleB_js"], {
"./src/moduleB.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"B\": () => (/* binding */ B)\n/* harmony export */ });\nfunction B() {\n  console.log('==> module B');\n}\n\n//# sourceURL=webpack://study-webpack/./src/moduleB.js?");
}]);

执行时会获取在初始化时定义的全局webpackChunkstudy_webpack,并调用push方法,由于push方法被重写了,此时实际上调用的就是之前的webpackJsonpCallback函数:

javascript
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
  var [chunkIds, moreModules, runtime] = data;
  var moduleId, chunkId, i = 0;
  if (chunkIds.some((id) => (installedChunks[id] !== 0))) {
    for (moduleId in moreModules) {
      if (__webpack_require__.o(moreModules, moduleId)) {
        // 1. 将新加载的模块代码添加到全局模块当中
        __webpack_require__.m[moduleId] = moreModules[moduleId];
      }
    }
    if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
    for (;i < chunkIds.length;i++) {
      chunkId = chunkIds[i];
      if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
        // 2. 执行 promise[0],也就是 resolve 函数
        installedChunks[chunkId][0]();
      }
      // 3. 标识该 chunk 已经加载完成
      installedChunks[chunkIds[i]] = 0;
    }
  }
}

该函数主要将加载完的模块代码放到__webpack_modules__对象当中。这样调用resolve后,再次执行__webpack_require__时就能找到对应的模块了。

总结

import()分包原理大致如下:

  1. ast解析时分析为调用的import()方法。
  2. 进行walkStatement时,触发hooks.importCall钩子,建立AsyncDependency存放到blocks属性当中。
  3. 在生成chunks的时候会去遍历blocks,单独生成chunks
  4. 生成的代码执行时:
    1. 初始化一些参数,比如webpackJsonpCallback以及chunkLoadingGlobal等等。
    2. 执行__webpack_require__.e判断模块是否已经加载,如果未加载会创建一个promise
    3. 调用__webpack_require__.l创建script标签加载chunk
    4. 执行chunk代码时调用push方法将引用到的模块传给webpackJsonpCallback。然后将该模块记录到__webpack_modules__中。随后执行__webpack_require__加载该模块。