虽然一直在用webpack,但很少去看它编译出来的js代码,大概是因为调试的时候有sourcemap,可以直接调试源码。一时心血来潮想研究一下,看了一些关于webpack编译方面的文章都有提到,再结合自己看源码的体会,记录一下自己的理解
说bootstap可能还有点不好理解,看一下webpack编译出来的js文件就很好理解了:
// 编译前的入口文件index.js的内容
let a = 1;
console.log(a);
// webpack编译后的文件内容
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {
let a = 1;
console.log(a);
/***/ })
],[0]);
复制代码
编译后的文件跟我们的源文件不太一样了,原本的内容被放到了一个function(module, exports){}
函数里,而最外层多了一个webpackJsonp
的执行代码。那么问题来了:
- webpackJsonp是在哪里定义的,它是干什么用的?
- 包裹原来代码的
function(module, exports){}
又是干什么用的?
这就是bootstrap的作用了。如果不用code split把bootstrap单独分离出来,它就在编译出的js文件最上面,因为需要先执行bootstrap后续的代码才能执行。我们可以用CommonChunkPlugin
把它单独提出来,方便我们阅读。把下面的代码写到你的webpack的plugin配置里即可:
new webpack.optimize.CommonsChunkPlugin({
name: "manifest" // 可以叫manifest,也可以用runtime
}),
复制代码
配置之后,编译出来的文件会多出一个manifest.js
文件,这就是webpack bootstrap的代码了。bootstrap和用户代码(就是我们自己写的部分)编译后的文件其实是一个整体,所以后面的分析会引入用户代码一起看
manifest.js
manifest源码分为3个部分:
- 创建了一个闭包,初始化需要用到的变量
- 定义webpackJsonp方法,挂载到window变量下
- 定义与编译相关的辅助函数和变量,如
__webpack_require__
(也就是我们在自己的代码里用到的require
语法)
我们一个一个来看。下面的每个部分,我们都只截取manifest源码的相关部分来看,完整的源码放在文章最后了
初始化部分
/******/ (function(modules) { // webpackBootstrap
// ......
// The module cache
/******/ var installedModules = {};
/******/
/******/ // objects to store loaded and loading chunks
/******/ var installedChunks = {
/******/ 1: 0
/******/ };
// ......
/******/ })
/************************************************************************/
/******/ ([]);
复制代码
我们截取了manifest最外层的代码和初始化部分的代码,可以看到整个文件都被一个闭包括在里面,而modules
的初始值是一个空的Array
([]
)。 这样做可以隔离作用域,保护内部的变量不被污染
modules
空的Array
([]
),用来存放每个module的内容installedModules
存放module的cache,一个module被执行后(module的执行会在webpackJsonp的源码部分提到)的结果被保存到这里,之后再用到这个模块就可以直接使用缓存而无需再次执行了installedChunks
用来存放chunk的执行情况。若一个chunk已经加载了,在installedChunks里这个chunk的值会变成0,也就是无需再加载了
如果分不清module和chunk这两个概念的区别,文章最后一节专门对此作了解释
webpackJsonp
源码分析
在讲webpackJsonp的源码之前,先回忆一下我们自己的chunk代码
// 编译前的入口文件index.js的内容
let a = 1;
console.log(a);
// webpack编译后的文件内容
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {
let a = 1;
console.log(a);
/***/ })
],[0]);
复制代码
执行webpackJsonp,传了3个参数:
-
chunkIds
chunk的id,这里用了array,但一般一个文件就是一个chunk -
moreModules
chunk里所有模块的内容。模块内容可能不是很直观,再看上面编译后的代码,我们的代码被包在function(module, exports) {}
里,其实是变成了一个函数,这就是一个模块内容。这其实是CommonJs规范中一个模块的定义,只是我们在写模块的时候不用自己写这个头尾,工具会帮我们生成。还记得AMD规范吗?moreModules还隐藏了对每个module的id的定义。从编译后的文件里可以看到
/* 0 */
这样的注释,结合代码来看,其实module的id就是它在moreModules里的数组下标。那么问题来了,只有一个entry chunk还好说,如果有多个chunk,每个chunk里的moreModules的Id不会冲突吗?这里有个小技巧,如下是一个异步chunk的部分代码:webpackJsonp([0],[ /* 0 */, /* 1 */, /* 2 */, /* 3 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { // ...... 复制代码
看到了吗,moreModules的前3个元素是空的,也就是说
0-2
这三个id已经被别的chunk使用了 -
executeModules
需要执行的module,也是一个array。并不是每一个chunk都有executeModules,事实上只有entry chunk才有,因为entry.js是需要执行的
ok,有了使用webpackJsonp部分的印象,再来看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++) { // part 1
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
// 取出每个module的内容
/******/ for(moduleId in moreModules) { // part 2
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
//
/******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/ while(resolves.length) { // part 3
/******/ resolves.shift()();
/******/ }
// 执行executeModules
/******/ if(executeModules) { // part 4
/******/ for(i=0; i < executeModules.length; i++) {
/******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ }
/******/ }
/******/ return result;
/******/ };
复制代码
首先,webpackJsonp是挂在window
全局变量上的,看看每个chunk的开头就知道为什么。我把它分为4块:
-
part 1
这部分涉及到installedChunks
,我们之前了解过,如果没有异步加载的chunk,这部分是用不到的,我们留到异步chunk再说 -
part 2
取出这个chunk里所有module的内容,放到modules
里,这里并不执行每个module,而是真正用到这个module时再从modules里取出来执行 -
part 3
与part 1一样是对installedChunks
的操作,放到后面再说 -
part 4
执行executeModules,一般只有入口文件对应的module是需要执行的。执行module调用了__webpack_require__
方法。还记得我们在代码里怎么引入别的js吗? 对,
require
方法。其实我们的代码编译后会被转成__webpack_require__
,只不过要把引用的路径换成moduleId,这一步也是webpack处理的。所以__webpack_require__
的作用就是执行一个module,把它的exports
返回。先来看看它的实现:// The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { // line 1 /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { // line 2 /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // line 3 /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; // line 4 /******/ } 复制代码
line 1
检查这个module是不是已经执行过,是的话一定在缓存installedModules
里,直接把缓存里的exports
返回。如果没有执行过,那就新建一个module,也就是line 2
。这里module有2个额外的属性,i
记录moduleId,l
记录module是否已经执行。line 3
执行这个module。我们前面说过,我们的代码都被包在一个函数里了,这个函数提供3个参数:module
,exports
,require
。仔细看这行,是不是这三个参数都被传进去了。line 4
返回exports
。值得一提的是,line 3
的执行结果是传给了line 2
我们新建的module
变量,也就是把exports
赋值给module
了,所以我们直接返回了module.exports
使用场景
webpackJsonp的使用场景跟chunk相关,有异步chunk的情况会复杂一些
没有异步加载chunk的情况
没有异步加载chunk的情况是很简单的,它的执行过程可以简单归纳为:依次执行每个chunk文件,也就是执行webpackJsonp
,从moreModules
里取出每个module的内容,放到modules
里,然后执行入口文件对应的module。因为每次执行module,都会缓存这个module的执行结果,所以即使你没有抽取出每个chunk里的相同module(CommonChunkPlugin),也不会重复执行重复的module
有异步加载chunk的情况
当我们使用require.ensure
或者import()
语法时就会产生一个异步chunk,官方文档传送门。异步chunk的js文件不需要手动写到html里,在执行到它时会通过动态加载script
的方式引入,异步加载的函数就是__webpack_require__.e
。
// 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) { // part 1
/******/ return new Promise(function(resolve) { resolve(); });
/******/ }
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) { // part 2
/******/ return installedChunkData[2];
/******/ }
/******/
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) { // part 3
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject]
/******/ });
/******/ installedChunkData[2] = promise;
/******/
/******/ // start chunk loading
/******/ var head = document.getElementsByTagName('head')[0]; // part 4
/******/ 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":"modC","1":"modA"}[chunkId]||chunkId) + ".js"; // line 1
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() { // line 2
/******/ // 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;
/******/ };
复制代码
代码有点多~但其实大部分(part 4)都是异步加载script。我们从头开始看
-
part 1
判断chunk是否已经加载过了,是的话直接返回一个空的Promise。为什么在installedChunks
里的记录为0就表示已经加载过了?这要回到我们之前在讲webpackJsonp
跳过的部分,单独截下来看:for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); // line 1 } installedChunks[chunkId] = 0; // line 2 } 复制代码
加载当前chunk时在
installedChunks
里记录这个chunk已经加载了,也就是置0了(line 1) -
part 2
和part 3
是一体的,它的作用是在chunk还没加载好时就被使用了,这时先返回一个promise,等chunk加载好了,这个promise会resolve
,通知调用者可以使用这个chunk了。因为chunk的js文件需要通过网络,不能保证什么时候加载好,才会用到promise。我们先看看是怎么实现的:其实应该倒过来先看
part 3
再看part 2
。part 3
定义了一个promise,然后把这个promise的resolve
放到installedChunks
里了。这一步很关键,因为chunk加载时需要执行这个resolve告诉这个chunk的使用者已经可以使用了。part 3
执行完成后,installedChunks
里这个chunk对应的记录应该是一个Array
且有3个元素:这个promise的resolve,reject和promise本身。另外需要注意一点,new Promise(function(){})
语句的function
是立即执行的。再来看
part 2
,如果installedChunks
里有这条记录,且它又没有加载完成,那么就把part 3
定义的promise返回给调用者。这样的作用是,当chunk加载完成了,只需要执行这个promise的resolve就能通知调用者继续往下执行顺带提一下这个promise的resolve是何时执行的。看
part 1
webpackJsonp的代码line 1
这行,installedChunks[chunkId][0]
是不是很眼熟,对,这就是chunk在为加载完成时创建的promise的resolve方法,而后会把所有的使用到这个chunk的resolve方法都执行(如下),因为执行到webpackJsonp
就说明这个chunk已经加载完成了while(resolves.length) { resolves.shift()(); } 复制代码
-
part 4
是动态加载script
的代码,没什么可说的,值得一提的是line 1
在拼接script的src时出现的{"0":"modC","1":"modA"}
,这个是我自己的两个异步chunk的id,是webpack分析依赖后插入进来的,如果你有多个异步chunk,这里会随之变化。line 2
是异步chunk加载超时和报错时的处理
ok,有了__webpack_require__.e
的理解,我们再来看加载异步chunk的情况就很轻松了。先来看一段示例:
// 编译前
import(/* webpackChunkName: "modA" */ './mods/a').then(a => {
let ret = a();
console.log('ret', ret);
})
// 编译后
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 0)).then(a => {
let ret = a();
console.log('ret', ret);
})
复制代码
我们用import()
的方式做code spliting,换成require.ensure
也类似,区别在import()
的返回值是promise形式的,require.ensure
是callback形式。对比编译前后,import被替换成了__webpack_require__.e
,在源码的.then
中间加了一行.then(__webpack_require__.bind(null, 0))
。
首先,__webpack_require__.e
保证chunk异步加载完成,但是并不返回chunk的执行结果(见上文__webpack_require__.e的源码分析),所以加了一个.then
来require这个chunk里的module。再然后,就是我们取这个module的代码了
注:/* webpackChunkName: "modA" */
这个是给chunk起名字的,webpack会读这段注释,取modA
作为这个chunk的name,在output.chunkFilename
可以用[name].js
来命名这个chunk,不然webpack会用数字id作为chunk的文件名
其他辅助函数
webpack_require.p
等于output.publicPath
的值(publicPath传送门)。webpack在编译时会把源码中的本地路径替换成publicPath的值,但是异步chunk是动态加载的,它的src
需要加上publicPath。看个小栗子就明白了:
// webpack.config.js
module.exports = {
entry: path.resolve("test", "src", "index.js"),
output: {
path: path.resolve("test", "dist"),
filename: "[name].js",
publicPath: 'http://game.qq.com/images/test', // 这里定义了publicPath
chunkFilename: "[name].js"
},
// ......
}
复制代码
这是配置文件,我们定义了publicPath
// manifest.js
// ...
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "http://game.qq.com/images/test"; // 赋值publicPath的值
//...
//
复制代码
webpack把publicPath带进manifest.js
// 还是manifest.js
// ...
/******/ script.src = __webpack_require__.p + "" + ({"0":"modA","1":"modC"}[chunkId]||chunkId) + ".js";
// ...
复制代码
还记得这行代码吗,这是动态加载异步chunk时拼src
的部分。这里就把__webpack_require__.p
拼在异步chunk的url上了
webpack_require.e
上面已经详细分析了~
webpack_require.d 和 webpack_require.n
webpack从2.0开始原生支持es6 modules,也就是import,export语法,不需要借助babel编译。这会出现一个问题,es6 modules语法的import引入了default
的概念,在Commonjs模块里是没有的,那么如果在一个Commonjs模块里引用es6 modules就会出问题,反之亦然。webpack对这种情况做了兼容处理,就是用__webpack_require__.d
和__webpack_require__.n
来实现的,限于篇幅,就不在这里细讲了,大家可以阅读webpack模块化原理-ES module这篇文章,写的比较详细
webpack_require.nc
script属性nonce
的值,如果你有使用的话,会在每个异步加载的script加上这个属性
A cryptographic nonce (number used once) to whitelist inline scripts in a script-src Content-Security-Policy . The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
一些alias
webpack在__webpack_require__
上加了一些manifest.js里的变量引用,应该是给webpack内部js或者plugin加进来的js使用的:
- webpack_require.m modules的引用
- webpack_require.c installedModules的引用
如果你尝试在你的代码里使用这些变量或者require本身(不是用require来引入模块),webpack会把它编译成一个报错函数
一些工具函数的简写
- webpack_require.o
Object.prototype.hasOwnProperty.call
的简写 - webpack_require.oe 异步加载chunk报错的函数
chunk与module的区别
可能很多同学搞不清楚chunk和module的区别,在这里特别说明一下
module的概念很简单,未编译的代码里每个js文件都是一个module,比如:
// entry.js
import a from './a.js';
console.log(a); // 1
// a.js
module.exports = 1;
复制代码
这里entry.js和a.js都是module
那什么是chunk呢。先说简单的,如果你的代码既没有code split,也没有需要异步加载的module,这时编译出的js文件只有两个:
- manifest.js,也就是bootstrap代码
- 你的源代码编译后的js文件
它们都是chunk。有图为证:
main
chunk就是你的源码编译生成的,因为它是以入口文件为起点生成的,所以也叫entry chunk
还记得在初始化部分installedChunks
的初始化值么
/******/ // objects to store loaded and loading chunks
/******/ var installedChunks = {
/******/ 1: 0
/******/ };
复制代码
这里已经把id为1
的chunk的值置成0了,说明这个chunk已经加载好了。what?这不是才开始初始化吗! 再看看上面的那张图,manifest这个chunk的id为1,manifest当然执行了~
再说复杂的,也就是有code split的情况,这时就不止有entry chunk了,还有因为code split产生的chunk。 code split的情形有两种:
- 通过CommonChunkPlugin分离出的chunk
- 异步模块产生的chunk
第2点的异步模块,指的是通过require.ensure
或者import()
引入的模块,这些模块因为是异步加载的,会被单独打包到一个文件,在 触发加载条件时才会加载这个chunk.js
ok,我们总结一下产生chunk的3种情形
- entry chunk 也就是入口文件产生的chunk,这个必有
- initial chunk 也就是manifest生成的chunk,这个也是必有
- normal chunk 也就是code split产生的chunk,这个得看你是否有用到code split,且他们是异步加载的
完整的manifest.js
/******/ (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()();
/******/ }
/******/ if(executeModules) {
/******/ for(i=0; i < executeModules.length; i++) {
/******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // objects to store loaded and loading chunks
/******/ var installedChunks = {
/******/ 1: 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;
/******/ }
/******/
/******/
/******/ // 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; };
/******/ })
/************************************************************************/
/******/ ([]);
复制代码