揭祕webpack外掛的工作原理

frank發表於2020-06-24

webpack系列1:常見 loader 原始碼簡析,以及動手實現一個 md2html-loader
webpack系列2:揭祕webpack 外掛工作原理
webpack系列3:webpack 主流程原始碼閱讀以及實現一個 webpack

前言

通過外掛我們可以擴充套件webpack,在合適的時機通過Webpack提供的 API 改變輸出結果,使webpack可以執行更廣泛的任務,擁有更強的構建能力。
本文將嘗試探索 webpack 外掛的工作流程,進而去揭祕它的工作原理。同時需要你對webpack底層和構建流程的一些東西有一定的瞭解。

想要了解 webpack 的外掛的機制,需要弄明白以下幾個知識點:

  1. 一個簡單的外掛的構成
  2. webpack構建流程
  3. Tapable是如何把各個外掛串聯到一起的
  4. compiler以及compilation物件的使用以及它們對應的事件鉤子。

外掛基本結構

plugins是可以用自身原型方法apply來例項化的物件。apply只在安裝外掛被Webpack compiler執行一次。apply方法傳入一個webpck compiler的引用,來訪問編譯器回撥。

一個簡單的外掛結構:

class HelloPlugin {
  // 在建構函式中獲取使用者給該外掛傳入的配置
  constructor(options) {}
  // Webpack 會呼叫 HelloPlugin 例項的 apply 方法給外掛例項傳入 compiler 物件
  apply(compiler) {
    // 在emit階段插入鉤子函式,用於特定時機處理額外的邏輯;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成後可以呼叫 webpack 提供的回撥函式;
    })
    // 如果事件是非同步的,會帶兩個引數,第二個引數為回撥函式,在外掛處理完任務時需要呼叫回撥函式通知webpack,才會進入下一個處理流程。
    compiler.plugin('emit', function (compilation, callback) {
      // 支援處理邏輯
      // 處理完畢後執行 callback 以通知 Webpack
      // 如果不執行 callback,執行流程將會一直卡在這不往下執行
      callback()
    })
  }
}

module.exports = HelloPlugin

安裝外掛時, 只需要將它的一個例項放到Webpack config plugins 陣列裡面:

const HelloPlugin = require('./hello-plugin.js')
var webpackConfig = {
  plugins: [new HelloPlugin({ options: true })],
}

先來分析一下 webpack Plugin 的工作原理

  1. 讀取配置的過程中會先執行 new HelloPlugin(options) 初始化一個 HelloPlugin 獲得其例項。
  2. 初始化 compiler 物件後呼叫 HelloPlugin.apply(compiler) 給外掛例項傳入 compiler 物件。
  3. 外掛例項在獲取到 compiler 物件後,就可以通過compiler.plugin(事件名稱, 回撥函式) 監聽到 Webpack 廣播出來的事件。
    並且可以通過 compiler 物件去操作 Webpack

webapck 構建流程

在編寫外掛之前,還需要了解一下Webpack的構建流程,以便在合適的時機插入合適的外掛邏輯。

Webpack 的基本構建流程如下:

  1. 校驗配置檔案 :讀取命令列傳入或者webpack.config.js檔案,初始化本次構建的配置引數
  2. 生成Compiler物件:執行配置檔案中的外掛例項化語句new MyWebpackPlugin(),為webpack事件流掛上自定義hooks
  3. 進入entryOption階段:webpack開始讀取配置的Entries,遞迴遍歷所有的入口檔案
  4. run/watch:如果執行在watch模式則執行watch方法,否則執行run方法
  5. compilation:建立Compilation物件回撥compilation相關鉤子,依次進入每一個入口檔案(entry),使用 loader 對檔案進行編譯。通過compilation我可以可以讀取到moduleresource(資源路徑)、loaders(使用的 loader)等資訊。再將編譯好的檔案內容使用acorn解析生成 AST 靜態語法樹。然後遞迴、重複的執行這個過程,
    所有模組和和依賴分析完成後,執行 compilationseal 方法對每個 chunk 進行整理、優化、封裝__webpack_require__來模擬模組化操作.
  6. emit:所有檔案的編譯及轉化都已經完成,包含了最終輸出的資源,我們可以在傳入事件回撥的compilation.assets上拿到所需資料,其中包括即將輸出的資源、程式碼塊 Chunk 等等資訊。
