内容简介:虽然一直在用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个参数:
-
chunkIdschunk的id,这里用了array,但一般一个文件就是一个chunk -
moreModuleschunk里所有模块的内容。模块内容可能不是很直观,再看上面编译后的代码,我们的代码被包在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 1webpackJsonp的代码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源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
重新定义团队:谷歌如何工作
拉兹洛·博克 / 宋伟 / 中信出版集团 / 2015-12-1 / CNY 56.00
谷歌首席人才官拉斯洛•博克权威力作,谷歌公开认可的谷歌高层作品,首度揭秘谷歌颠覆工业时代模式的人才和团队管理的核心法则,《纽约时报》畅销榜第一名,Business Insider 2015最佳商业书籍,谷歌的创造力就在于此! 编辑推荐! 1、 谷歌人才官首次公开谷歌人才和团队管理的核心秘籍 在谷歌执掌人事多年的拉斯洛•博克是人才和团队管理的顶级专家。他加入谷歌后,谷歌的员工数从六......一起来看看 《重新定义团队:谷歌如何工作》 这本书的介绍吧!