webpack bootstrap源码解读

虽然一直在用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的执行代码。那么问题来了:

  1. webpackJsonp是在哪里定义的,它是干什么用的?
  2. 包裹原来代码的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个部分:

  1. 创建了一个闭包,初始化需要用到的变量
  2. 定义webpackJsonp方法,挂载到window变量下
  3. 定义与编译相关的辅助函数和变量,如__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 2part 3是一体的,它的作用是在chunk还没加载好时就被使用了,这时先返回一个promise,等chunk加载好了,这个promise会resolve,通知调用者可以使用这个chunk了。因为chunk的js文件需要通过网络,不能保证什么时候加载好,才会用到promise。我们先看看是怎么实现的:

    其实应该倒过来先看part 3再看part 2part 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的源码分析),所以加了一个.thenrequire这个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使用的:

  1. webpack_require.m modules的引用
  2. webpack_require.c installedModules的引用

如果你尝试在你的代码里使用这些变量或者require本身(不是用require来引入模块),webpack会把它编译成一个报错函数

一些工具函数的简写

  1. webpack_require.o Object.prototype.hasOwnProperty.call的简写
  2. 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文件只有两个:

  1. manifest.js,也就是bootstrap代码
  2. 你的源代码编译后的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的情形有两种:

  1. 通过CommonChunkPlugin分离出的chunk
  2. 异步模块产生的chunk

第2点的异步模块,指的是通过require.ensure或者import()引入的模块,这些模块因为是异步加载的,会被单独打包到一个文件,在 触发加载条件时才会加载这个chunk.js

ok,我们总结一下产生chunk的3种情形

  1. entry chunk 也就是入口文件产生的chunk,这个必有
  2. initial chunk 也就是manifest生成的chunk,这个也是必有
  3. 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; };
/******/ })
/************************************************************************/
/******/ ([]);
复制代码

你可能感兴趣的:(webpack bootstrap源码解读)