// 修改或新增資源
compilation.assets['new-file.js'] = {
  source() {
    return 'var a=1'
  },
  size() {
    return this.source().length
  },
}
  1. afterEmit:檔案已經寫入磁碟完成
  2. done:完成編譯
    奉上一張滴滴雲部落格的 WebPack 編譯流程圖,不喜歡看文字講解的可以看流程圖理解記憶
    WebPack 編譯流程圖

原圖出自:https://blog.didiyun.com/inde...

看完之後,如果還是看不懂或者對縷不清 webpack 構建流程的話,建議通讀一下全文,再回來看這段話,相信一定會對 webpack 構建流程有很更加深刻的理解。

理解事件流機制 tapable

webpack本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是 Tapable。

WebpackTapable 事件流機制保證了外掛的有序性,將各個外掛串聯起來, Webpack 在執行過程中會廣播事件,外掛只需要監聽它所關心的事件,就能加入到這條 webapck 機制中,去改變 webapck 的運作,使得整個系統擴充套件性良好。

Tapable也是一個小型的 library,是Webpack的一個核心工具。類似於node中的events庫,核心原理就是一個訂閱釋出模式。作用是提供類似的外掛介面。

webpack 中最核心的負責編譯的Compiler和負責建立 bundles 的Compilation都是 Tapable 的例項,可以直接在 CompilerCompilation 物件上廣播和監聽事件,方法如下:

/**
 * 廣播事件
 * event-name 為事件名稱,注意不要和現有的事件重名
 */
compiler.apply('event-name', params)
compilation.apply('event-name', params)
/**
 * 監聽事件
 */
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})

Tapable類暴露了taptapAsynctapPromise方法,可以根據鉤子的同步/非同步方式來選擇一個函式注入邏輯。

tap 同步鉤子

compiler.hooks.compile.tap('MyPlugin', (params) => {
  console.log('以同步方式觸及 compile 鉤子。')
})

tapAsync 非同步鉤子,通過callback回撥告訴Webpack非同步執行完畢
tapPromise 非同步鉤子,返回一個Promise告訴Webpack非同步執行完畢

compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
  console.log('以非同步方式觸及 run 鉤子。')
  callback()
})

compiler.hooks.run.tapPromise('MyPlugin', (compiler) => {
  return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
    console.log('以具有延遲的非同步方式觸及 run 鉤子')
  })
})

Tabable 用法

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
} = require('tapable')

tapable

簡單實現一個 SyncHook

class Hook {
  constructor(args) {
    this.taps = []
    this.interceptors = [] // 這個放在後面用
    this._args = args
  }
  tap(name, fn) {
    this.taps.push({ name, fn })
  }
}
class SyncHook extends Hook {
  call(name, fn) {
    try {
      this.taps.forEach((tap) => tap.fn(name))
      fn(null, name)
    } catch (error) {
      fn(error)
    }
  }
}

tapable是如何將webapck/webpack外掛關聯的?

Compiler.js

const { AsyncSeriesHook ,SyncHook } = require("tapable");
//建立類
class Compiler {
    constructor() {
        this.hooks = {
           run: new AsyncSeriesHook(["compiler"]), //非同步鉤子
           compile: new SyncHook(["params"]),//同步鉤子
        };
    },
    run(){
      //執行非同步鉤子
      this.hooks.run.callAsync(this, err => {
         this.compile(onCompiled);
      });
    },
    compile(){
      //執行同步鉤子 並傳參
      this.hooks.compile.call(params);
    }
}
module.exports = Compiler

MyPlugin.js

const Compiler = require('./Compiler')

class MyPlugin {
  apply(compiler) {
    //接受 compiler引數
    compiler.hooks.run.tap('MyPlugin', () => console.log('開始編譯...'))
    compiler.hooks.complier.tapAsync('MyPlugin', (name, age) => {
      setTimeout(() => {
        console.log('編譯中...')
      }, 1000)
    })
  }
}

//這裡類似於webpack.config.js的plugins配置
//向 plugins 屬性傳入 new 例項

const myPlugin = new MyPlugin()

const options = {
  plugins: [myPlugin],
}
let compiler = new Compiler(options)
compiler.run()

