打包原理
- 簡單講就是生成ast語法樹,根據語法樹生成對應的js代碼
- 這裏僅分析打包輸出的結果,從結果分析webpack對咱們代碼作了啥
分析
// main.js
// 經過CommonJS規範導入
const show = require('./show.js');
// 執行 show 函數
show('Webpack');
// show.js
// 操做 DOM 元素,把 content 顯示到網頁上
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 經過 CommonJS 規範導出 show 函數
module.exports = show;
// bundle.js
(
// webpackBootstrap 啓動函數
// modules 即爲存放全部模塊的數組,數組中的每個元素都是一個函數
function (modules) {
// 安裝過的模塊都存放在這裏面
// 做用是把已經加載過的模塊緩存在內存中,提高性能
var installedModules = {};
// 去數組中加載一個模塊,moduleId 爲要加載模塊在數組中的 index
// 做用和 Node.js 中 require 語句類似
function __webpack_require__(moduleId) {
// 若是須要加載的模塊已經被加載過,就直接從內存緩存中返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 若是緩存中不存在須要加載的模塊,就新建一個模塊,並把它存在緩存中
var module = installedModules[moduleId] = {
// 模塊在數組中的 index
i: moduleId,
// 該模塊是否已經加載完畢
l: false,
// 該模塊的導出值
exports: {}
};
// 從 modules 中獲取 index 爲 moduleId 的模塊對應的函數
// 再調用這個函數,同時把函數須要的參數傳入
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 把這個模塊標記爲已加載
module.l = true;
// 返回這個模塊的導出值
return module.exports;
}
// Webpack 配置中的 publicPath,用於加載被分割出去的異步代碼
__webpack_require__.p = "";
// 使用 __webpack_require__ 去加載 index 爲 0 的模塊,而且返回該模塊導出的內容
// index 爲 0 的模塊就是 main.js 對應的文件,也就是執行入口模塊
// __webpack_require__.s 的含義是啓動模塊對應的 index
return __webpack_require__(__webpack_require__.s = 0);
})(
// 全部的模塊都存放在了一個數組裏,根據每一個模塊在數組的 index 來區分和定位模塊
[
/* 0 */
(function (module, exports, __webpack_require__) {
// 經過 __webpack_require__ 規範導入 show 函數,show.js 對應的模塊 index 爲 1
const show = __webpack_require__(1);
// 執行 show 函數
show('Webpack');
}),
/* 1 */
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 經過 CommonJS 規範導出 show 函數
module.exports = show;
})
]
);
// 以上看上去複雜的代碼實際上是一個當即執行函數,能夠簡寫爲以下:
(function(modules) {
// 模擬 require 語句
function __webpack_require__() {
}
// 執行存放全部模塊數組中的第0個模塊
__webpack_require__(0);
})([/*存放全部模塊的數組*/])
- 能夠看到
bundle.js
是一個自執行函數,入參就是main.js
和show.js
改造後的代碼塊所構成的數組
- 自執行函數裏運行了
__webpack_require__
這個函數,入參是0,0其實就是代碼塊數組中對應的入參,表示第一個代碼塊
- 再來看
__webpack_require__
函數,首先執行的是緩存判斷,經過moduleId
判斷以前是否已經加載過,若是加載過,直接返回直接的加載結果exports
,mouduleId
就是不一樣代碼模塊在入參數組中的index
- 而若是沒有加載過,則新建一個對象,重要的是這個對象中的
exports
屬性,裏面存放的就是加載模塊後,對應模塊export出來的東西
- 而後用這個
exports
做爲上下文去執行對應的代碼塊,傳遞參數爲剛纔新建的module,module裏的exports,以及__webpack_require__
這個方法自己
- 而後看到
main.js
中的require被改形成了__webpack_require__
,__webpack_require__(1)
表明加載第二個代碼塊
- 第二個代碼塊中,定義了show這個方法,而後show會做爲module.exports的導出,也就是賦值給了
installedModules[0].module.exports
,也就是這個導出已經被緩存起來了,下次再有別的地方用到,會直接被導出
- 這就是webpack大體的打包思路,將各個單獨的模塊改形成數組做爲入參,傳給自執行函數,同時維護一個
installedModules
記錄加載過的模塊,利用模塊數組中的index做爲key值,exports記錄導出對象
按需加載
- 因爲單頁應用也會有路由這個概念,在沒有切換到對應路由以前,可能並不但願瀏覽器對這部分頁面的js進行下載,從而提高首頁打開的速度,就涉及到一個懶加載,即按需加載的問題
- webpack的按需加載是經過
import(XXX)
實現的,import()是一個提案,而webpack支持了它
// 異步加載 show.js
import(/* webpackChunkName: 'show' */ './show').then((module) => {
// 執行 show 函數
const show = module.default;
show('Webpack');
});
- 經過這種方式打包,咱們能夠發現最終打包出來的文件分紅了兩個,
bundle.js
和show.xxx.js
- 其中
/* webpackChunkName: 'show' */
是專門註釋給webpack看的,爲的是指定按需加載的包的名字,同時記得在webpack的配置文件的entry中,配置chunkFilename: '[name].[hash].js'
,否則這個指定不會生效
- 先來看入口文件,將暫時沒有用到的函數都隱藏後以下:
(function (modules) {
// webpackJsonp 用於從異步加載的文件中安裝模塊
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// ... 先省略
};
// 緩存已經安裝的模塊
var installedModules = {};
// 存儲每一個 Chunk 的加載狀態;
// 鍵爲 Chunk 的 ID,值爲0表明已經加載成功
var installedChunks = {
1: 0
};
// 模擬 require 語句,和上面介紹的一致
function __webpack_require__(moduleId) {
// ... 省略和上面同樣的內容
}
// 用於加載被分割出去的,須要異步加載的 Chunk 對應的文件
__webpack_require__.e = function requireEnsure(chunkId) {
// ... 先省略
};
// 加載並執行入口模塊,和上面介紹的一致
return __webpack_require__(__webpack_require__.s = 0);
})
(
// 存放全部沒有通過異步加載的,隨着執行入口文件加載的模塊
[
// main.js 對應的模塊
(function (module, exports, __webpack_require__) {
// 經過 __webpack_require__.e 去異步加載 show.js 對應的 Chunk
__webpack_require__.e('show').then(__webpack_require__.bind(null, 'show')).then((show) => {
// 執行 show 函數
show('Webpack');
});
})
]
);
- 能夠看到
import(xxx).then
被替換成了__webpack_require__.e(0).then
,__webpack_require__.e(0)
返回了一個promise
- 第一個then裏至關於執行了
__webpack_require__(1)
,但很明顯能夠看到自執行函數的入參數組只有一個元素,不存在[1],這個[1]是何時被插入的呢
- 看一下
__webpack_require__.e
的實現
__webpack_require__.e = function requireEnsure(chunkId) {
// 從上面定義的 installedChunks 中獲取 chunkId 對應的 Chunk 的加載狀態
var installedChunkData = installedChunks[chunkId];
// 若是加載狀態爲0表示該 Chunk 已經加載成功了,直接返回 resolve Promise
if (installedChunkData === 0) {
return new Promise(function (resolve) {
resolve();
});
}
// installedChunkData 不爲空且不爲0表示該 Chunk 正在網絡加載中
if (installedChunkData) {
// 返回存放在 installedChunkData 數組中的 Promise 對象
return installedChunkData[2];
}
// installedChunkData 爲空,表示該 Chunk 尚未加載過,去加載該 Chunk 對應的文件
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 經過 DOM 操做,往 HTML head 中插入一個 script 標籤去異步加載 Chunk 對應的 JavaScript 文件
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
// 文件的路徑爲配置的 publicPath、chunkId 拼接而成
script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
// 設置異步加載的最長超時時間
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
// 在 script 加載和執行完成時回調
function onScriptComplete() {
// 防止內存泄露
script.onerror = script.onload = null;
clearTimeout(timeout);
// 去檢查 chunkId 對應的 Chunk 是否安裝成功,安裝成功時纔會存在於 installedChunks 中
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;
};
- 首先判斷這個chunkId是否已經加載過,若是是的話,直接返回一個resolve的promise
- 若是不爲空又不爲0,說明正在加載中,這裏的
installedChunks[chunkId]
是一個數組,裏面保存着[resovle, reject]
,是在發起網絡請求的時候賦值的
- 若是上面兩個判斷都沒擊中,說明是沒有加載過,下面開始構造加載方法,主要是經過jsonp的形式
- 首先新建一個promise,並對
installedChunks[chunkId]
賦值,把這個promise以及他的resolve和reject保存在裏面,這也是上面爲何能夠經過判斷installedChunks[chunkId]
不爲空又不爲0即正處於請求當中,直接返回數組第三個值,即新建的promise,讓後續操做能夠在這個promise上進行回調的註冊
- 而後後面的方法就是經過構造一個script標籤,插入到head中,保證代碼能立刻被下載,同時定義代碼執行完畢時的回調,判斷是已經加載了代碼,若是加載成功清除監聽等,若是加載失敗,拋出異常
- 最後返回這個promise,供外部註冊回調
- 而這裏經過jsonp加載的代碼就是打包分離出來的另外一個文件
show.xx.js
,也就是異步加載的show.js
相關的代碼
webpackJsonp(
// 在其它文件中存放着的模塊的 ID
['show'],
// 本文件所包含的模塊
{// show.js 所對應的模塊
show: (function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
module.exports = show;
})
}
);
- 接着看webpackJsonp這個方法是怎麼定義的
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// 把 moreModules 添加到 modules 對象中
// 把全部 chunkIds 對應的模塊都標記成已經加載成功
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];
}
}
while (resolves.length) {
resolves.shift()();
}
};
chunkIds
表明本身這個文件的id名,由於動態加載的時候,是利用動態加載的文件名構成script標籤進行下載的,這裏傳入這個id是爲了觸發後續promise的resolve以及標記模塊以及被加載
moreModules
就是對應的代碼模塊集合
executeModules
就是加載完成後須要被執行模塊的index
- 首先遍歷
installedChunks
,前面提到過installedChunks[chunkId]
經過網絡下載的時候,回賦予三個值,表明其對應的promise,這裏取出第一個resolve,保存起來,同時將加載標記置爲0,表示已加載
- 而後遍歷動態加載的模塊,把代碼塊塞到modules數組裏
- 最後執行以前保存下來的resolve函數,觸發
__webpack_require__.e(0).then
的執行
- 這樣動態加載的代碼經過構造jsonp進行下載,而且將對應代碼傳到
bundle.js
的modules中進行保存,而後在then函數中經過__webpack_require__
執行模塊,緩存輸出
- 這裏爲了便於理解,有對代碼作必定調整,真實的輸出狀況,能夠經過具體打包輸出查看,這裏僅描述具體打包思路