内容简介:虽然一直在用webpack,但很少去看它编译出来的js代码,大概是因为调试的时候有sourcemap,可以直接调试源码。一时心血来潮想研究一下,看了一些关于webpack编译方面的文章都有提到,再结合自己看源码的体会,记录一下自己的理解说bootstap可能还有点不好理解,看一下webpack编译出来的js文件就很好理解了:编译后的文件跟我们的源文件不太一样了,原本的内容被放到了一个
虽然一直在用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
的执行代码。那么问题来了:
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; }; /******/ }) /************************************************************************/ /******/ ([]);
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- Redux 源码解读 —— 从源码开始学 Redux
- AQS源码详细解读
- SDWebImage源码解读《一》
- MJExtension源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。