想要深入瞭解tapable的文章可以看看這篇文章:

webpack4核心模組tapable原始碼解析:
https://www.cnblogs.com/tugen...

理解 Compiler(負責編譯)

開發外掛首先要知道compilercompilation 物件是做什麼的

Compiler 物件包含了當前執行Webpack的配置,包括entry、output、loaders等配置,這個物件在啟動Webpack時被例項化,而且是全域性唯一的。Plugin可以通過該物件獲取到 Webpack 的配置資訊進行處理。

如果看完這段話,你還是沒理解compiler是做啥的,不要怕,接著看。
執行npm run build,把compiler的全部資訊輸出到控制檯上console.log(Compiler)

compiler

// 為了能更直觀的讓大家看清楚compiler的結構,裡面的大量程式碼使用省略號(...)代替。
Compiler {
  _pluginCompat: SyncBailHook {
    ...
  },
  hooks: {
    shouldEmit: SyncBailHook {
     ...
    },
    done: AsyncSeriesHook {
     ...
    },
    additionalPass: AsyncSeriesHook {
     ...
    },
    beforeRun: AsyncSeriesHook {
     ...
    },
    run: AsyncSeriesHook {
     ...
    },
    emit: AsyncSeriesHook {
     ...
    },
    assetEmitted: AsyncSeriesHook {
     ...
    },
    afterEmit: AsyncSeriesHook {
     ...
    },
    thisCompilation: SyncHook {
     ...
    },
    compilation: SyncHook {
     ...
    },
    normalModuleFactory: SyncHook {
     ...
    },
    contextModuleFactory: SyncHook {
     ...
    },
    beforeCompile: AsyncSeriesHook {
      ...
    },
    compile: SyncHook {
     ...
    },
    make: AsyncParallelHook {
     ...
    },
    afterCompile: AsyncSeriesHook {
     ...
    },
    watchRun: AsyncSeriesHook {
     ...
    },
    failed: SyncHook {
     ...
    },
    invalid: SyncHook {
     ...
    },
    watchClose: SyncHook {
     ...
    },
    infrastructureLog: SyncBailHook {
     ...
    },
    environment: SyncHook {
     ...
    },
    afterEnvironment: SyncHook {
     ...
    },
    afterPlugins: SyncHook {
     ...
    },
    afterResolvers: SyncHook {
     ...
    },
    entryOption: SyncBailHook {
     ...
    },
    infrastructurelog: SyncBailHook {
     ...
    }
  },
  ...
  outputPath: '',//輸出目錄
  outputFileSystem: NodeOutputFileSystem {
   ...
  },
  inputFileSystem: CachedInputFileSystem {
    ...
  },
  ...
  options: {
    //Compiler物件包含了webpack的所有配置資訊,entry、module、output、resolve等資訊
    entry: [
      'babel-polyfill',
      '/Users/frank/Desktop/fe/fe-blog/webpack-plugin/src/index.js'
    ],
    devServer: { port: 3000 },
    output: {
      ...
    },
    module: {
      ...
    },
    plugins: [ MyWebpackPlugin {} ],
    mode: 'production',
    context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',
    devtool: false,
    ...
    performance: {
      maxAssetSize: 250000,
      maxEntrypointSize: 250000,
      hints: 'warning'
    },
    optimization: {
      ...
    },
    resolve: {
      ...
    },
    resolveLoader: {
      ...
    },
    infrastructureLogging: { level: 'info', debug: false }
  },
  context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',//上下文,檔案目錄
  requestShortener: RequestShortener {
    ...
  },
  ...
  watchFileSystem: NodeWatchFileSystem {
    //監聽檔案變化列表資訊
     ...
  }
}

Compiler 原始碼精簡版程式碼解析

原始碼地址(948 行):https://github.com/webpack/we...

