做者:崔靜javascript
webpack 對於每一個前端兒來講都不陌生,它將每一個靜態文件當成一個模塊,通過一系列的處理爲咱們整合出最後的須要的 js、css、圖片、字體等文件。來自官網的圖很形象的闡述了 webpack 的功能 —— bundle js / css / ... (打包全世界ヾ(◍°∇°◍)ノ゙)css
在閱讀一個東西的源碼以前,首先須要瞭解這個東西是什麼,怎麼用。這樣在閱讀源碼過程當中才能在大腦中造成一副總體的認知。因此,先了解一下 webpack 打包先後代碼發生了什麼?找一個簡單的例子前端
入口文件爲 main.js, 在其中引入了 a.js, b.jsjava
// main.js
import { A } from './a'
import B from './b'
console.log(A)
B()
複製代碼
// a.js
export const A = 'a'
複製代碼
// b.js
export default function () {
console.log('b')
}
複製代碼
通過 webpack 的一番蹂躪,最後變成了一個文件:bundle.js。先忽略細節,看最外面的代碼結構webpack
(function(modules){
...(webpack的函數)
return __webpack_require__(__webpack_require__.s = "./demo01/main.js");
})(
{
"./demo01/a.js": (function(){...}),
"./demo01/b.js": (function(){...}),
"./demo01/main.js": (function(){...}),
}
)
複製代碼
最外層是一個當即執行函數,參數是 modules。 a.js、b.js 和 main.js 最後被編譯成三個函數(下文將這三個函數稱爲 module 函數),key 是文件的相對路徑。bundle.js 會執行到 __webpack_require__(__webpack_require__.s = "./demo01/main.js");
即經過 __webpack_require__('./demo01/main.js')
開始主入口函數的執行。web
經過 bundle.js 的主接口能夠清晰的看出,對於 webpack 每一個文件就是一個 module。 咱們寫的 import 'xxx'
,則最終爲 __webpack_require__
函數執行。更多的時候咱們使用的是 import A from 'xxx'
或者 import { B } from 'xxx'
,能夠猜測一下,這個 __webpack_require__
函數中除了找到對應的 'xxx' 來執行,還須要一個返回 'xxx' 中 export 出來的內容。json
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;
}
複製代碼
調用每個 module 函數時,參數爲 module
、module.exports
、__webpack_require__
。 module.exports
用來收集 module 中全部的 export xxx 。看 」./demo/a.js「 的 modulepromise
(function(module, __webpack_exports__, __webpack_require__) {
// ...
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });
const A = 'a'
/***/ })
// ...
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// ...
複製代碼
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });
簡單理解就是緩存
__webpack_exports__.A = A;
複製代碼
而 __webpack_exports__
實際爲上面的 __webpack_require__
中傳入的 moule.exports
, 如此,就將 A 變量收集到了 module.exports
中。如此咱們的bash
import { A } from './a.js'
console.log(A)
複製代碼
就編譯爲
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./demo/a.js");
console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A"])
複製代碼
對於 b.js 咱們使用的是 export default
,webpack 處理後,會在 module.exports 中增長一個 default 屬性。
__webpack_exports__["default"] = (function () {
console.log('b')
});
複製代碼
最後 import B from './b.js
編譯爲
var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./demo/b.js")
Object(_b__WEBPACK_IMPORTED_MODULE_1__["default"])()
複製代碼
在 webpack 中咱們能夠很方便的實現異步加載,以簡單的 demo 入手
// c.js
export default {
key: 'something'
}
複製代碼
// main.js
import('./c').then(test => {
console.log(test)
})
複製代碼
打包結果,異步加載的 c.js,最後打包在一個單獨的文件 0.js 中
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./demo/c.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = ({
key2: 'key2'
});
})
}]);
複製代碼
簡化一下,執行的就是
var t = window["webpackJsonp"] = window["webpackJonsp"] || [])
t.push([[0], {function(){...}}])
複製代碼
執行 import('./c.js') 時,實際上經過在 HTML 中插入一個 script 標籤加載 0.js。 0.js 加載後會執行 window["webpackJsonp"].push 方法。 在 main.js 在還有一段:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
複製代碼
這裏篡改了一下, window["webpackJsonp"] 的 push 方法,將 push 方法外包裝了一層 webpackJonspCallback 的邏輯。當 0.js 加載後,會執行 window["webpackJsonp"].push
,這時便會進入 webpackJsonpCallback 的執行邏輯。
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
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(data);
while(resolves.length) {
resolves.shift()();
}
};
複製代碼
在 webpackJsonpCallback 中會將 0.js 中的 chunks 和 modules 保存到全局的 modules 變量中,並設置 installedChunks 的標誌位。
有兩點須要詳細說明的:
咱們知道 import('xxx.js') 會返一個 Promise 實例 promise,在 webpack 打包出來的最終文件中是如何處理這個 promise 的?
在加載 0.js 以前會在全局 installedChunks
中先存入了一個 promise 對象
installedChunks[chunkId] = [resolve, reject, promise]
複製代碼
resolve 這個值在 webpackJsonpCallback 中會被用到,這時就會進入到咱們寫的 import('./c.js').then()
的 then 語句中了。
在 main.js 中處理 webpackJsonp 過程當中還有一段特殊的邏輯:
jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
...
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
複製代碼
也就是說若是以前已經存在全局的 window["webpackJsonp"]
那麼在替換其 push 函數以前會將原有的 push 方法保存爲 oldJsonpFunction,同時將已存在於 window["webpackJsonp"]
中的內容,一一執行 webpackJsonpCallback
。而且在 webpackJsonpCallback
中也將異步加載的內容也會在 parentJsonpFunction
中一樣執行一次
if(parentJsonpFunction) parentJsonpFunction(data);
複製代碼
這樣的同步意義何在?試想下面的場景,webpack 中多入口狀況下,例如以下配置
{
entry: {
bundle1: 'bundle1.js',
bundle2: 'bundle2.js'
}
}
複製代碼
而且 bundle1 和 bundle2 中都用到了異步加載了 0.js。並且在同一個頁面中同時加載了 bundle1 和 bundle2。那麼因爲上面的邏輯,執行的流程以下圖:
經過上圖能夠看到,這樣設計對於多入口的地方,能夠將 bundle1.js 和 bundle2.js 中異步模塊進行同步,這樣不只保證了 0.js 能夠同時在兩個文件中被引用,並且不會重複加載。
異步加載中,有兩個須要注意的地方:
Promise
在 webpack 異步加載使用了 Promise。要兼容低版本的安卓,好比4.x 的代碼來講,須要有全局的 Promise polyfill。
window["webpackJsonp"]
若是一個 HTML 頁面中,會加載多個 webpack 獨立打包出來的文件。那麼這些文件異步加載的回調函數,默認都叫 "webpackJonsp",會相互衝突。須要經過 output.jsonpFunction 配置修改這個默認的函數名稱。
知道上面的產出,根據產出看 webpack 的總流程。這裏咱們暫時不考慮 webpack 的緩存、錯誤處理、watch 等邏輯,只看主流程。 首先會有一個入口文件寫在配置文件中,肯定 webpack 從哪一個文件開始處理。
step1 webpack 配置文件處理
咱們在寫配置文件中 entry 的時候,確定寫過 ./main.js
這時一個相對目錄,因此會有一個將相對目錄變成絕對目錄的處理
step2 文件位置解析
webpack 須要從入口文件開始,順藤摸瓜找到全部的文件。那麼會有一個
step3 加載文件 step4 文件解析 step5 從解析結果中找到文件引入的其餘文件
在加載文件的時候,咱們會在 webpack 中配置不少的 loaders 來處理 js 文件的 babel 轉化等等,還應該有文件對應的 loader 解析,loader 執行。
step3.1 找到全部對應的 loader,而後逐個執行
處理完整入口文件以後,獲得依賴的其餘文件,遞歸進行處理。最後獲得了全部文件的 module 。最終輸出的是打包完成的 bundle 文件。因此會有
step4 module 合併成 chunk 中 輸出最終文件
根據 webpack 的使用和結果,咱們猜想了一下 webpack 中大概的流程。而後看一下 webpack 的源碼,並和咱們腦中的流程對比一下。實際的 webpack 流程圖以下:
對總體框架和流程有了大體的概念以後,咱們能夠將源碼拆分爲一部分一部分來詳細閱讀。後續會經過一系列文章一一介紹:
底層 Tapable 介紹
webpack 的底層使用的 Tapable 用來處理各類類型的 hook,這部分主要介紹 Tapable 原理,已更新,點擊查看
reslove 過程
webpack 中咱們所寫的各類相對路徑/絕對路徑,alias 等是如何被處理,最終找到正確的執行文件的。已更新,點擊查看
loaders 處理
寫在 webpack 配置中,各類 loaders 如何被加載、解析;已更新 webpack loader詳解1 webpack loader詳解2 webpack loader詳解3
module 生成
js文件如何被解析,分析出依賴,同時遞歸處理全部的依賴;已更新 module 生成1 module 生成2
chunk 生成
項目中各個文件之間的依賴圖的生成,以及根據定義的規則,module 最終如何聚合爲 chunk。已更新,點擊查看
最終文件的生成
經歷了上面的全部過程後,內存中保存了生成文件的各類信息,這些信息如何整合吐出最終真正執行的全部文件。已更新,點擊查看