webpack的模块化不仅支持commonjs和es module,还能通过code splitting实现模块的动态加载。根据wepack官方文档,实现动态加载的方式有两种:import
和require.ensure
。
那么,这篇文档就来分析一下,webpack是如何实现code splitting的。
PS:如果你对webpack如何实现commonjs和es module感兴趣,可以查看我的前两篇文章:webpack模块化原理-commonjs和webpack模块化原理-ES module。
准备
首先我们依然创建一个简单入口模块index.js
和两个依赖模块foo.js
和bar.js
:
// index.js
'use strict';
import(/* webpackChunkName: "foo" */ './foo').then(foo => {
console.log(foo());
})
import(/* webpackChunkName: "bar" */ './bar').then(bar => {
console.log(bar());
})
// foo.js
'use strict';
exports.foo = function () {
return 2;
}
// bar.js
'use strict';
exports.bar = function () {
return 1;
}
webpack配置如下:
var path = require("path");
module.exports = {
entry: path.join(__dirname, 'index.js'),
output: {
path: path.join(__dirname, 'outs'),
filename: 'index.js',
chunkFilename: '[name].bundle.js'
},
};
这是一个最简单的配置,指定了模块入口和打包文件输出路径,值得注意的是,这次还指定了分离模块的文件名[name].bundle.js
(不指定会有默认文件名)。
在根目录下执行webpack
,得到经过webpack打包的代码如下(去掉了不必要的注释):
(function(modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [], result;
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
while(resolves.length) {
resolves.shift()();
}
};
// The module cache
var installedModules = {};
// objects to store loaded and loading chunks
var installedChunks = {
2: 0
};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
if(installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
// a Promise means "currently loading".
if(installedChunkData) {
return installedChunkData[2];
}
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// start chunk loading
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = __webpack_require__.p + "" + ({"0":"foo","1":"bar"}[chunkId]||chunkId) + ".bundle.js";
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
function onScriptComplete() {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = 0);
})
([
(function(module, exports, __webpack_require__) {
"use strict";
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(foo => {
console.log(foo());
})
__webpack_require__.e/* import() */(1).then(__webpack_require__.bind(null, 2)).then(bar => {
console.log(bar());
})
})
]);
分析
编译后的代码,整体跟前两篇文章中使用commonjs和es6 module编写的代码编译后的结构差别不大,都是通过IFFE的方式启动代码,然后使用webpack实现的require
和exports
实现的模块化。
而对于code splitting的支持,区别在于这里使用__webpack_require__.e
实现动态加载模块和实现基于promise的模块导入。
所以首先分析__webpack_require__.e
函数的定义,这个函数实现了动态加载:
__webpack_require__.e = function requireEnsure(chunkId) {
// 1、缓存查找
var installedChunkData = installedChunks[chunkId];
if(installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
if(installedChunkData) {
return installedChunkData[2];
}
// 2、缓存模块
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 3、加载模块
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = __webpack_require__.p + "" + ({"0":"foo"}[chunkId]||chunkId) + ".bundle.js";
// 4、异常处理
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
function onScriptComplete() {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
// 5、返回promise
return promise;
};
代码大致逻辑如下:
- 缓存查找:从缓存
installedChunks
中查找是否有缓存模块,如果缓存标识为0,则表示模块已加载过,直接返回promise
;如果缓存为数组,表示缓存正在加载中,则返回缓存的promise
对象 - 如果没有缓存,则创建一个
promise
,并将promise
和resolve
、reject
缓存在installedChunks
中 - 构建一个script标签,append到head标签中,src指向加载的模块脚本资源,实现动态加载js脚本
- 添加script标签onload、onerror 事件,如果超时或者模块加载失败,则会调用reject返回模块加载失败异常
- 如果模块加载成功,则返回当前模块
promise
,对应于import()
以上便是模块加载的过程,当资源加载完成,模块代码开始执行,那么我们来看一下模块代码的结构:
webpackJsonp([0],[
/* 0 */,
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
exports.foo = function () {
return 2;
}
/***/ })
]);
可以看到,模块代码不仅被包在一个函数中(用来模拟模块作用域),外层还被当做参数传入webpackJsonp
中。那么这个webpackJsonp
函数的作用是什么呢?
其实这里的webpackJsonp
类似于jsonp中的callback,作用是作为模块加载和执行完成的回调,从而触发import
的resolve
。
具体细看webpackJsonp
代码来分析:
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0, resolves = [], result;
// 1、收集模块resolve
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 2、copy模块到modules
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
// 3、resolve import
while(resolves.length) {
resolves.shift()();
}
};
代码大致逻辑如下:
- 根据
chunkIds
收集对应模块的resolve
,这里的chunkIds
为数组是因为require.ensure
是可以实现异步加载多个模块的,所以需要兼容 - 把动态模块添加到IFFE的
modules
中,提供其他CMD方案使用模块 - 直接调用
resolve
,完成整个异步加载
总结
webpack通过__webpack_require__.e
函数实现了动态加载,再通过webpackJsonp
函数实现异步加载回调,把模块内容以promise的方式暴露给调用方,从而实现了对code splitting的支持。