const { SyncHook, SyncBailHook, AsyncSeriesHook } = require('tapable')
class Compiler {
  constructor() {
    // 1. 定義生命週期鉤子
    this.hooks = Object.freeze({
      // ...只列舉幾個常用的常見鉤子,更多hook就不列舉了,有興趣看原始碼
      done: new AsyncSeriesHook(['stats']), //一次編譯完成後執行,回撥引數:stats
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']), //在編譯器開始讀取記錄前執行
      emit: new AsyncSeriesHook(['compilation']), //在生成檔案到output目錄之前執行,回撥引數: compilation
      afterEmit: new AsyncSeriesHook(['compilation']), //在生成檔案到output目錄之後執行
      compilation: new SyncHook(['compilation', 'params']), //在一次compilation建立後執行外掛
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']), //在一個新的compilation建立之前執行
      make: new AsyncParallelHook(['compilation']), //完成一次編譯之前執行
      afterCompile: new AsyncSeriesHook(['compilation']),
      watchRun: new AsyncSeriesHook(['compiler']),
      failed: new SyncHook(['error']),
      watchClose: new SyncHook([]),
      afterPlugins: new SyncHook(['compiler']),
      entryOption: new SyncBailHook(['context', 'entry']),
    })
    // ...省略程式碼
  }
  newCompilation() {
    // 建立Compilation物件回撥compilation相關鉤子
    const compilation = new Compilation(this)
    //...一系列操作
    this.hooks.compilation.call(compilation, params) //compilation物件建立完成
    return compilation
  }
  watch() {
    //如果執行在watch模式則執行watch方法,否則執行run方法
    if (this.running) {
      return handler(new ConcurrentCompilationError())
    }
    this.running = true
    this.watchMode = true
    return new Watching(this, watchOptions, handler)
  }
  run(callback) {
    if (this.running) {
      return callback(new ConcurrentCompilationError())
    }
    this.running = true
    process.nextTick(() => {
      this.emitAssets(compilation, (err) => {
        if (err) {
          // 在編譯和輸出的流程中遇到異常時,會觸發 failed 事件
          this.hooks.failed.call(err)
        }
        if (compilation.hooks.needAdditionalPass.call()) {
          // ...
          // done:完成編譯
          this.hooks.done.callAsync(stats, (err) => {
            // 建立compilation物件之前
            this.compile(onCompiled)
          })
        }
        this.emitRecords((err) => {
          this.hooks.done.callAsync(stats, (err) => {})
        })
      })
    })

    this.hooks.beforeRun.callAsync(this, (err) => {
      this.hooks.run.callAsync(this, (err) => {
        this.readRecords((err) => {
          this.compile(onCompiled)
        })
      })
    })
  }
  compile(callback) {
    const params = this.newCompilationParams()
    this.hooks.beforeCompile.callAsync(params, (err) => {
      this.hooks.compile.call(params)
      const compilation = this.newCompilation(params)
      //觸發make事件並呼叫addEntry,找到入口js,進行下一步
      this.hooks.make.callAsync(compilation, (err) => {
        process.nextTick(() => {
          compilation.finish((err) => {
            // 封裝構建結果(seal),逐次對每個module和chunk進行整理,每個chunk對應一個入口檔案
            compilation.seal((err) => {
              this.hooks.afterCompile.callAsync(compilation, (err) => {
                // 非同步的事件需要在外掛處理完任務時呼叫回撥函式通知 Webpack 進入下一個流程,
                // 不然執行流程將會一直卡在這不往下執行
                return callback(null, compilation)
              })
            })
          })
        })
      })
    })
  }
  emitAssets(compilation, callback) {
    const emitFiles = (err) => {
      //...省略一系列程式碼
      // afterEmit:檔案已經寫入磁碟完成
      this.hooks.afterEmit.callAsync(compilation, (err) => {
        if (err) return callback(err)
        return callback()
      })
    }

    // emit 事件發生時,可以讀取到最終輸出的資源、程式碼塊、模組及其依賴,並進行修改(這是最後一次修改最終檔案的機會)
    this.hooks.emit.callAsync(compilation, (err) => {
      if (err) return callback(err)
      outputPath = compilation.getPath(this.outputPath, {})
      mkdirp(this.outputFileSystem, outputPath, emitFiles)
    })
  }
  // ...省略程式碼
}

apply方法中插入鉤子的一般形式如下:

// compiler提供了compiler.hooks,可以根據這些不同的時刻去讓外掛做不同的事情。
compiler.hooks.階段.tap函式('外掛名稱', (階段回撥引數) => {})
compiler.run(callback)

理解 Compilation(負責建立 bundles)

