webpack 打包原理
原理學習
這個過程發生了什麼?
開始:從 webpack 命令列說起
通過 npm scripts 執行 webpack
- 開發環境: npm run dev
- 生產環境:npm run build
通過 webpack 直接執行
webpack entry.js bundle.js
查詢 webpack 入口檔案
在命令列執行以上命令後,npm會讓命令列工具進入node_modules.bin 目錄 查詢是否存在 webpack.sh 或者 webpack.cmd 檔案,如果存在,就執行,不 存在,就丟擲錯誤。
實際的入口檔案是:node_modules\webpack\bin\webpack.js
分析 webpack 的入口檔案:webpack.js
process.exitCode = 0; //1. 正常執行返回 const runCommand = (command, args) =>{...}; //2. 執行某個命令 const isInstalled = packageName =>{...}; //3. 判斷某個包是否安裝 const CLIs =[...]; //4. webpack 可用的 CLI: webpack-cli webpack-command const installedClis = CLIs.filter(cli => cli.installed); //5. 判斷是否兩個 ClI 是否安裝了 if (installedClis.length === 0){...}else if //6. 根據安裝數量進行處理 (installedClis.length === 1){...}else{...}.
啟動後的結果
webpack 最終找到 webpack-cli (webpack-command) 這個 npm 包,並且 執行 CLI
webpack-cli 做的事情
引入 yargs,對命令列進行定製
分析命令列引數,對各個引數進行轉換,組成編譯配置項
引用webpack,根據配置項進行編譯和構建
從NON_COMPILATION_CMD分析出不需要編譯的命令
webpack-cli 處理不需要經過編譯的命令
const { NON_COMPILATION_ARGS } = require("./utils/constants"); const NON_COMPILATION_CMD = process.argv.find(arg => { if (arg === "serve") { global.process.argv = global.process.argv.filter(a => a !== "serve"); process.argv = global.process.argv; } return NON_COMPILATION_ARGS.find(a => a === arg); }); if (NON_COMPILATION_CMD) { return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv); }
NON_COMPILATION_ARGS的內容
webpack-cli 提供的不需要編譯的命令
const NON_COMPILATION_ARGS = [
"init", //建立一份 webpack 配置檔案
"migrate", // 進行 webpack 版本遷移
"add", // 往 webpack 配置檔案中增加屬性
"remove", // 往 webpack 配置檔案中刪除屬性
"serve", // 執行 webpack-serve
"generate-loader", // 生成 webpack loader 程式碼
"generate-plugin", // 生成 webpack plugin 程式碼
"info” //返回與本地環境相關的一些資訊
];
命令列工具包 yargs 介紹
提供命令和分組引數
動態生成 help 幫助資訊
webpack-cli 使用 args 分析
引數分組 (config/config-args.js),將命令劃分為9類:
·Config options: 配置相關引數(檔名稱、執行環境等)
·Basic options: 基礎引數(entry設定、debug模式設定、watch監聽設定、devtool設定)
·Module options: 模組引數,給 loader 設定擴充套件 ·Output options: 輸出引數(輸出路徑、輸出檔名稱)
·Advanced options: 高階用法(記錄設定、快取設定、監聽頻率、bail等) ·Resolving options: 解析引數(alias 和 解析的檔案字尾設定)
·Optimizing options: 優化引數
·Stats options: 統計引數
·options: 通用引數(幫助命令、版本資訊等)
webpack-cli 執行的結果
webpack-cli對配置檔案和命令列引數進行轉換最終生成配置選項引數 options
最終會根據配置引數例項化 webpack 物件,然後執行構建流程
Webpack 的本質
Webpack可以將其理解是一種基於事件流的程式設計範例,一系列的外掛執行。
先看一段程式碼
核心物件 Compiler 繼承 Tapable
class Compiler extends Tapable {
// ...
}
核心物件 Compilation 繼承 Tapable
class Compilation extends Tapable {
// ...
}
Tapable 是什麼?
Tapable 是一個類似於 Node.js 的 EventEmitter 的庫, 主要是控制鉤子函式的釋出 與訂閱,控制著 webpack 的外掛系統。
Tapable庫暴露了很多 Hook(鉤子)類,為外掛提供掛載的鉤子
const {
SyncHook, //同步鉤子
SyncBailHook, //同步熔斷鉤子
SyncWaterfallHook, //同步流水鉤子
SyncLoopHook, //同步迴圈鉤子
AsyncParallelHook, //非同步併發鉤子
AsyncParallelBailHook, //非同步併發熔斷鉤子
AsyncSeriesHook, //非同步序列鉤子
AsyncSeriesBailHook, //非同步序列熔斷鉤子
AsyncSeriesWaterfallHook //非同步序列流水鉤子
} = require('tapable');
Tapable hooks 型別
type | function |
---|---|
Hook | 所有鉤子的字尾 |
Waterfall | 同步方法 |
Bail | 熔斷:當函式有任何返回值,就會在當前執行函式停止 |
Loop | 監聽函式返回 true 表示繼續迴圈,返回 undefined 表示結束迴圈 |
Sync | 同步方法 |
AsyncSeries | 非同步序列鉤子 |
AsyncParallel | 非同步並行鉤子 |
Tapable 的使用 -new Hook 新建鉤子
Tapable 暴露出來的都是類方法,new 一個類方法獲得我們需要的鉤子
class 接受陣列引數 options ,非必傳。類方法會根據傳參,接受同樣數量的引數。
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
Tapable 的使用-鉤子的繫結與執行
Tabpack 提供了同步&非同步繫結鉤子的方法,並且他們都有繫結事件和執行事件對 應的方法。
Async* | Sync* |
---|---|
繫結:tapAsync/tapPromise/tap | 繫結:tap |
執行:callAsync/promise | 執行:call |
Tapable 的使用-hook 基本用法示例
const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);
//繫結事件到webapck事件流
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3
//執行繫結的事件
hook1.call(1,2,3)
Tapable 的使用-實際例子演示
定義一個 Car 方法,在內部 hooks 上新建鉤子。分別是同步鉤子 accelerate、 brake( accelerate 接受一個引數)、非同步鉤子 calculateRoutes
使用鉤子對應的繫結和執行方法
calculateRoutes 使用 tapPromise 可以返回一個 promise 物件
Tapable 是如何和 webpack 聯絡起來的?
if (Array.isArray(options)) {
compiler = new MultiCompiler(options.map((options) => webpack(options)));
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
} else {
plugin.call(compiler, compiler);
}
}
}
plugin.apply(compiler);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
}
模擬 Compiler.js
module.exports = class Compiler {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newspeed']),
brake: new SyncHook(),
}
}
calculateRoutes: new AsyncSeriesHook(["source", "target", "routesList"])
run() {
this.accelerate(10) this.break()
}
this.calculateRoutes('Async', 'hook', 'demo')
accelerate(speed) {}
this.hooks.accelerate.call(speed);
break () {}
this.hooks.brake.call();
calculateRoutes() {
this.hooks.calculateRoutes.promise(...arguments).then(() => {}, err => {
console.error(err);
})
}
};
外掛 my-plugin.js
const Compiler = require("./Compiler");
class MyPlugin {
constructor() {}
apply(compiler) {
compiler.hooks.brake.tap("WarningLampPlugin", () =>
console.log("WarningLampPlugin")
);
compiler.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log(`Accelerating to${newSpeed}`)
);
compiler.hooks.calculateRoutes.tapPromise(
"calculateRoutes tapAsync",
(source, target, routesList) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`tapPromise to ${source} ${target} ${routesList}`);
resolve();
}, 1000);
});
}
);
}
}
模擬外掛執行
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin],
};
const compiler = new Compiler();
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {}
}
plugin.apply(compiler);
compiler.run();
Webpack 流程篇
webpack的編譯都按照下面的鉤子呼叫順序執行
WebpackOptionsApply
將所有的配置 options 引數轉換成 webpack 內部外掛
使用預設外掛列表
舉例:
·output.library -> LibraryTemplatePlugin
·externals -> ExternalsPlugin
·devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
·AMDPlugin, CommonJsPlugin ·RemoveEmptyChunksPlugin
Compiler hooks
流程相關:
·(before-)run ·(before-/after-)compile ·make
·(after-)emit
·done
監聽相關:
·watch-run
·watch-close
Compilation
Compiler 呼叫 Compilation 生命週期方法
·addEntry -> addModuleChain
·finish (上報模組錯誤)
·seal
ModuleFactory
Module
NormalModule
Build
·使用 loader-runner 執行 loaders
·通過 Parser 解析 (內部是 acron)
·ParserPlugins 新增依賴
Compilation hooks
模組相關:
·build-module
·failed-module
·succeed-module
資源生成相關:
·module-asset
·chunk-asset
優化和 seal相關:
·(after-)seal
·optimize
·optimize-modules(-basic/advanced)
·after-optimize-modules
·after-optimize-chunks
·after-optimize-tree
·optimize-chunk-modules (-basic/advanced)
·after-optimize-chunk-modules
·optimize-module/chunk-order
·before-module/chunk-ids
·(after-)optimize-module/ chunk-ids
·before/after-hash
Chunk 生成演算法
- webpack 先將 entry 中對應的 module 都生成一個新的 chunk
- 遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中
- 如果一個依賴 module 是動態引入的模組,那麼就會根據這個 module 建立一個 新的 chunk,繼續遍歷依賴
- 重複上面的過程,直至得到所有的 chunks
模組化:增強程式碼可讀性和維護性
傳統的網頁開發轉變成 Web Apps 開發
程式碼複雜度在逐步增高
分離的 JS檔案/模組,便於後續程式碼的維護性
部署時希望把程式碼優化成幾個 HTTP 請求
常見的幾種模組化方式
- ES module
import * as largeNumber from 'large-number';
// ...
largeNumber.add('999', '1');
}
- CJS
const largeNumbers = require('large-number');
// ...
largeNumber.add('999', '1');
}
- AMD
require(['large-number'], function (large-number) {
// ...
largeNumber.add('999', '1');
});
AMD
AST 基礎知識
抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是 原始碼的抽象語法結構的樹狀表現形式,這裡特指程式語言的原始碼。樹上的每個節點都 表示原始碼中的一種結構。
線上demo: https://esprima.org/demo/parse.html
複習一下 webpack 的模組機制
動手實現一個簡易的 webpack
可以將 ES6 語法轉換成 ES5 的語法
- 通過 babylon 生成AST
- 通過 babel-core 將AST重新生成原始碼
可以分析模組之間的依賴關係 - 通過 babel-traverse 的 ImportDeclaration 方法獲取依賴屬性
生成的 JS 檔案可以在瀏覽器中執行