我們為什麼要閱讀webpack原始碼

百寶門園地發表於2023-03-16

相信很多人都有這個疑問,為什麼要閱讀原始碼,僅僅只是一個打包工具,會用不就行了,一些配置項在官網,或者谷歌查一查不就好了嗎,誠然在大部分的時候是這樣的,但這樣在深入時也會遇到以下幾種問題。

  1. webpack 配置繁瑣,具有 100 多個內建外掛,200 多個鉤子函式,在保持靈活配置的同時,也把問題拋給了開發者。如不同的配置項會不會對同一個功能產生影響,引用 Plugin 的先後順序會不會影響打包結果?這些問題,不看原始碼是無法真正清晰的。

  2. plugin 也就是外掛,是 webpack 的支柱功能。開發者可以自己使用鉤子函式寫出外掛,來豐富 webpack 的生態,也可以在自己或公司的專案中引用自己開發的外掛,來去解決實際的工程問題,不去探究原始碼,無法理解 webpack 外掛的執行,也無法寫出高質量的外掛。

  3. 從前端整體來看,現代前端的生態與打包工具高度相關,webpack 作為其中的佼佼者,瞭解原始碼,也就是在瞭解前端的生態圈。

Tapable淺析

首先我們要先明白什麼是 Tapable,這個小型庫是 webpack 的一個核心工具。在 webpack 的編譯過程中,本質上透過 Tapable 實現了在編譯過程中的一種釋出訂閱者模式的外掛機制。它提供了一系列事件的釋出訂閱 API ,透過 Tapable 可以註冊事件,從而在不同時機去觸發註冊的事件進行執行。

下面將會有一個模擬 webpack 註冊外掛的例子來嘗試幫助理解。

compiler.js

const { SyncHook, AsyncParallelHook }  = require('tapable');

class Compiler {
  constructor(options) {
    this.hooks = {
      testSyncHook: new SyncHook(['name', 'age']),
      testAsyncHook: new AsyncParallelHook(['name', 'age'])
    }

    let plugins = options.plugins;

    plugins.forEach(plugin => {
      plugin.apply(this);
    });
  }

  run() {
    this.testSyncHook('ggg', 25);
    this.testAsyncHook('hhh', 24);
  }

  testSyncHook(name, age) {
    this.hooks.testSyncHook.call(name, age);
  }

  testAsyncHook(name, age) {
    this.hooks.testAsyncHook.callAsync(name, age);
  }
}

module.exports = Compiler;

index.js

const Compiler = require('./complier');
const MockWebpackPlugin = require('./mock-webpack-plugin');

const complier = new Compiler({
  plugins: [
    new MockWebpackPlugin(),
  ]
});

complier.run();

mock-webpack-plugin.js

class MockWebpackPlugin {

  apply(compiler) {

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
      console.log('同步事件', name, age);
    })

    compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
      setTimeout(() => {
        console.log('非同步事件', name, age)
      }, 3000)
    })
  }
}

module.exports = MockWebpackPlugin;

我相信有些小夥伴看到上述程式碼,就已經明白了大概的邏輯,我們只需要抓住釋出訂閱這兩個詞,在程式碼中呈現的就是 tap 和 call,如果是非同步鉤子,使用 tapAsync, tapPromise 註冊(釋出),就要用 callAsync, promise(注意這裡的 promise 是 Tapable 鉤子例項方法,不要跟 Promise API 搞混) 觸發(訂閱)。

釋出

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
      console.log('同步事件', name, age);
    })

    compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
      setTimeout(() => {
        console.log('非同步事件', name, age)
      }, 3000)
    })

這裡可以看到使用 tab 和 tabAsync 進行註冊,在什麼時機註冊的呢,在 Compiler 類的初始化時期,也就是在透過 new 命令生成物件例項的時候,下面的程式碼已經在 constructor 中被呼叫並執行了,當然這個時候並沒有像函式一樣被呼叫,列印出來姓名和年齡,這時我們只需要先知道,它們已經被註冊了。

訂閱

  run() {
    this.testSyncHook('ggg', 25);
    this.testAsyncHook('hhh', 24);
  }

  testSyncHook(name, age) {
    this.hooks.testSyncHook.call(name, age);
  }

  testAsyncHook(name, age) {
    this.hooks.testAsyncHook.callAsync(name, age);
  }

透過 compiler.run() 命令將會執行下面兩個函式,使用 call 和 callAsync 訂閱。這個時候就會執行 console.log 來列印姓名和年齡了,所以說此時我們就能明白 webpack 中 compiler 和 compilation 中的鉤子函式是以觸發的時期進行區分,歸根結底,是註冊的鉤子在 webpack 不同的編譯時期被觸發。

注意事項

這裡要注意在初始化 Tapable Hook 的同時,要加上引數,傳入引數的數量需要與例項化時傳遞給鉤子類建構函式的陣列長度保持一致。

    this.hooks = {
      testSyncHook: new SyncHook(['name', 'age']),
      testAsyncHook: new AsyncParallelHook(['name', 'age'])
    }