Compilation物件代表了一次資源版本構建。當執行 webpack 開發環境中介軟體時,每當檢測到一個檔案變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 Compilation 物件表現了當前的模組資源、編譯生成資源、變化的檔案、以及被跟蹤依賴的狀態資訊,簡單來講就是把本次打包編譯的內容存到記憶體裡。Compilation 物件也提供了外掛需要自定義功能的回撥,以供外掛做自定義處理時選擇使用擴充。

簡單來說,Compilation的職責就是構建模組和 Chunk,並利用外掛優化構建過程。

Compiler 用法相同,鉤子型別不同,也可以在某些鉤子上訪問 tapAsynctapPromise。

控制檯輸出console.log(compilation)
compilation

通過 Compilation 也能讀取到 Compiler 物件。

原始碼 2000 多行,看不動了- -,有興趣的可以自己看看。
https://github.com/webpack/we...

介紹幾個常用的 Compilation Hooks

鉤子 型別 什麼時候呼叫
buildModule SyncHook 在模組開始編譯之前觸發,可以用於修改模組
succeedModule SyncHook 當一個模組被成功編譯,會執行這個鉤子
finishModules AsyncSeriesHook 當所有模組都編譯成功後被呼叫
seal SyncHook 當一次compilation停止接收新模組時觸發
optimizeDependencies SyncBailHook 在依賴優化的開始執行
optimize SyncHook 在優化階段的開始執行
optimizeModules SyncBailHook 在模組優化階段開始時執行,外掛可以在這個鉤子裡執行對模組的優化,回撥引數:modules
optimizeChunks SyncBailHook 在程式碼塊優化階段開始時執行,外掛可以在這個鉤子裡執行對程式碼塊的優化,回撥引數:chunks
optimizeChunkAssets AsyncSeriesHook 優化任何程式碼塊資源,這些資源存放在compilation.assets 上。一個 chunk 有一個 files 屬性,它指向由一個 chunk 建立的所有檔案。任何額外的 chunk 資源都存放在 compilation.additionalChunkAssets 上。回撥引數:chunks
optimizeAssets AsyncSeriesHook 優化所有存放在 compilation.assets 的所有資源。回撥引數:assets

Compiler 和 Compilation 的區別

Compiler 代表了整個 Webpack 從啟動到關閉的生命週期,而 Compilation 只是代表了一次新的編譯,只要檔案有改動,compilation就會被重新建立。

常用 API

外掛可以用來修改輸出檔案、增加輸出檔案、甚至可以提升 Webpack 效能、等等,總之外掛通過呼叫Webpack 提供的 API 能完成很多事情。 由於 Webpack提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面來介紹一些常用的 API。

讀取輸出資源、程式碼塊、模組及其依賴

有些外掛可能需要讀取 Webpack 的處理結果,例如輸出資源、程式碼塊、模組及其依賴,以便做下一步處理。
在 emit 事件發生時,代表原始檔的轉換和組裝已經完成,在這裡可以讀取到最終將輸出的資源、程式碼塊、模組及其依賴,並且可以修改輸出資源的內容。
外掛程式碼如下:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有程式碼塊,是一個陣列
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一個程式碼塊
        // 程式碼塊由多個模組組成,通過 chunk.forEachModule 能讀取組成程式碼塊的每個模組
        chunk.forEachModule(function (module) {
          // module 代表一個模組
          // module.fileDependencies 存放當前模組的所有依賴的檔案路徑,是一個陣列
          module.fileDependencies.forEach(function (filepath) {})
        })

        // Webpack 會根據 Chunk 去生成輸出的檔案資源,每個 Chunk 都對應一個及其以上的輸出檔案
        // 例如在 Chunk 中包含了 CSS 模組並且使用了 ExtractTextPlugin 時,
        // 該 Chunk 就會生成 .js 和 .css 兩個檔案
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放當前所有即將輸出的資源
          // 呼叫一個輸出資源的 source() 方法能獲取到輸出資源的內容
          let source = compilation.assets[filename].source()
        })
      })

      // 這是一個非同步事件,要記得呼叫 callback 通知 Webpack 本次事件監聽處理結束。
      // 如果忘記了呼叫 callback,Webpack 將一直卡在這裡而不會往後執行。
      callback()
    })
  }
}

監聽檔案變化

