Webpack原理與實踐之Webpack運行機制與核心工作原理
寫在前面
Webpack在整個打包過程中:
通過loader處理特殊類型資源的加載,例如加載樣式、圖片
通過plugin實現各種自動化的構建任務,例如自動壓縮、自動發布
那么webpack的工作過程和原理又是如何實現的呢?
Webpack的工作過程
首先webpack會加載入口文件js,通過分析代碼中import、require等去解析依賴,然后通過依賴形成依賴關系樹,webpack會去遍歷依賴關系樹,去加載所依賴的資源模塊。webpack會通過Loader配置去加載模塊,通過plugins實現自動化構建。
對于依賴模塊中無法通過js代碼表示的資源模塊,例如圖片或字體文件,一般的Loader會將它們單獨作為資源文件拷貝到輸出目錄中,然后將這個資源文件所對應的訪問路徑作為這個模塊的導出成員暴露給外部。
webpack在每個打包環節都預留了鉤子,我們可以通過plugins去配置其所依賴的插件。
具體的:
- Webpack cli啟動打包流程
- 載入Webpack核心模塊,創建Compiler對象
- 使用創建Compiler對象開始編譯整個項目
- 從入口文件開始,解析模塊依賴,形成依賴關系樹
- 遞歸遍歷依賴樹,將每個模塊交給對應的loader處理
- 合并loader處理完的結果,將打包結果輸出到dist目錄
Webpack cli的作用是將cli參數和webpack配置文件中的配置進行整合得到一個完整的配置對象。Webpack cli會通過yargs模塊解析cli參數,運行webpack命令時通過命令行傳入的參數。
- const config = { options: {}, path: new WeakMap() };
- // 判斷是否指定了配置文件
- if (options.config && options.config.length > 0) {
- const loadedConfigs = await Promise.all(
- options.config.map((configPath) =>
- loadConfigByPath(path.resolve(configPath), options.argv),
- ),
- );
- config.options = [];
- loadedConfigs.forEach((loadedConfig) => {
- const isArray = Array.isArray(loadedConfig.options);
- // TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release
- if (config.options.length === 0) {
- config.options = loadedConfig.options;
- } else {
- if (!Array.isArray(config.options)) {
- config.options = [config.options];
- }
- if (isArray) {
- loadedConfig.options.forEach((item) => {
- config.options.push(item);
- });
- } else {
- config.options.push(loadedConfig.options);
- }
- }
- if (isArray) {
- loadedConfig.options.forEach((options) => {
- config.path.set(options, loadedConfig.path);
- });
- } else {
- config.path.set(loadedConfig.options, loadedConfig.path);
- }
- });
- config.options = config.options.length === 1 ? config.options[0] : config.options;
- } else {
- // 按照配置文件規則找到加載配置文件
- // Order defines the priority, in decreasing order
- const defaultConfigFiles = [
- "webpack.config",
- ".webpack/webpack.config",
- ".webpack/webpackfile",
- ]
- .map((filename) =>
- // Since .cjs is not available on interpret side add it manually to default config extension list
- [...Object.keys(interpret.extensions), ".cjs"].map((ext) => ({
- path: path.resolve(filename + ext),
- ext: ext,
- module: interpret.extensions[ext],
- })),
- )
- .reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
- let foundDefaultConfigFile;
- for (const defaultConfigFile of defaultConfigFiles) {
- if (!fs.existsSync(defaultConfigFile.path)) {
- continue;
- }
- foundDefaultConfigFile = defaultConfigFile;
- break;
- }
- if (foundDefaultConfigFile) {
- const loadedConfig = await loadConfigByPath(foundDefaultConfigFile.path, options.argv);
- config.options = loadedConfig.options;
- if (Array.isArray(config.options)) {
- config.options.forEach((item) => {
- config.path.set(item, loadedConfig.path);
- });
- } else {
- config.path.set(loadedConfig.options, loadedConfig.path);
- }
- }
- }
開始載入webpack核心模塊,傳入配置選項,創建Compiler對象。
- // 創建Compiler對象的函數
- async createCompiler(options, callback) {
- if (typeof options.nodeEnv === "string") {
- process.env.NODE_ENV = options.nodeEnv;
- }
- let config = await this.loadConfig(options);
- config = await this.buildConfig(config, options);
- let compiler;
- try {
- // 開始調用webpack核心模塊
- compiler = this.webpack(
- config.options,
- callback
- ? (error, stats) => {
- if (error && this.isValidationError(error)) {
- this.logger.error(error.message);
- process.exit(2);
- }
- callback(error, stats);
- }
- : callback,
- );
- } catch (error) {
- if (this.isValidationError(error)) {
- this.logger.error(error.message);
- } else {
- this.logger.error(error);
- }
- process.exit(2);
- }
- // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
- if (compiler && compiler.compiler) {
- compiler = compiler.compiler;
- }
- return compiler;
- }
make階段
make階段主體的目標是:根據entry配置找到入口模塊,開始依次遞歸出所有依賴,形成依賴關系樹,然后遞歸到的每個模塊交給不同的loader處理。
- // 多路打包
- if (Array.isArray(options)) {
- await Promise.all(
- options.map(async (_, i) => {
- if (typeof options[i].then === "function") {
- options[i] = await options[i];
- }
- // `Promise` may return `Function`
- if (typeof options[i] === "function") {
- // when config is a function, pass the env from args to the config function
- options[i] = await options[i](argv.env, argv);
- }
- }),
- );
- } else {
- // 單線打包
- if (typeof options.then === "function") {
- options = await options;
- }
- // `Promise` may return `Function`
- if (typeof options === "function") {
- // when config is a function, pass the env from args to the config function
- options = await options(argv.env, argv);
- }
- }
默認使用的就是單一入口打包的方式,所以這里最終會執行其中的SingleEntryPlugin。
- SingleEntryPlugin中調用了Compilation對象的addEntry方法,開始解析入口。
- addEntry方法中又調用了_addModuleChain方法,將入口模塊添加到模塊依賴列表。
- 然后通過Compilation對象的buildModule方法進行模塊構建
- buildModule方法中執行具體的Loader,處理特殊資源加載
- build完成后,通過acorn庫生成模塊代碼的AST語法樹
- 根據語法樹分析這個模塊是否還有依賴的模塊,如果有則繼續循環build每個依賴
- 所有依賴解析完成,build階段結束
- 最后合并生成需要輸出的bundle.js寫入目錄
參考文章
《webpack原理與實踐》
《webpack中文文檔》
寫在最后
本文主要說明了webpack的工作過程和原理是如何實現的,并且對部分源碼進行了分析,源碼相當于牛津詞典,你不可能專門單獨設定時間去閱讀,而應該是需要什么查閱什么,帶著目的性去學習。