webpack系列1:常見 loader 原始碼簡析,以及動手實現一個 md2html-loader
webpack系列2:揭祕webpack 外掛工作原理
webpack系列3:webpack 主流程原始碼閱讀以及實現一個 webpack
前言
通過外掛我們可以擴充套件webpack
,在合適的時機通過Webpack
提供的 API 改變輸出結果,使webpack
可以執行更廣泛的任務,擁有更強的構建能力。
本文將嘗試探索 webpack
外掛的工作流程,進而去揭祕它的工作原理。同時需要你對webpack
底層和構建流程的一些東西有一定的瞭解。
想要了解 webpack 的外掛的機制,需要弄明白以下幾個知識點:
- 一個簡單的外掛的構成
-
webpack
構建流程 -
Tapable
是如何把各個外掛串聯到一起的 -
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 的工作原理
- 讀取配置的過程中會先執行
new HelloPlugin(options)
初始化一個HelloPlugin
獲得其例項。 - 初始化
compiler
物件後呼叫HelloPlugin.apply(compiler)
給外掛例項傳入compiler
物件。 - 外掛例項在獲取到
compiler
物件後,就可以通過compiler.plugin(事件名稱, 回撥函式)
監聽到 Webpack 廣播出來的事件。
並且可以通過compiler
物件去操作Webpack
。
webapck 構建流程
在編寫外掛之前,還需要了解一下Webpack
的構建流程,以便在合適的時機插入合適的外掛邏輯。
Webpack 的基本構建流程如下:
- 校驗配置檔案 :讀取命令列傳入或者
webpack.config.js
檔案,初始化本次構建的配置引數 - 生成
Compiler
物件:執行配置檔案中的外掛例項化語句new MyWebpackPlugin()
,為webpack
事件流掛上自定義hooks
- 進入
entryOption
階段:webpack
開始讀取配置的Entries
,遞迴遍歷所有的入口檔案 -
run/watch
:如果執行在watch
模式則執行watch
方法,否則執行run
方法 -
compilation
:建立Compilation
物件回撥compilation
相關鉤子,依次進入每一個入口檔案(entry
),使用 loader 對檔案進行編譯。通過compilation
我可以可以讀取到module
的resource
(資源路徑)、loaders
(使用的 loader)等資訊。再將編譯好的檔案內容使用acorn
解析生成 AST 靜態語法樹。然後遞迴、重複的執行這個過程,
所有模組和和依賴分析完成後,執行compilation
的seal
方法對每個 chunk 進行整理、優化、封裝__webpack_require__
來模擬模組化操作. -
emit
:所有檔案的編譯及轉化都已經完成,包含了最終輸出的資源,我們可以在傳入事件回撥的compilation.assets
上拿到所需資料,其中包括即將輸出的資源、程式碼塊 Chunk 等等資訊。
// 修改或新增資源
compilation.assets['new-file.js'] = {
source() {
return 'var a=1'
},
size() {
return this.source().length
},
}
-
afterEmit
:檔案已經寫入磁碟完成 -
done
:完成編譯
奉上一張滴滴雲部落格的 WebPack 編譯流程圖,不喜歡看文字講解的可以看流程圖理解記憶
原圖出自:https://blog.didiyun.com/inde...
看完之後,如果還是看不懂或者對縷不清 webpack 構建流程的話,建議通讀一下全文,再回來看這段話,相信一定會對 webpack 構建流程有很更加深刻的理解。
理解事件流機制 tapable
webpack
本質上是一種事件流的機制,它的工作流程就是將各個外掛串聯起來,而實現這一切的核心就是 Tapable。
Webpack
的 Tapable
事件流機制保證了外掛的有序性,將各個外掛串聯起來, Webpack 在執行過程中會廣播事件,外掛只需要監聽它所關心的事件,就能加入到這條 webapck 機制中,去改變 webapck 的運作,使得整個系統擴充套件性良好。
Tapable
也是一個小型的 library,是Webpack
的一個核心工具。類似於node
中的events
庫,核心原理就是一個訂閱釋出模式。作用是提供類似的外掛介面。
webpack 中最核心的負責編譯的Compiler
和負責建立 bundles 的Compilation
都是 Tapable 的例項,可以直接在 Compiler
和 Compilation
物件上廣播和監聽事件,方法如下:
/**
* 廣播事件
* 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
類暴露了tap
、tapAsync
和tapPromise
方法,可以根據鉤子的同步/非同步方式來選擇一個函式注入邏輯。
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')
簡單實現一個 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(負責編譯)
開發外掛首先要知道compiler
和 compilation
物件是做什麼的
Compiler
物件包含了當前執行Webpack
的配置,包括entry、output、loaders
等配置,這個物件在啟動Webpack
時被例項化,而且是全域性唯一的。Plugin
可以通過該物件獲取到 Webpack 的配置資訊進行處理。
如果看完這段話,你還是沒理解compiler
是做啥的,不要怕,接著看。
執行npm run build
,把compiler
的全部資訊輸出到控制檯上console.log(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
用法相同,鉤子型別不同,也可以在某些鉤子上訪問 tapAsync
和 tapPromise。
控制檯輸出console.log(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 打包過程或者外掛程式碼裡該如何除錯?
- 在當前 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
- 谷歌瀏覽器輸入 chrome://inspect/#devices
- 然後點一下 Chrome 偵錯程式裡的“繼續執行”,斷點就提留在我們設定在外掛裡的 debugger 斷點了。