Webpack 會從配置的入口模組出發,依次找出所有的依賴模組,當入口模組或者其依賴的模組發生變化時, 就會觸發一次新的 Compilation

在開發外掛時經常需要知道是哪個檔案發生變化導致了新的 Compilation,為此可以使用如下程式碼:

// 當依賴的檔案發生變化時會觸發 watch-run 事件
compiler.hooks.watchRun.tap('MyPlugin', (watching, callback) => {
  // 獲取發生變化的檔案列表
  const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes
  // changedFiles 格式為鍵值對,鍵為發生變化的檔案路徑。
  if (changedFiles[filePath] !== undefined) {
    // filePath 對應的檔案發生了變化
  }
  callback()
})

預設情況下 Webpack 只會監視入口和其依賴的模組是否發生變化,在有些情況下專案可能需要引入新的檔案,例如引入一個 HTML 檔案。 由於 JavaScript 檔案不會去匯入 HTML 檔案,Webpack 就不會監聽 HTML 檔案的變化,編輯 HTML 檔案時就不會重新觸發新的 Compilation。 為了監聽 HTML 檔案的變化,我們需要把 HTML 檔案加入到依賴列表中,為此可以使用如下程式碼:

compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => {
  // 把 HTML 檔案新增到檔案依賴列表,好讓 Webpack 去監聽 HTML 模組檔案,在 HTML 模版檔案發生變化時重新啟動一次編譯
  compilation.fileDependencies.push(filePath)
  callback()
})

修改輸出資源

有些場景下外掛需要修改、增加、刪除輸出的資源,要做到這點需要監聽 emit 事件,因為發生 emit 事件時所有模組的轉換和程式碼塊對應的檔案已經生成好, 需要輸出的資源即將輸出,因此 emit 事件是修改 Webpack 輸出資源的最後時機。

所有需要輸出的資源會存放在 compilation.assets 中,compilation.assets 是一個鍵值對,鍵為需要輸出的檔名稱,值為檔案對應的內容。

設定 compilation.assets 的程式碼如下:

// 設定名稱為 fileName 的輸出資源
compilation.assets[fileName] = {
  // 返回檔案內容
  source: () => {
    // fileContent 既可以是代表文字檔案的字串,也可以是代表二進位制檔案的 Buffer
    return fileContent
  },
  // 返回檔案大小
  size: () => {
    return Buffer.byteLength(fileContent, 'utf8')
  },
}
callback()

判斷 webpack 使用了哪些外掛

// 判斷當前配置使用使用了 ExtractTextPlugin,
// compiler 引數即為 Webpack 在 apply(compiler) 中傳入的引數
function hasExtractTextPlugin(compiler) {
  // 當前配置所有使用的外掛列表
  const plugins = compiler.options.plugins
  // 去 plugins 中尋找有沒有 ExtractTextPlugin 的例項
  return (
    plugins.find(
      (plugin) => plugin.__proto__.constructor === ExtractTextPlugin
    ) != null
  )
}

以上 4 種方法來源於文章:
[Webpack 學習-Plugin] :http://wushaobin.top/2019/03/...

管理 Warnings 和 Errors

做一個實驗,如果你在 apply函式內插入 throw new Error("Message"),會發生什麼,終端會列印出 Unhandled rejection Error: Message。然後 webpack 中斷執行。
為了不影響 webpack 的執行,要在編譯期間向使用者發出警告或錯誤訊息,則應使用 compilation.warnings 和 compilation.errors。

compilation.warnings.push('warning')
compilation.errors.push('error')

文章中的案例 demo 程式碼展示

https://github.com/6fedcom/fe-blog/tree/master/webpack/plugin

webpack 打包過程或者外掛程式碼裡該如何除錯?

  1. 在當前 webpack 專案工程資料夾下面,執行命令列:
node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress

其中引數--inspect-brk 就是以除錯模式啟動 node:

終端會輸出:

Debugger listening on ws://127.0.0.1:9229/1018c03f-7473-4d60-b62c-949a6404c81d
For help, see: https://nodejs.org/en/docs/inspector
  1. 谷歌瀏覽器輸入 chrome://inspect/#devices

點選inspect

  1. 然後點一下 Chrome 偵錯程式裡的“繼續執行”,斷點就提留在我們設定在外掛裡的 debugger 斷點了。

debugger

相關文章