這裡並非要嚴格的傳入 ['name', 'age'],你也可以取其它的名字,如 ['fff', 'ggg],但是為了語義化,還是要進行規範,如下方程式碼,擷取自原始碼中的 lib/Compiler.js 片段,它們在初始化中也是嚴格按照了這個規範。

    /** @type {AsyncSeriesHook<[Compiler]>} */
    beforeRun: new AsyncSeriesHook(["compiler"]),
    /** @type {AsyncSeriesHook<[Compiler]>} */
    run: new AsyncSeriesHook(["compiler"]),
    /** @type {AsyncSeriesHook<[Compilation]>} */
    emit: new AsyncSeriesHook(["compilation"]),

更具體的可以檢視這篇文章 走進 Tapable - 掘金 (juejin.cn)

如何除錯

想除錯 webpack 原始碼,一般有兩種方式,一種是 clone 除錯,一種是 npm 包除錯,筆者這裡選擇透過 clone 除錯,執行 webpack 也有兩種方式,一是透過 webpack-cli 輸入命令啟動,另外一種如下,引入 webapck,使用 webpack.run() 啟動。

準備工作

首先可以用 https 從 github 上克隆 webpack 原始碼。

    git clone https://github.com/webpack/webpack
    npm install

之後可以在根目錄建立一個名為 source 的資料夾,source 資料夾目錄如下

-- webpack
    -- source 
        -- src 
             -- foo.js
             -- main.js
        -- index.html
        -- index.js
        -- webpack.config.js 

index.js

const webpack = require('../lib/index.js');
const config = require('./webpack.config.js');

const complier = webpack(config);
complier.run((err, stats) => {
  if (err) {
    console.error(err);
  } else {
    console.log(stats);
  }
})

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/main.js',
  output: {
      path: path.join(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: /node_modules/,
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Test Webpack',
      template: './index.html',
      filename: 'template.html'
    })
  ]
}

引用 html-webpack-plugin 和 babel-loader 主要是想更清晰看到在構建過程中 webpack 會如何處理引入的 plugin 和 loader。

main.js

import foo from './foo.js';
import { isEmpty } from 'lodash';

foo();

const obj = {};
console.log(isEmpty(obj));
console.log('main.js');

foo.js

export default function foo() {
  console.log('foo');
}

檔案建立好了,這裡使用 Vscode 進行除錯, 開啟 JavaScript 除錯終端。

image.png

原始碼閱讀

按照下面命令,啟動 webpack

    cd source
    node index.js

這裡為了更加清晰, 可以打上一個斷點。如在 lib/webpack.js 中,將斷點打在 158 行,檢視是如何生成的 compiler 例項。

image.png

這裡需要點選單步除錯,這樣才能進入 create 函式中,一步步除錯可以看到,首先會對傳入的 options 進行校驗, 如果不符合規範,將會丟擲錯誤,由於這裡的 options 是一個物件,將會進入到 createCompiler 函式內。

image.png

在這個函式內將會創造 Compiler 例項,以及註冊引入的外掛和內建外掛。

image.png

筆者將會一步步的講解這個函式都做了什麼事,如

applyWebpackOptionsBaseDefaults:給沒設定的基本配置加上預設值。

new Compiler:生成 compiler 例項,初始化一些鉤子和引數。

NodeEnvironmentPlugin:主要是對檔案模組進行了封裝和最佳化,感興趣的讀者可以打斷點,詳細去檢視。

接下來要做的事情就是註冊鉤子,如上文中引入了 html-webpack-plugin, 這裡將會呼叫 HtmlWebpackplugin 例項的 apply 函式,這樣就能明白為什麼以 class 類的方式,寫外掛,為什麼裡面一定要加上 apply。緊接著建立完 compiler 例項後,正如官網上描述的,關於 compiler.hooks.environment 的訂閱時期,在編譯器準備環境時呼叫,時機就在配置檔案中初始化外掛之後。我們就能知其然,也能知所以然了。

image.png

再往下,

new WebpackOptionsApply().process(options, compiler):註冊了內部外掛,如 DllPlugin, HotModuleReplacementPlugin 等。

小技巧分享

這裡簡單分享了筆者看原始碼的步驟,然後還有兩個技巧分享。

一是由於 webpack 運用了大量回撥函式,一步步打斷點是很難看的清楚的,可直接在 Vscode 中全域性搜尋 compiler.hooks.xxx 和 compilation.hooks.xxx, 去看 tap 中回撥函式的執行。

二是可在 Vscode 除錯中的 watch 模組,新增上 compiler 和 compilation,這樣也是更方便觀察回撥函式的執行。如

image.png

總結

webpack 中的細節很是繁多,裡面有大量的異常處理,在看的時候要有重點的看,有選擇的看,如果你要看 make 階段所做的事情, 可以重點去看如何生成模組,模組分為幾種,如何遞迴處理依賴,如何使用 loader 解析檔案等。筆者認為看原始碼還有一個好處,那就是讓你對這些知名開源庫沒有畏懼心理,它們也是用 js 一行行寫的,裡面會有一些程式碼片段,可能寫的也沒有那麼優美,我們在閱讀程式碼的同時,說不定也能成為程式碼貢獻者,能夠在簡歷上留下濃墨重彩的一筆。

作者:百寶門-前端組-閆磊剛

原文地址:https://blog.baibaomen.com/我們為什麼要閱讀webpack原始碼/

相關文章