webpack是現代前端開發中最火的模塊打包工具,只需要通過簡單的配置,便可以完成模塊的加載和打包。那它是怎麼做到通過對一些插件的配置,便可以輕鬆實現對代碼的構建呢?
webpack的配置
const path =require('path');
module.exports =
{ entry
:
"./app/entry"
,
// string | object | array
// Webpack打包的入口
output
:
{
// 定義webpack如何輸出的選項
path
:
path
.
resolve
(
__dirname
,
"dist"
),
// string
// 所有輸出文件的目標路徑
filename
:
"[chunkhash].js"
,
// string
// 「入口(entry chunk)」文件命名模版
publicPath
:
"/assets/"
,
// string
// 構建文件的輸出目錄
/* 其它高級配置 */
},
module
:
{
// 模塊相關配置
rules
:
[
// 配置模塊loaders,解析規則
{
test
:
/\\.jsx?$/
,
// RegExp | string
include
:
[
// 和test一樣,必須匹配選項
path
.
resolve
(
__dirname
,
"app"
)
],
exclude
:
[
// 必不匹配選項(優先級高於test和include)
path
.
resolve
(
__dirname
,
"app/demo-files"
)
],
loader
:
"babel-loader"
,
// 模塊上下文解析
options
:
{
// loader的可選項
presets
:
[
"es2015"
]
},
},
},
resolve
:
{
// 解析模塊的可選項
modules
:
[
// 模塊的查找目錄
"node_modules"
,
path
.
resolve
(
__dirname
,
"app"
)
],
extensions
:
[
".js"
,
".json"
,
".jsx"
,
".css"
],
// 用到的文件的擴展
alias
:
{
// 模塊別名列表
"module"
:
"new-module"
},
},
devtool
:
"source-map"
,
// enum
// 為瀏覽器開發者工具添加元數據增強調試
plugins
:
[
// 附加插件列表
// ...
],
}
從上面我們可以看到,webpack配置中需要理解幾個核心的概念 Entry 、 Output、 Loaders 、 Plugins、 Chunk:
- Entry:指定webpack開始構建的入口模塊,從該模塊開始構建並計算出直接或間接依賴的模塊或者庫
- Output:告訴webpack如何命名輸出的文件以及輸出的目錄
- Loaders:由於webpack只能處理javascript,所以我們需要對一些非js文件處理成webpack能夠處理的模塊,比如sass文件
- Plugins: Loaders將各類型的文件處理成webpack能夠處理的模塊, plugins有著很強的能力。插件的範圍包括,從打包優化和壓縮,一直到重新定義環境中的變量。但也是最複雜的一個。比如對js文件進行壓縮優化的 UglifyJsPlugin插件
- Chunk:coding split的產物,我們可以對一些代碼打包成一個單獨的chunk,比如某些公共模塊,去重,更好的利用緩存。或者按需加載某些功能模塊,優化加載時間。在webpack3及以前我們都利用 CommonsChunkPlugin將一些公共代碼分割成一個chunk,實現單獨加載。在webpack4 中 CommonsChunkPlugin被廢棄,使用 SplitChunksPlugin
webpack詳解
讀到這裡,或許你對webpack有一個大概的瞭解,那webpack 是怎麼運行的呢?我們都知道,webpack是高度複雜抽象的插件集合,理解webpack的運行機制,對於我們日常定位構建錯誤以及寫一些插件處理構建任務有很大的幫助。
不得不說的tapable
webpack本質上是一種事件流的機制,它的工作流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的 Compiler和負責創建bundles的 Compilation都是Tapable的實例。在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括:
- plugin(name:string,handler:function)註冊插件到Tapable對象中
- apply(…pluginInstances:(AnyPlugin|function)[])調用插件的定義,將事件監聽器註冊到Tapable實例註冊表中
- applyPlugins*(name:string,…)多種策略細緻地控制事件的觸發,包括 applyPluginsAsync、 applyPluginsParallel等方法實現對事件觸發的控制,實現
(1)多個事件連續順序執行
(2)並行執行
(3)異步執行
(4)一個接一個地執行插件,前面的輸出是後一個插件的輸入的瀑布流執行順序
(5)在允許時停止執行插件,即某個插件返回了一個 undefined的值,即退出執行
我們可以看到,Tapable就像nodejs中 EventEmitter,提供對事件的註冊 on和觸發 emit,理解它很重要,看個栗子:比如我們來寫一個插件
function
CustomPlugin
()
{}
CustomPlugin
.
prototype
.
apply
=
function
(
compiler
)
{
compiler
.
plugin
(
'emit'
,
pluginFunction
);
}
在webpack的生命週期中會適時的執行:
this
.
apply
*(
"emit"
,
options
)
當然上面提到的Tapable都是1.0版本之前的,如果想深入學習,可以查看Tapable和事件流(https://segmentfault.com/a/1190000008060440)。
那1.0的Tapable又是什麼樣的呢?1.0版本發生了巨大的改變,不再是此前的通過 plugin註冊事件,通過 applyPlugins*觸發事件調用,那1.0的Tapable是什麼呢?
暴露出很多的鉤子,可以使用它們為插件創建鉤子函數
const
{
SyncHook
,
SyncBailHook
,
SyncWaterfallHook
,
SyncLoopHook
,
AsyncParallelHook
,
AsyncParallelBailHook
,
AsyncSeriesHook
,
AsyncSeriesBailHook
,
AsyncSeriesWaterfallHook
}
=
require
(
"tapable"
);
我們來看看怎麼使用。
class
Order
{
constructor
()
{
this
.
hooks
=
{
//hooks
goods
:
new
SyncHook
([
'goodsId'
,
'number'
]),
consumer
:
new
AsyncParallelHook
([
'userId'
,
'orderId'
])
}
}
queryGoods
(
goodsId
,
number
)
{
this
.
hooks
.
goods
.
call
(
goodsId
,
number
);
}
consumerInfoPromise
(
userId
,
orderId
)
{
this
.
hooks
.
consumer
.
promise
(
userId
,
orderId
).
then
(()
=>
{
//TODO
})
}
consumerInfoAsync
(
userId
,
orderId
)
{
this
.
hooks
.
consumer
.
callAsync
(
userId
,
orderId
,
(
err
,
data
)
=>
{
//TODO
})
}
}
對於所有的hook的構造函數均接受一個可選的string類型的數組。
const
hook
=
new
SyncHook
([
"arg1"
,
"arg2"
,
"arg3"
]);
// 調用tap方法註冊一個consument
order
.
hooks
.
goods
.
tap
(
'QueryPlugin'
,
(
goodsId
,
number
)
=>
{
return
fetchGoods
(
goodsId
,
number
);
})
// 再添加一個
order
.
hooks
.
goods
.
tap
(
'LoggerPlugin'
,
(
goodsId
,
number
)
=>
{
logger
(
goodsId
,
number
);
})
// 調用
order
.
queryGoods
(
'10000000'
,
1
)
對於一個 SyncHook,我們通過 tap來添加消費者,通過 call來觸發鉤子的順序執行。
對於一個非 sync*類型的鉤子,即 async*類型的鉤子,我們還可以通過其它方式註冊消費者和調用
// 註冊一個sync 鉤子
order
.
hooks
.
consumer
.
tap
(
'LoggerPlugin'
,
(
userId
,
orderId
)
=>
{
logger
(
userId
,
orderId
);
})
order
.
hooks
.
consumer
.
tapAsync
(
'LoginCheckPlugin'
,
(
userId
,
orderId
,
callback
)
=>
{
LoginCheck
(
userId
,
callback
);
})
order
.
hooks
.
consumer
.
tapPromise
(
'PayPlugin'
,
(
userId
,
orderId
)
=>
{
return
Promise
.
resolve
();
})
// 調用
// 返回Promise
order
.
consumerInfoPromise
(
'user007'
,
'1024'
);
//回調函數
order
.
consumerInfoAsync
(
'user007'
,
'1024'
)
通過上面的栗子,你可能已經大致瞭解了 Tapable的用法,它的用法:
- 插件註冊數量
- 插件註冊的類型(sync, async, promise)
- 調用的方式(sync, async, promise)
- 實例鉤子的時候參數數量
- 是否使用了 interception
Tapable詳解
對於 Sync*類型的鉤子來說:
- 註冊在該鉤子下面的插件的執行順序都是順序執行。
- 只能使用 tap註冊,不能使用 tapPromise和 tapAsync註冊
// 所有的鉤子都繼承於Hook
class
Sync
*
extends
Hook
{
tapAsync
()
{
// Sync*類型的鉤子不支持tapAsync
throw
new
Error
(
"tapAsync is not supported on a Sync*"
);
}
tapPromise
()
{
// Sync*類型的鉤子不支持tapPromise
throw
new
Error
(
"tapPromise is not supported on a Sync*"
);
}
compile
(
options
)
{
// 編譯代碼來按照一定的策略執行Plugin
factory
.
setup
(
this
,
options
);
return
factory
.
create
(
options
);
}
}
對於 Async*類型鉤子:支持 tap、 tapPromise、 tapAsync註冊。
class
AsyncParallelHook
extends
Hook
{
constructor
(
args
)
{
super
(
args
);
this
.
call
=
this
.
_call
=
undefined
;
}
compile
(
options
)
{
factory
.
setup
(
this
,
options
);
return
factory
.
create
(
options
);
}
}
class
Hook
{
constructor
(
args
)
{
if
(!
Array
.
isArray
(
args
))
args
=
[];
this
.
_args
=
args
;
// 實例鉤子的時候的string類型的數組
this
.
taps
=
[];
// 消費者
this
.
interceptors
=
[];
// interceptors
this
.
call
=
this
.
_call
=
// 以sync類型方式來調用鉤子
this
.
_createCompileDelegate
(
"call"
,
"sync"
);
this
.
promise
=
this
.
_promise
=
// 以promise方式
this
.
_createCompileDelegate
(
"promise"
,
"promise"
);
this
.
callAsync
=
this
.
_callAsync
=
// 以async類型方式來調用
this
.
_createCompileDelegate
(
"callAsync"
,
"async"
);
this
.
_x
=
undefined
;
//
}
_createCall
(
type
)
{
return
this
.
compile
({
taps
:
this
.
taps
,
interceptors
:
this
.
interceptors
,
args
:
this
.
_args
,
type
:
type
});
}
_createCompileDelegate
(
name
,
type
)
{
const
lazyCompileHook
=
(...
args
)
=>
{
this
[
name
]
=
this
.
_createCall
(
type
);
return
this
[
name
](...
args
);
};
return
lazyCompileHook
;
}
// 調用tap 類型註冊
tap
(
options
,
fn
)
{
// ...
options
=
Object
.
assign
({
type
:
"sync"
,
fn
:
fn
},
options
);
// ...
this
.
_insert
(
options
);
// 添加到 this.taps中
}
// 註冊 async類型的鉤子
tapAsync
(
options
,
fn
)
{
// ...
options
=
Object
.
assign
({
type
:
"async"
,
fn
:
fn
},
options
);
// ...
this
.
_insert
(
options
);
// 添加到 this.taps中
}
註冊
promise
類型鉤子
tapPromise
(
options
,
fn
)
{
// ...
options
=
Object
.
assign
({
type
:
"promise"
,
fn
:
fn
},
options
);
// ...
this
.
_insert
(
options
);
// 添加到 this.taps中
}
}
每次都是調用 tap、 tapSync、 tapPromise註冊不同類型的插件鉤子,通過調用 call、 callAsync 、 promise方式調用。其實調用的時候為了按照一定的執行策略執行,調用 compile方法快速編譯出一個方法來執行這些插件。
const
factory
=
new
Sync
*
CodeFactory
();
class
Sync
*
extends
Hook
{
// ...
compile
(
options
)
{
// 編譯代碼來按照一定的策略執行Plugin
factory
.
setup
(
this
,
options
);
return
factory
.
create
(
options
);
}
}
class
Sync
*
CodeFactory
extends
HookCodeFactory
{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})
{
return
this
.
callTapsSeries
({
onError
:
(
i
,
err
)
=>
onError
(
err
),
onDone
,
rethrowIfPossible
});
}
}
compile中調用 HookCodeFactory#create方法編譯生成執行代碼。
class
HookCodeFactory
{
constructor
(
config
)
{
this
.
config
=
config
;
this
.
options
=
undefined
;
}
create
(
options
)
{
this
.
init
(
options
);
switch
(
this
.
options
.
type
)
{
case
"sync"
:
// 編譯生成sync, 結果直接返回
return
new
Function
(
this
.
args
(),
"\\"use strict\\";\\n"
+
this
.
header
()
+
this
.
content
({
// ...
onResult
:
result
=>
`
return
$
{
result
};
\\n
`,
// ...
}));
case
"async"
:
// async類型, 異步執行,最後將調用插件執行結果來調用callback,
return
new
Function
(
this
.
args
({
after
:
"_callback"
}),
"\\"use strict\\";\\n"
+
this
.
header
()
+
this
.
content
({
// ...
onResult
:
result
=>
`
_callback
(
null
,
$
{
result
});
\\n
`,
onDone
:
()
=>
"_callback();\\n"
}));
case
"promise"
:
// 返回promise類型,將結果放在resolve中
// ...
code
+=
"return new Promise((_resolve, _reject) => {\\n"
;
code
+=
"var _sync = true;\\n"
;
code
+=
this
.
header
();
code
+=
this
.
content
({
// ...
onResult
:
result
=>
`
_resolve
(
$
{
result
});
\\n
`,
onDone
:
()
=>
"_resolve();\\n"
});
// ...
return
new
Function
(
this
.
args
(),
code
);
}
}
// callTap 就是執行一些插件,並將結果返回
callTap
(
tapIndex
,
{
onError
,
onResult
,
onDone
,
rethrowIfPossible
})
{
let code
=
""
;
let hasTapCached
=
false
;
// ...
code
+=
`
var
_fn$
{
tapIndex
}
=
$
{
this
.
getTapFn
(
tapIndex
)};
\\n
`;
const
tap
=
this
.
options
.
taps
[
tapIndex
];
switch
(
tap
.
type
)
{
case
"sync"
:
// ...
if
(
onResult
)
{
code
+=
`
var
_result$
{
tapIndex
}
=
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?
"_context"
:
undefined
})});
\\n
`;
}
else
{
code
+=
`
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?
"_context"
:
undefined
})});
\\n
`;
}
if
(
onResult
)
{
// 結果透傳
code
+=
onResult
(`
_result$
{
tapIndex
}`);
}
if
(
onDone
)
{
// 通知插件執行完畢,可以執行下一個插件
code
+=
onDone
();
}
break
;
case
"async"
:
//異步執行,插件運行完後再將結果通過執行callback透傳
let cbCode
=
""
;
if
(
onResult
)
cbCode
+=
`(
_err$
{
tapIndex
},
_result$
{
tapIndex
})
=>
{
\\n
`;
else
cbCode
+=
`
_err$
{
tapIndex
}
=>
{
\\n
`;
cbCode
+=
`
if
(
_err$
{
tapIndex
})
{
\\n
`;
cbCode
+=
onError
(`
_err$
{
tapIndex
}`);
cbCode
+=
"} else {\\n"
;
if
(
onResult
)
{
cbCode
+=
onResult
(`
_result$
{
tapIndex
}`);
}
cbCode
+=
"}\\n"
;
cbCode
+=
"}"
;
code
+=
`
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?
"_context"
:
undefined
,
after
:
cbCode
//cbCode將結果透傳
})});
\\n
`;
break
;
case
"promise"
:
// _fn${tapIndex} 就是第tapIndex 個插件,它必須是個Promise類型的插件
code
+=
`
var
_hasResult$
{
tapIndex
}
=
false
;
\\n
`;
code
+=
`
_fn$
{
tapIndex
}(
$
{
this
.
args
({
before
:
tap
.
context
?
"_context"
:
undefined
})}).
then
(
_result$
{
tapIndex
}
=>
{
\\n
`;
code
+=
`
_hasResult$
{
tapIndex
}
=
true
;
\\n
`;
if
(
onResult
)
{
code
+=
onResult
(`
_result$
{
tapIndex
}`);
}
// ...
break
;
}
return
code
;
}
// 按照插件的註冊順序,按照順序遞歸調用執行插件
callTapsSeries
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})
{
// ...
const
firstAsync
=
this
.
options
.
taps
.
findIndex
(
t
=>
t
.
type
!==
"sync"
);
const
next
=
i
=>
{
// ...
const
done
=
()
=>
next
(
i
+
1
);
// ...
return
this
.
callTap
(
i
,
{
// ...
onResult
:
onResult
&&
((
result
)
=>
{
return
onResult
(
i
,
result
,
done
,
doneBreak
);
}),
// ...
});
};
return
next
(
0
);
}
callTapsLooping
({
onError
,
onDone
,
rethrowIfPossible
})
{
const
syncOnly
=
this
.
options
.
taps
.
every
(
t
=>
t
.
type
===
"sync"
);
let code
=
""
;
if
(!
syncOnly
)
{
code
+=
"var _looper = () => {\\n"
;
code
+=
"var _loopAsync = false;\\n"
;
}
code
+=
"var _loop;\\n"
;
code
+=
"do {\\n"
;
code
+=
"_loop = false;\\n"
;
// ...
code
+=
this
.
callTapsSeries
({
// ...
onResult
:
(
i
,
result
,
next
,
doneBreak
)
=>
{
// 一旦某個插件返回不為undefined, 即一隻調用某個插件執行,如果為undefined,開始調用下一個
let code
=
""
;
code
+=
`
if
(
$
{
result
}
!==
undefined
)
{
\\n
`;
code
+=
"_loop = true;\\n"
;
if
(!
syncOnly
)
code
+=
"if(_loopAsync) _looper();\\n"
;
code
+=
doneBreak
(
true
);
code
+=
`}
else
{
\\n
`;
code
+=
next
();
code
+=
`}
\\n
`;
return
code
;
},
// ...
})
code
+=
"} while(_loop);\\n"
;
// ...
return
code
;
}
// 並行調用插件執行
callTapsParallel
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
,
onTap
=
(
i
,
run
)
=>
run
()
})
{
// ...
// 遍歷註冊都所有插件,並調用
for
(
let i
=
0
;
i
<
this
.
options
.
taps
.
length
;
i
++)
{
// ...
code
+=
"if(_counter <= 0) break;\\n"
;
code
+=
onTap
(
i
,
()
=>
this
.
callTap
(
i
,
{
// ...
onResult
:
onResult
&&
((
result
)
=>
{
let code
=
""
;
code
+=
"if(_counter > 0) {\\n"
;
code
+=
onResult
(
i
,
result
,
done
,
doneBreak
);
code
+=
"}\\n"
;
return
code
;
}),
// ...
}),
done
,
doneBreak
);
}
// ...
return
code
;
}
}
在 HookCodeFactory#create中調用到 content方法,此方法將按照此鉤子的執行策略,調用不同的方法來執行編譯 生成最終的代碼。
SyncHook中調用 callTapsSeries編譯生成最終執行插件的函數, callTapsSeries做的就是將插件列表中插件按照註冊順序遍歷執行。
class
SyncHookCodeFactory
extends
HookCodeFactory
{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})
{
return
this
.
callTapsSeries
({
onError
:
(
i
,
err
)
=>
onError
(
err
),
onDone
,
rethrowIfPossible
});
}
}
SyncBailHook中當一旦某個返回值結果不為 undefined便結束執行列表中的插件。
class
SyncBailHookCodeFactory
extends
HookCodeFactory
{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})
{
return
this
.
callTapsSeries
({
// ...
onResult
:
(
i
,
result
,
next
)
=>
`
if
(
$
{
result
}
!==
undefined
)
{
\\n$
{
onResult
(
result
)};
\\n
}
else
{
\\n$
{
next
()}}
\\n
`,
// ...
});
}
}
SyncWaterfallHook中上一個插件執行結果當作下一個插件的入參。
class
SyncWaterfallHookCodeFactory
extends
HookCodeFactory
{
content
({
onError
,
onResult
,
onDone
,
rethrowIfPossible
})
{
return
this
.
callTapsSeries
({
// ...
onResult
:
(
i
,
result
,
next
)
=>
{
let code
=
""
;
code
+=
`
if
(
$
{
result
}
!==
undefined
)
{
\\n
`;
code
+=
`
$
{
this
.
_args
[
0
]}
=
$
{
result
};
\\n
`;
code
+=
`}
\\n
`;
code
+=
next
();
return
code
;
},
onDone
:
()
=>
onResult
(
this
.
_args
[
0
]),
});
}
}
- AsyncParallelHook調用 callTapsParallel並行執行插件
class
AsyncParallelHookCodeFactory
extends
HookCodeFactory
{
content
({
onError
,
onDone
})
{
return
this
.
callTapsParallel
({
onError
:
(
i
,
err
,
done
,
doneBreak
)
=>
onError
(
err
)
+
doneBreak
(
true
),
onDone
});
}
}
webpack流程篇
本文關於webpack 的流程講解是基於webpack4的。
webpack 入口文件
從webpack項目的package.json文件中我們找到了入口執行函數,在函數中引入webpack,那麼入口將會是 lib/webpack.js,而如果在shell中執行,那麼將會走到 ./bin/webpack.js,我們就以 lib/webpack.js為入口開始吧!
{
"name"
:
"webpack"
,
"version"
:
"4.1.1"
,
...
"main"
:
"lib/webpack.js"
,
"web"
:
"lib/webpack.web.js"
,
"bin"
:
"./bin/webpack.js"
,
...
}
webpack入口
const
webpack
=
(
options
,
callback
)
=>
{
// ...
// 驗證options正確性
// 預處理options
options
=
new
WebpackOptionsDefaulter
().
process
(
options
);
// webpack4的默認配置
compiler
=
new
Compiler
(
options
.
context
);
// 實例Compiler
// ...
// 若options.watch === true && callback 則開啟watch線程
compiler
.
watch
(
watchOptions
,
callback
);
compiler
.
run
(
callback
);
return
compiler
;
};
webpack 的入口文件其實就實例了 Compiler並調用了 run方法開啟了編譯,webpack的編譯都按照下面的鉤子調用順序執行:
- before-run 清除緩存
- run 註冊緩存數據鉤子
- before-compile
- compile 開始編譯
- make 從入口分析依賴以及間接依賴模塊,創建模塊對象
- build-module 模塊構建
- seal 構建結果封裝, 不可再更改
- after-compile 完成構建,緩存數據
- emit 輸出到dist目錄
編譯&構建流程
webpack中負責構建和編譯都是 Compilation。
class
Compilation
extends
Tapable
{
constructor
(
compiler
)
{
super
();
this
.
hooks
=
{
// hooks
};
// ...
this
.
compiler
=
compiler
;
// ...
// template
this
.
mainTemplate
=
new
MainTemplate
(
this
.
outputOptions
);
this
.
chunkTemplate
=
new
ChunkTemplate
(
this
.
outputOptions
);
this
.
hotUpdateChunkTemplate
=
new
HotUpdateChunkTemplate
(
this
.
outputOptions
);
this
.
runtimeTemplate
=
new
RuntimeTemplate
(
this
.
outputOptions
,
this
.
requestShortener
);
this
.
moduleTemplates
=
{
javascript
:
new
ModuleTemplate
(
this
.
runtimeTemplate
),
webassembly
:
new
ModuleTemplate
(
this
.
runtimeTemplate
)
};
// 構建生成的資源
this
.
chunks
=
[];
this
.
chunkGroups
=
[];
this
.
modules
=
[];
this
.
additionalChunkAssets
=
[];
this
.
assets
=
{};
this
.
children
=
[];
// ...
}
//
buildModule
(
module
,
optional
,
origin
,
dependencies
,
thisCallback
)
{
// ...
// 調用module.build方法進行編譯代碼,build中 其實是利用acorn編譯生成AST
this
.
hooks
.
buildModule
.
call
(
module
);
module
.
build
(
/**param*/
);
}
// 將模塊添加到列表中,並編譯模塊
_addModuleChain
(
context
,
dependency
,
onModule
,
callback
)
{
// ...
// moduleFactory.create創建模塊,這裡會先利用loader處理文件,然後生成模塊對象
moduleFactory
.
create
(
{
contextInfo
:
{
issuer
:
""
,
compiler
:
this
.
compiler
.
name
},
context
:
context
,
dependencies
:
[
dependency
]
},
(
err
,
module
)
=>
{
const
addModuleResult
=
this
.
addModule
(
module
);
module
=
addModuleResult
.
module
;
onModule
(
module
);
dependency
.
module
=
module
;
// ...
// 調用buildModule編譯模塊
this
.
buildModule
(
module
,
false
,
null
,
null
,
err
=>
{});
}
});
}
// 添加入口模塊,開始編譯&構建
addEntry
(
context
,
entry
,
name
,
callback
)
{
// ...
this
.
_addModuleChain
(
// 調用_addModuleChain添加模塊
context
,
entry
,
module
=>
{
this
.
entries
.
push
(
module
);
},
// ...
);
}
seal
(
callback
)
{
this
.
hooks
.
seal
.
call
();
// ...
const
chunk
=
this
.
addChunk
(
name
);
const
entrypoint
=
new
Entrypoint
(
name
);
entrypoint
.
setRuntimeChunk
(
chunk
);
entrypoint
.
addOrigin
(
null
,
name
,
preparedEntrypoint
.
request
);
this
.
namedChunkGroups
.
set
(
name
,
entrypoint
);
this
.
entrypoints
.
set
(
name
,
entrypoint
);
this
.
chunkGroups
.
push
(
entrypoint
);
GraphHelpers
.
connectChunkGroupAndChunk
(
entrypoint
,
chunk
);
GraphHelpers
.
connectChunkAndModule
(
chunk
,
module
);
chunk
.
entryModule
=
module
;
chunk
.
name
=
name
;
// ...
this
.
hooks
.
beforeHash
.
call
();
this
.
createHash
();
this
.
hooks
.
afterHash
.
call
();
this
.
hooks
.
beforeModuleAssets
.
call
();
this
.
createModuleAssets
();
if
(
this
.
hooks
.
shouldGenerateChunkAssets
.
call
()
!==
false
)
{
this
.
hooks
.
beforeChunkAssets
.
call
();
this
.
createChunkAssets
();
}
// ...
}
createHash
()
{
// ...
}
// 生成 assets 資源並 保存到 Compilation.assets 中 給webpack寫插件的時候會用到
createModuleAssets
()
{
for
(
let i
=
0
;
i
<
this
.
modules
.
length
;
i
++)
{
const
module
=
this
.
modules
[
i
];
if
(
module
.
buildInfo
.
assets
)
{
for
(
const
assetName of
Object
.
keys
(
module
.
buildInfo
.
assets
))
{
const
fileName
=
this
.
getPath
(
assetName
);
this
.
assets
[
fileName
]
=
module
.
buildInfo
.
assets
[
assetName
];
this
.
hooks
.
moduleAsset
.
call
(
module
,
fileName
);
}
}
}
}
createChunkAssets
()
{
// ...
}
}
在webpack make鉤子中, tapAsync註冊了一個 DllEntryPlugin, 就是將入口模塊通過調用 compilation.addEntry方法將所有的入口模塊添加到編譯構建隊列中,開啟編譯流程。
compiler
.
hooks
.
make
.
tapAsync
(
"DllEntryPlugin"
,
(
compilation
,
callback
)
=>
{
compilation
.
addEntry
(
this
.
context
,
new
DllEntryDependency
(
this
.
entries
.
map
((
e
,
idx
)
=>
{
const
dep
=
new
SingleEntryDependency
(
e
);
dep
.
loc
=
`
$
{
this
.
name
}:
$
{
idx
}`;
return
dep
;
}),
this
.
name
),
// ...
);
});
隨後在 addEntry 中調用 _addModuleChain開始編譯。在 _addModuleChain首先會生成模塊,最後構建。
class
NormalModuleFactory
extends
Tapable
{
// ...
create
(
data
,
callback
)
{
// ...
this
.
hooks
.
beforeResolve
.
callAsync
(
{
contextInfo
,
resolveOptions
,
context
,
request
,
dependencies
},
(
err
,
result
)
=>
{
if
(
err
)
return
callback
(
err
);
// Ignored
if
(!
result
)
return
callback
();
// factory 鉤子會觸發 resolver 鉤子執行,而resolver鉤子中會利用acorn 處理js生成AST,再利用acorn處理前,會使用loader加載文件
const
factory
=
this
.
hooks
.
factory
.
call
(
null
);
factory
(
result
,
(
err
,
module
)
=>
{
if
(
err
)
return
callback
(
err
);
if
(
module
&&
this
.
cachePredicate
(
module
))
{
for
(
const
d of dependencies
)
{
d
.
__NormalModuleFactoryCache
=
module
;
}
}
callback
(
null
,
module
);
});
}
);
}
}
在編譯完成後,調用 compilation.seal方法封閉,生成資源,這些資源保存在 compilation.assets, compilation.chunk, 在給webpack寫插件的時候會用到。
class
Compiler
extends
Tapable
{
constructor
(
context
)
{
super
();
this
.
hooks
=
{
beforeRun
:
new
AsyncSeriesHook
([
"compilation"
]),
run
:
new
AsyncSeriesHook
([
"compilation"
]),
emit
:
new
AsyncSeriesHook
([
"compilation"
]),
afterEmit
:
new
AsyncSeriesHook
([
"compilation"
]),
compilation
:
new
SyncHook
([
"compilation"
,
"params"
]),
beforeCompile
:
new
AsyncSeriesHook
([
"params"
]),
compile
:
new
SyncHook
([
"params"
]),
make
:
new
AsyncParallelHook
([
"compilation"
]),
afterCompile
:
new
AsyncSeriesHook
([
"compilation"
]),
// other hooks
};
// ...
}
run
(
callback
)
{
const
startTime
=
Date
.
now
();
const
onCompiled
=
(
err
,
compilation
)
=>
{
// ...
this
.
emitAssets
(
compilation
,
err
=>
{
if
(
err
)
return
callback
(
err
);
if
(
compilation
.
hooks
.
needAdditionalPass
.
call
())
{
compilation
.
needAdditionalPass
=
true
;
const
stats
=
new
Stats
(
compilation
);
stats
.
startTime
=
startTime
;
stats
.
endTime
=
Date
.
now
();
this
.
hooks
.
done
.
callAsync
(
stats
,
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
hooks
.
additionalPass
.
callAsync
(
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
compile
(
onCompiled
);
});
});
return
;
}
// ...
});
};
this
.
hooks
.
beforeRun
.
callAsync
(
this
,
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
hooks
.
run
.
callAsync
(
this
,
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
readRecords
(
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
compile
(
onCompiled
);
});
});
});
}
// 輸出文件到構建目錄
emitAssets
(
compilation
,
callback
)
{
// ...
this
.
hooks
.
emit
.
callAsync
(
compilation
,
err
=>
{
if
(
err
)
return
callback
(
err
);
outputPath
=
compilation
.
getPath
(
this
.
outputPath
);
this
.
outputFileSystem
.
mkdirp
(
outputPath
,
emitFiles
);
});
}
newCompilationParams
()
{
const
params
=
{
normalModuleFactory
:
this
.
createNormalModuleFactory
(),
contextModuleFactory
:
this
.
createContextModuleFactory
(),
compilationDependencies
:
new
Set
()
};
return
params
;
}
compile
(
callback
)
{
const
params
=
this
.
newCompilationParams
();
this
.
hooks
.
beforeCompile
.
callAsync
(
params
,
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
hooks
.
compile
.
call
(
params
);
const
compilation
=
this
.
newCompilation
(
params
);
this
.
hooks
.
make
.
callAsync
(
compilation
,
err
=>
{
if
(
err
)
return
callback
(
err
);
compilation
.
finish
();
// make 鉤子執行後,調用seal生成資源
compilation
.
seal
(
err
=>
{
if
(
err
)
return
callback
(
err
);
this
.
hooks
.
afterCompile
.
callAsync
(
compilation
,
err
=>
{
if
(
err
)
return
callback
(
err
);
// emit, 生成最終文件
return
callback
(
null
,
compilation
);
});
});
});
});
}
}
最後輸出
在 seal執行後,便會調用 emit鉤子,根據webpack config文件的output配置的path屬性,將文件輸出到指定的path.
閱讀更多 法大哈 的文章
關鍵字: 瀏覽器 JavaScript 模塊