雖然一直在用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; };
/******/ })
/************************************************************************/
/******/ ([]);
複製代碼