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 生成算法

  1. webpack 先將 entry 中對應的 module 都生成一個新的 chunk
  2. 遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中
  3. 如果一個依賴 module 是動態引入的模塊,那么就會根據這個 module 創建一個 新的 chunk,繼續遍歷依賴
  4. 重復上面的過程,直至得到所有的 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 文件可以在瀏覽器中運行


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM