虽然一直在用webpack,但不多去看它编译出来的js代码,大概是由于调试的时候有sourcemap,能够直接调试源码。一时心血来潮想研究一下,看了一些关于webpack编译方面的文章都有提到,再结合本身看源码的体会,记录一下本身的理解javascript
说bootstap可能还有点很差理解,看一下webpack编译出来的js文件就很好理解了:html
// 编译前的入口文件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
的执行代码。那么问题来了:java
function(module, exports){}
又是干什么用的?这就是bootstrap的做用了。若是不用code split把bootstrap单独分离出来,它就在编译出的js文件最上面,由于须要先执行bootstrap后续的代码才能执行。咱们能够用CommonChunkPlugin
把它单独提出来,方便咱们阅读。把下面的代码写到你的webpack的plugin配置里便可:webpack
new webpack.optimize.CommonsChunkPlugin({
name: "manifest" // 能够叫manifest,也能够用runtime
}),
复制代码
配置以后,编译出来的文件会多出一个manifest.js
文件,这就是webpack bootstrap的代码了。bootstrap和用户代码(就是咱们本身写的部分)编译后的文件实际上是一个总体,因此后面的分析会引入用户代码一块儿看es6
manifest源码分为3个部分:web
__webpack_require__
(也就是咱们在本身的代码里用到的require
语法)咱们一个一个来看。下面的每一个部分,咱们都只截取manifest源码的相关部分来看,完整的源码放在文章最后了bootstrap
/******/ (function(modules) { // webpackBootstrap
// ......
// The module cache
/******/ var installedModules = {};
/******/
/******/ // objects to store loaded and loading chunks
/******/ var installedChunks = {
/******/ 1: 0
/******/ };
// ......
/******/ })
/************************************************************************/
/******/ ([]);
复制代码
咱们截取了manifest最外层的代码和初始化部分的代码,能够看到整个文件都被一个闭包括在里面,而modules
的初始值是一个空的Array
([]
)。 这样作能够隔离做用域,保护内部的变量不被污染segmentfault
modules
空的Array
([]
),用来存放每一个module的内容installedModules
存放module的cache,一个module被执行后(module的执行会在webpackJsonp的源码部分提到)的结果被保存到这里,以后再用到这个模块就能够直接使用缓存而无需再次执行了installedChunks
用来存放chunk的执行状况。若一个chunk已经加载了,在installedChunks里这个chunk的值会变成0,也就是无需再加载了若是分不清module和chunk这两个概念的区别,文章最后一节专门对此做了解释数组
在讲webpackJsonp的源码以前,先回忆一下咱们本身的chunk代码promise
// 编译前的入口文件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文件,也就是执行webpackJsonp
,从moreModules
里取出每一个module的内容,放到modules
里,而后执行入口文件对应的module。由于每次执行module,都会缓存这个module的执行结果,因此即便你没有抽取出每一个chunk里的相同module(CommonChunkPlugin),也不会重复执行重复的module
当咱们使用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的文件名
等于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从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这篇文章,写的比较详细
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.
webpack在__webpack_require__
上加了一些manifest.js里的变量引用,应该是给webpack内部js或者plugin加进来的js使用的:
若是你尝试在你的代码里使用这些变量或者require自己(不是用require来引入模块),webpack会把它编译成一个报错函数
Object.prototype.hasOwnProperty.call
的简写可能不少同窗搞不清楚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文件只有两个:
它们都是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的情形有两种:
第2点的异步模块,指的是经过require.ensure
或者import()
引入的模块,这些模块由于是异步加载的,会被单独打包到一个文件,在 触发加载条件时才会加载这个chunk.js
ok,咱们总结一下产生chunk的3种情形
/******/ (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; };
/******/ })
/************************************************************************/
/******/ ([]);
复制代码