在平時開發中咱們常常會用到Webpack
這個時下最流行的前端打包工具。它打包開發代碼,輸出能在各類瀏覽器運行的代碼,提高了開發至發佈過程的效率。前端
咱們知道一份Webpack
配置文件主要包含入口(entry
)、輸出文件(output
)、模式、加載器(Loader
)、插件(Plugin
)等幾個部分。但若是隻須要組織 JS 文件的話,指定入口和輸出文件路徑便可完成一個迷你項目的打包。下面咱們來經過一個簡單的項目來看一下Webpack
是怎樣運行的。webpack
本文使用 webpack ^4.30.0 做示例.爲了更好地觀察產出的文件,咱們將模式設置爲 development 關閉代碼壓縮,再開啓 source-map 支持原始源代碼調試。除此以外。咱們還簡單的寫了一個插件
MyPlugin
來去除源碼中的註釋。
新建src/index.js
git
console.log('Hello webpack!');
新建webpack
配置文件webpack.config.js
github
const path = require('path'); const MyPlugin = require('./src/MyPlugin.js') module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist') }, plugins:[ new MyPlugin() ] };
新建src/MyPlugin.js
。瞭解webpack插件更多信息web
class MyPlugin { constructor(options) { this.options = options this.externalModules = {} } apply(compiler) { var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g compiler.hooks.emit.tap('CodeBeautify', (compilation)=> { Object.keys(compilation.assets).forEach((data)=> { let content = compilation.assets[data].source() // 欲處理的文本 content = content.replace(reg, function (word) { // 去除註釋後的文本 return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word; }); compilation.assets[data] = { source(){ return content }, size(){ return content.length } } }) }) } } module.exports = MyPlugin
如今咱們運行命令 webpack --config webpack.config.js
,打包完成後會多出一個輸出目錄 dist
:dist/main.js
。main
是 webpack
默認設置的輸出文件名,咱們快速瞄一眼這個文件:json
(function(modules){ // ... })({ "./src/index.js": (function(){ // ... }) });
整個文件只含一個當即執行函數(IIFE)
,咱們稱它爲 webpackBootstrap
,它僅接收一個對象 —— 未加載的 模塊集合(modules
),這個 modules
對象的 key
是一個路徑,value
是一個函數。你也許會問,這裏的模塊是什麼?它們又是如何加載的呢?
在細看產出代碼前,咱們先豐富一下源代碼:
新文件 src/utils/math.js
:數組
export const plus = (a, b) => { return a + b; };
修改src/index.js
:promise
import { plus } from './utils/math.js'; console.log('Hello webpack!'); console.log('1 + 2: ', plus(1, 2));
咱們按照 ES 規範的模塊化語法寫了一個簡單的模塊 src/utils/math.js,給 src/index.js 引用。Webpack 用本身的方式支持了 ES6 Module 規範,前面提到的 module 就是和 ES6 module 對應的概念。瀏覽器
接下來咱們看一下這些模塊是如何通 ES5 代碼實現的。再次運行命令 webpack --config webpack.config.js
後查看輸出文件:緩存
(function(modules){ // ... })({ "./src/index.js": (function(){ // ... }), "./src/utils/math.js": (function() { // ... }) });
IIFE 傳入的 modules 對象裏多了一個鍵值對,對應着新模塊 src/utils/math.js,這和咱們在源代碼中拆分的模塊互相呼應。然而,有了 modules 只是第一步,這份文件最終達到的效果應該是讓各個模塊按開發者編排的順序運行。
接下來看看 webpackBootstrap
函數中有些什麼:
// webpackBootstrap (function(modules){ // 緩存 __webpack_require__ 函數加載過的模塊 var installedModules = {}; /** * Webpack 加載函數,用來加載 webpack 定義的模塊 * @param {String} moduleId 模塊 ID,通常爲模塊的源碼路徑,如 "./src/index.js" * @returns {Object} exports 導出對象 */ function __webpack_require__(moduleId) { // ... } // 在 __webpack_require__ 函數對象上掛載一些變量及函數 ... // 傳入表達式的值爲 "./src/index.js" return __webpack_require__(__webpack_require__.s = "./src/index.js"); })(/* modules */);
能夠看到其實主要作了兩件事:
__webpack_require__
。"./src/index.js"
。整個 webpackBootstrap 中只出現了入口模塊的影子,那其餘模塊又是如何加載的呢?咱們順着 __webpack_require__("./src/index.js") 細看加載函數的內部邏輯:
function __webpack_require__(moduleId) { // 重複加載則利用緩存 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 若是是第一次加載,則初始化模塊對象,並緩存 var module = installedModules[moduleId] = { i: moduleId, // 模塊 ID l: false, // 模塊加載標識 exports: {} // 模塊導出對象 }; /** * 執行模塊 * @param module.exports -- 模塊導出對象引用,改變模塊包裹函數內部的 this 指向 * @param module -- 當前模塊對象引用 * @param module.exports -- 模塊導出對象引用 * @param __webpack_require__ -- 用於在模塊中加載其餘模塊 */ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 模塊加載標識置爲已加載 module.l = true; // 返回當前模塊的導出對象引用 return module.exports; }
首先,加載函數使用了閉包變量 installedModules
,用來將已加載過的模塊保存在內存中。 接着是初始化模塊對象,並把它掛載到緩存裏。而後是模塊的執行過程,加載入口文件時 modules[moduleId]
其實就是 ./src/index.js
對應的模塊函數。執行模塊函數前傳入了跟模塊相關的幾個實參,讓模塊能夠導出內容,以及加載其餘模塊的導出。最後標識該模塊加載完成,返回模塊的導出內容。
根據 __webpack_require__
的緩存和導出邏輯,咱們得知在整個 IIFE
運行過程當中,加載已緩存的模塊時,都會直接返回installedModules[moduleId].exports
,換句話說,相同的模塊只有在第一次引用的時候纔會執行模塊自己。
__webpack_require__
中經過 modules[moduleId].call()
運行了模塊執行函數,下面咱們就進入到 webpackBootstrap
的參數部分,看看模塊的執行函數。
/*** 入口模塊 ./src/index.js ***/ "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; // 用於區分 ES 模塊和其餘模塊規範,不影響理解 demo,戰略跳過。 __webpack_require__.r(__webpack_exports__); /* harmony import */ // 源模塊代碼中,`import {plus} from './utils/math.js';` 語句被 loader 解析轉化。 // 加載 "./src/utils/math.js" 模塊, var _utils_math_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils/math.js"); console.log('Hello webpack!'); console.log('1 + 2: ', Object(_utils_math_js__WEBPACK_IMPORTED_MODULE_0__["plus"])(1, 2)); }), "./src/utils/math.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ // 源模塊代碼中,`export` 語句被 loader 解析轉化。 __webpack_require__.d(__webpack_exports__, "plus", function () { return plus; }); const plus = (a, b) => { return a + b; }; })
執行順序是:入口模塊 -> 工具模塊 -> 入口模塊。入口模塊中首先就經過 __webpack_require__("./src/utils/math.js")
拿到了工具模塊的 exports
對象。再看工具模塊,ES
導出語法轉化成了__webpack_require__.d(__webpack_exports__, [key], [getter])
,而 __webpack_require__.d
函數的定義在 webpackBootstrap
內:
// 定義 exports 對象導出的屬性。 __webpack_require__.d = function (exports, name, getter) { // 若是 exports (不含原型鏈上)沒有 [name] 屬性,定義該屬性的 getter。 if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; // 包裝 Object.prototype.hasOwnProperty 函數。 __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
可見 __webpack_require__.d
其實就是 Object.defineProperty
的簡單包裝.
引用工具模塊導出的變量後,入口模塊再執行它剩餘的部分。至此,Webpack 基本的模塊執行過程就結束了。
好了,咱們用流程圖總結一下 Webpack 模塊的加載思路:
有上面的打包咱們發現將不一樣的打包進一個 main.js
文件。main.js
會集中消耗太多網絡資源,致使用戶須要等待好久才能夠開始與網頁交互。
通常的解決方式是:根據需求下降首次加載文件的體積,在須要時(如切換前端路由器,交互事件回調)異步加載其餘文件並使用其中的模塊。
Webpack 推薦用 ES import() 規範來異步加載模塊,咱們根據 ES 規範修改一下入口模塊的 import 方式,讓其可以異步加載模塊:
src/index.js
console.log('Hello webpack!'); window.setTimeout(() => { import('./utils/math').then(mathUtil => { console.log('1 + 2: ' + mathUtil.plus(1, 2)); }); }, 2000);
工具模塊(src/utils/math.js)
依然不變,在webpack
配置裏,咱們指定一下資源文件的公共資源路徑(publicPath)
,後面的探索過程當中會遇到。
const path = require('path'); const MyPlugin = require('./src/MyPlugin.js') module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), publicPath: '/dist/' }, plugins:[ new MyPlugin() ] };
接着執行一下打包,能夠看到除了 dist/main.js
外,又多了一個 dist/0.js
./src/utils/math.js
。模塊從main chunk
遷移到了 0 chunk
中。而與 demo1
不一樣的是,main chunk
中添加了一些用於異步加載的代碼,咱們概覽一下:
// webpackBootstrap (function (modules) { // 加載其餘 chunk 後的回調函數 function webpackJsonpCallback(data) { // ... } // ... // 用於緩存 chunk 的加載狀態,0 爲已加載 var installedChunks = { "main": 0 }; // 拼接 chunk 的請求地址 function jsonpScriptSrc(chunkId) { // ... } // 同步 require 函數,內容不變 function __webpack_require__(moduleId) { // ... } // 異步加載 chunk,返回封裝加載過程的 promise __webpack_require__.e = function requireEnsure(chunkId) { // ... } // ... // defineProperty 的包裝,內容不變 __webpack_require__.d = function (exports, name, getter) {} // ... // 根據配置文件肯定的 publicPath __webpack_require__.p = "/dist/"; /**** JSONP 初始化 ****/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); var parentJsonpFunction = oldJsonpFunction; /**** JSONP 初始化 ****/ return __webpack_require__(__webpack_require__.s = "./src/index.js"); })({ "./src/index.js": (function(module, exports, __webpack_require__) { document.write('Hello webpack!\n'); window.setTimeout(() => { __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./utils/math */ "./src/utils/math.js")).then(mathUtil => { console.log('1 + 2: ' + mathUtil.plus(1, 2)); }); }, 2000); }) })
能夠看到 webpackBootstrap
的函數體部分增長了一些內容,參數部分移除了 "./src/utils/math.js"
模塊。跟着包裹函數的執行順序,咱們先聚焦到「JSONP 初始化」
部分:
// 存儲 jsonp 的數組,首次運行爲 [] var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; // 保存 jsonpArray 的 push 函數,首次運行爲 Array.prototype.push var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 將 jsonpArray 的 push 重寫爲 webpackJsonpCallback (加載其餘 chunk 後的回調函數) jsonpArray.push = webpackJsonpCallback; // 將 jsonpArray 重置爲正常數組,push 重置爲 Array.prototype.push jsonpArray = jsonpArray.slice(); // 因爲 jsonpArray 爲 [],不作任何事 for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); // Array.prototype.push var parentJsonpFunction = oldJsonpFunction;
初始化結束後,變化就是 window 上掛載了一個 webpackJsonp 數組,它的值爲 [];此外,這個數組的 push 被改寫爲 webpackJsonpCallback 函數,咱們在後面會提到這些準備工做的做用。
接着是 __webpack_require__
入口模塊,因爲 __webpack_require__
函數沒有改變,咱們繼續觀察入口模塊執行函數有了什麼變化。
顯然,import('../utils/math.js')
被轉化爲__webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/utils/math.js"))
。0 是 ./src/utils/math.js
所在 chunk
的id
,「同步加載模塊」的邏輯拆分紅了「先加載 chunk
,完成後再加載模塊」。
咱們翻到 __webpack_require__.e
的定義位置:
__webpack_require__.e = function requireEnsure(chunkId) { var promises = []; // installedChunks 是在 webpackBootstrap 中維護的 chunk 緩存 var installedChunkData = installedChunks[chunkId]; // chunk 未加載 if(installedChunkData !== 0) { // installedChunkData 爲 promise 表示 chunk 加載中 if(installedChunkData) { promises.push(installedChunkData[2]); } else { /*** 首次加載 chunk: ***/ // 初始化 promise 對象 var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); // 建立 script 標籤加載 chunk var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); var onScriptComplete; // ... 省略一些 script 屬性設置 // src 根據 publicPath 和 chunkId 拼接 script.src = jsonpScriptSrc(chunkId); // 加載結束回調函數,處理 script 加載完成、加載超時、加載失敗的狀況 onScriptComplete = function (event) { script.onerror = script.onload = null; // 避免 IE 內存泄漏問題 clearTimeout(timeout); var chunk = installedChunks[chunkId]; // 處理 script 加載完成,但 chunk 沒有加載完成的狀況 if(chunk !== 0) { // chunk 加載中 if(chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'); error.type = errorType; error.request = realSrc; // reject(error) chunk[1](error); } // 統一將沒有加載的 chunk 標記爲未加載 installedChunks[chunkId] = undefined; } }; // 設置 12 秒超時時間 var timeout = setTimeout(function(){ onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; head.appendChild(script); /*** 首次加載 chunk ***/ } } return Promise.all(promises); };
看起來有點長,咱們一步步剖析,先從第一行和最後一行來看,整個函數將異步加載的過程封裝到了 promise 中,最終導出。
接着從第二行開始,installedChunkData
從緩存中取值,顯然首次加載 chunk 時此處是 undefined。接下來,installedChunkData
的 undefined
值觸發了第一層 if 語句的判斷條件。緊接着進行到第二層 if 語句,此時根據判斷條件走入 else
塊,這裏 if
塊裏的內容咱們先戰略跳過,else 裏主要有兩塊內容,一是 chunk 腳本加載過程,這個過程建立了一個 script
標籤,使其請求 chunk
所在地址並執行 chunk
內容;二是初始化 promise
,並用 promise
控制 chunk
文件加載過程。
不過,咱們只在這段 else
代碼塊中找到了 reject
的使用處,也就是在 chunk
加載異常時 chunk[1](error)
的地方,但並沒發現更重要的 resolve
的使用地點,僅僅是把 resolve
掛在了緩存上(installedChunks[chunkId] = [resolve, reject])
。
這裏的 chunk
文件加載下來會發生什麼呢?讓咱們打開dist/0.js
一探究竟:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], { "./src/utils/math.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "plus", function () { return plus; }); const plus = (a, b) => { return a + b; }; }) }]);
咱們發現了:
這段代碼開始執行,把異步加載相關的 chunk id
與模塊傳給 push
函數。而前面已經提到過,window["webpackJsonp"]
數組的 push 函數已被重寫爲 webpackJsonpCallback
函數,它的定義位置在 webpackBootstrap
中:
function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = []; // 將 chunk 標記爲已加載 for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } // 把 "moreModules" 加到 webpackBootstrap 中的 modules 閉包變量中。 for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } // parentJsonpFunction 是 window["webpackJsonp"] 的原生 push // 將 data 加入全局數組,緩存 chunk 內容 if(parentJsonpFunction) parentJsonpFunction(data); // 執行 resolve 後,加載 chunk 的 promise 狀態變爲 resolved,then 內的函數開始執行。 while(resolves.length) { resolves.shift()(); } };
走進這個函數中,意味着異步加載的 chunk 內容已經拿到,這個時候咱們要完成兩件事,一是讓依賴此次異步加載結果的模塊繼續執行,二是緩存加載結果。
關於第一點,咱們回憶一下以前 __webpack_require__.e
的內容,此時 chunk
還處於「加載中」的狀態,也就是說對應的 installedChunks[chunkId]
的值此時爲 [resolve, reject, promise]
。 而這裏,chunk 已經加載,但 promise
還未決議,因而 webpackJsonpCallback
內部定義了一個 resolves
變量用來收集 installedChunks
上的 resolve
並執行它。
接下來講到第二點,就要涉及幾個層面的緩存了。
首先是 chunk 層面,這裏有兩個相關操做,操做一將 installedChunks[chunkId]
置爲 0 可讓 __webpack_require__.e
在第二次加載同一 chunk
時返回一個當即決議的 promise(Promise.all([]))
;操做二將 chunk data
添加進 window["webpackJsonp"]
數組,能夠在多入口模式時,方便地拿到已加載過的 chunk
緩存。經過如下代碼實現:
/*** 緩存執行部分 ***/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; // ... for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); var parentJsonpFunction = oldJsonpFunction; /*** 緩存執行部分 ***/ /*** 緩存添加部分 ***/ function webpackJsonpCallback(data) { //... // 此處的 parentJsonpFunction 是 window["webpackJsonp"] 數組的原生 push if (parentJsonpFunction) parentJsonpFunction(data); //... } /*** 緩存添加部分 ***/
而在 modules 層面,chunk
中的 moreModules
被合入入口文件的 modules
中,可供下一個微任務中的 __webpack_require__
同步加載模塊。
({ "./src/index.js": (function (module, exports, __webpack_require__) { console.log('Hello webpack!'); window.setTimeout(() => { __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/utils/math.js")).then(mathUtil => { console.log('1 + 2: ' + mathUtil.plus(1, 2)); }); }, 2000); }) });
__webpack_require__.e(0)
返回的 promise 決議後,__webpack_require__.bind(null, "./src/utils/math.js")
能夠加載到 chunk
攜帶的模塊,並返回模塊做爲下一個微任務函數的入參,接下來就是 Webpack Loader
翻譯過的其餘業務代碼了。
如今讓咱們把異步流程梳理一下: