雖然一直在用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
的執行程式碼。那麼問題來了:
- 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; };
/******/ })
/************************************************************************/
/******/ ([]);
複製程式碼