Webpack原理與實(shí)踐之Webpack運(yùn)行機(jī)制與核心工作原理
寫在前面
Webpack在整個(gè)打包過(guò)程中:
通過(guò)loader處理特殊類型資源的加載,例如加載樣式、圖片
通過(guò)plugin實(shí)現(xiàn)各種自動(dòng)化的構(gòu)建任務(wù),例如自動(dòng)壓縮、自動(dòng)發(fā)布
那么webpack的工作過(guò)程和原理又是如何實(shí)現(xiàn)的呢?
Webpack的工作過(guò)程
首先webpack會(huì)加載入口文件js,通過(guò)分析代碼中import、require等去解析依賴,然后通過(guò)依賴形成依賴關(guān)系樹,webpack會(huì)去遍歷依賴關(guān)系樹,去加載所依賴的資源模塊。webpack會(huì)通過(guò)Loader配置去加載模塊,通過(guò)plugins實(shí)現(xiàn)自動(dòng)化構(gòu)建。
對(duì)于依賴模塊中無(wú)法通過(guò)js代碼表示的資源模塊,例如圖片或字體文件,一般的Loader會(huì)將它們單獨(dú)作為資源文件拷貝到輸出目錄中,然后將這個(gè)資源文件所對(duì)應(yīng)的訪問(wèn)路徑作為這個(gè)模塊的導(dǎo)出成員暴露給外部。
webpack在每個(gè)打包環(huán)節(jié)都預(yù)留了鉤子,我們可以通過(guò)plugins去配置其所依賴的插件。
具體的:
- Webpack cli啟動(dòng)打包流程
- 載入Webpack核心模塊,創(chuàng)建Compiler對(duì)象
- 使用創(chuàng)建Compiler對(duì)象開始編譯整個(gè)項(xiàng)目
- 從入口文件開始,解析模塊依賴,形成依賴關(guān)系樹
- 遞歸遍歷依賴樹,將每個(gè)模塊交給對(duì)應(yīng)的loader處理
- 合并loader處理完的結(jié)果,將打包結(jié)果輸出到dist目錄
Webpack cli的作用是將cli參數(shù)和webpack配置文件中的配置進(jìn)行整合得到一個(gè)完整的配置對(duì)象。Webpack cli會(huì)通過(guò)yargs模塊解析cli參數(shù),運(yùn)行webpack命令時(shí)通過(guò)命令行傳入的參數(shù)。
- const config = { options: {}, path: new WeakMap() };
- // 判斷是否指定了配置文件
- if (options.config && options.config.length > 0) {
- const loadedConfigs = await Promise.all(
- options.config.map((configPath) =>
- loadConfigByPath(path.resolve(configPath), options.argv),
- ),
- );
- config.options = [];
- loadedConfigs.forEach((loadedConfig) => {
- const isArray = Array.isArray(loadedConfig.options);
- // TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release
- if (config.options.length === 0) {
- config.options = loadedConfig.options;
- } else {
- if (!Array.isArray(config.options)) {
- config.options = [config.options];
- }
- if (isArray) {
- loadedConfig.options.forEach((item) => {
- config.options.push(item);
- });
- } else {
- config.options.push(loadedConfig.options);
- }
- }
- if (isArray) {
- loadedConfig.options.forEach((options) => {
- config.path.set(options, loadedConfig.path);
- });
- } else {
- config.path.set(loadedConfig.options, loadedConfig.path);
- }
- });
- config.options = config.options.length === 1 ? config.options[0] : config.options;
- } else {
- // 按照配置文件規(guī)則找到加載配置文件
- // Order defines the priority, in decreasing order
- const defaultConfigFiles = [
- "webpack.config",
- ".webpack/webpack.config",
- ".webpack/webpackfile",
- ]
- .map((filename) =>
- // Since .cjs is not available on interpret side add it manually to default config extension list
- [...Object.keys(interpret.extensions), ".cjs"].map((ext) => ({
- path: path.resolve(filename + ext),
- ext: ext,
- module: interpret.extensions[ext],
- })),
- )
- .reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
- let foundDefaultConfigFile;
- for (const defaultConfigFile of defaultConfigFiles) {
- if (!fs.existsSync(defaultConfigFile.path)) {
- continue;
- }
- foundDefaultConfigFile = defaultConfigFile;
- break;
- }
- if (foundDefaultConfigFile) {
- const loadedConfig = await loadConfigByPath(foundDefaultConfigFile.path, options.argv);
- config.options = loadedConfig.options;
- if (Array.isArray(config.options)) {
- config.options.forEach((item) => {
- config.path.set(item, loadedConfig.path);
- });
- } else {
- config.path.set(loadedConfig.options, loadedConfig.path);
- }
- }
- }
開始載入webpack核心模塊,傳入配置選項(xiàng),創(chuàng)建Compiler對(duì)象。
- // 創(chuàng)建Compiler對(duì)象的函數(shù)
- async createCompiler(options, callback) {
- if (typeof options.nodeEnv === "string") {
- process.env.NODE_ENV = options.nodeEnv;
- }
- let config = await this.loadConfig(options);
- config = await this.buildConfig(config, options);
- let compiler;
- try {
- // 開始調(diào)用webpack核心模塊
- compiler = this.webpack(
- config.options,
- callback
- ? (error, stats) => {
- if (error && this.isValidationError(error)) {
- this.logger.error(error.message);
- process.exit(2);
- }
- callback(error, stats);
- }
- : callback,
- );
- } catch (error) {
- if (this.isValidationError(error)) {
- this.logger.error(error.message);
- } else {
- this.logger.error(error);
- }
- process.exit(2);
- }
- // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
- if (compiler && compiler.compiler) {
- compiler = compiler.compiler;
- }
- return compiler;
- }
make階段
make階段主體的目標(biāo)是:根據(jù)entry配置找到入口模塊,開始依次遞歸出所有依賴,形成依賴關(guān)系樹,然后遞歸到的每個(gè)模塊交給不同的loader處理。
- // 多路打包
- if (Array.isArray(options)) {
- await Promise.all(
- options.map(async (_, i) => {
- if (typeof options[i].then === "function") {
- options[i] = await options[i];
- }
- // `Promise` may return `Function`
- if (typeof options[i] === "function") {
- // when config is a function, pass the env from args to the config function
- options[i] = await options[i](argv.env, argv);
- }
- }),
- );
- } else {
- // 單線打包
- if (typeof options.then === "function") {
- options = await options;
- }
- // `Promise` may return `Function`
- if (typeof options === "function") {
- // when config is a function, pass the env from args to the config function
- options = await options(argv.env, argv);
- }
- }
默認(rèn)使用的就是單一入口打包的方式,所以這里最終會(huì)執(zhí)行其中的SingleEntryPlugin。
- SingleEntryPlugin中調(diào)用了Compilation對(duì)象的addEntry方法,開始解析入口。
- addEntry方法中又調(diào)用了_addModuleChain方法,將入口模塊添加到模塊依賴列表。
- 然后通過(guò)Compilation對(duì)象的buildModule方法進(jìn)行模塊構(gòu)建
- buildModule方法中執(zhí)行具體的Loader,處理特殊資源加載
- build完成后,通過(guò)acorn庫(kù)生成模塊代碼的AST語(yǔ)法樹
- 根據(jù)語(yǔ)法樹分析這個(gè)模塊是否還有依賴的模塊,如果有則繼續(xù)循環(huán)build每個(gè)依賴
- 所有依賴解析完成,build階段結(jié)束
- 最后合并生成需要輸出的bundle.js寫入目錄
參考文章
《webpack原理與實(shí)踐》
《webpack中文文檔》
寫在最后
本文主要說(shuō)明了webpack的工作過(guò)程和原理是如何實(shí)現(xiàn)的,并且對(duì)部分源碼進(jìn)行了分析,源碼相當(dāng)于牛津詞典,你不可能專門單獨(dú)設(shè)定時(shí)間去閱讀,而應(yīng)該是需要什么查閱什么,帶著目的性去學(xué)習(xí)。