Webpack完整打包流程深入分析
前言
webpack
在前端工程領(lǐng)域起到了中流砥柱的作用,理解它的內(nèi)部實(shí)現(xiàn)機(jī)制會(huì)對(duì)你的工程建設(shè)提供很大的幫助(不論是定制功能還是優(yōu)化打包)。
下面我們基于 webpack5 源碼結(jié)構(gòu),對(duì)整個(gè)打包流程進(jìn)行簡(jiǎn)單梳理并進(jìn)行實(shí)現(xiàn),便與思考和理解每個(gè)階段所做的事情,為今后擴(kuò)展和定制工程化能力打下基礎(chǔ)。
一、準(zhǔn)備工作
在流程分析過(guò)程中我們會(huì)簡(jiǎn)單實(shí)現(xiàn) webpack
的一些功能,部分功能的實(shí)現(xiàn)會(huì)借助第三方工具:
tapable
提供 Hooks 機(jī)制來(lái)接入插件進(jìn)行工作;babel
相關(guān)依賴可用于將源代碼解析為 AST,進(jìn)行模塊依賴收集和代碼改寫(xiě)。
// 創(chuàng)建倉(cāng)庫(kù) mkdir webpack-demo && cd webpack-demo && npm init -y // 安裝 babel 相關(guān)依賴 npm install @babel/parser @babel/traverse @babel/types @babel/generator -D // 安裝 tapable(注冊(cè)/觸發(fā)事件流)和 fs-extra 文件操作依賴 npm install tapable fs-extra -D
接下來(lái)我們?cè)?src
目錄下新建兩個(gè)入口文件和一個(gè)公共模塊文件:
mkdir src && cd src && touch entry1.js && touch entry2.js && touch module.js
并分別為文件添加一些內(nèi)容:
// src/entry1.js const module = require('./module'); const start = () => 'start'; start(); console.log('entry1 module: ', module); // src/entry2.js const module = require('./module'); const end = () => 'end'; end(); console.log('entry2 module: ', module); // src/module.js const name = 'cegz'; module.exports = { name, };
有了打包入口,我們?cè)賮?lái)創(chuàng)建一個(gè) webpack.config.js
配置文件做一些基礎(chǔ)配置:
// ./webpack.config.js const path = require('path'); const CustomWebpackPlugin = require('./plugins/custom-webpack-plugin.js'); module.exports = { entry: { entry1: path.resolve(__dirname, './src/entry1.js'), entry2: path.resolve(__dirname, './src/entry2.js'), }, context: process.cwd(), output: { path: path.resolve(__dirname, './build'), filename: '[name].js', }, plugins: [new CustomWebpackPlugin()], resolve: { extensions: ['.js', '.ts'], }, module: { rules: [ { test: /\.js/, use: [ path.resolve(__dirname, './loaders/transformArrowFnLoader.js'), // 轉(zhuǎn)換箭頭函數(shù) ], }, ], }, };
以上配置,指定了兩個(gè)入口文件,以及一個(gè) output.build
輸出目錄,同時(shí)還指定了一個(gè) plugin
和一個(gè) loader
。
接下來(lái)我們編寫(xiě) webpack
的核心入口文件,來(lái)實(shí)現(xiàn)打包邏輯。這里我們創(chuàng)建 webpack 核心實(shí)現(xiàn)所需的文件:
// cd webpack-demo mkdir lib && cd lib touch webpack.js // webpack 入口文件 touch compiler.js // webpack 核心編譯器 touch compilation.js // webpack 核心編譯對(duì)象 touch utils.js // 工具函數(shù)
這里我們創(chuàng)建了兩個(gè)比較相似的文件:compiler
和 compilation
,在這里做下簡(jiǎn)要說(shuō)明:
compiler
:webpack 的編譯器,它提供的run
方法可用于創(chuàng)建compilation
編譯對(duì)象來(lái)處理代碼構(gòu)建工作;compilation
:由compiler.run
創(chuàng)建生成,打包編譯的工作都由它來(lái)完成,并將打包產(chǎn)物移交給compiler
做輸出寫(xiě)入操作。
對(duì)于入口文件 lib/webpack.js
,你會(huì)看到大致如下結(jié)構(gòu):
// lib/webpack.js function webpack(options) { ... } module.exports = webpack;
對(duì)于執(zhí)行入口文件的測(cè)試用例,代碼如下:
// 測(cè)試用例 webpack-demo/build.js const webpack = require('./lib/webpack'); const config = require('./webpack.config'); const compiler = webpack(config); // 調(diào)用run方法進(jìn)行打包 compiler.run((err, stats) => { if (err) { console.log(err, 'err'); } // console.log('構(gòu)建完成!', stats.toJSON()); });
接下來(lái),我們從 lib/webpack.js
入口文件,按照以下步驟開(kāi)始分析打包流程。
1、初始化階段 - webpack
- 合并配置項(xiàng)
- 創(chuàng)建 compiler
- 注冊(cè)插件
2、編譯階段 - build
- 讀取入口文件
- 從入口文件開(kāi)始進(jìn)行編譯
- 調(diào)用 loader 對(duì)源代碼進(jìn)行轉(zhuǎn)換
- 借助 babel 解析為 AST 收集依賴模塊
- 遞歸對(duì)依賴模塊進(jìn)行編譯操作
3、生成階段 - seal
- 創(chuàng)建 chunk 對(duì)象
- 生成 assets 對(duì)象
4、寫(xiě)入階段 - emit
二、初始化階段
初始化階段的邏輯集中在調(diào)用 webpack(config)
時(shí)候,下面我們來(lái)看看 webpack()
函數(shù)體內(nèi)做了哪些事項(xiàng)。
2.1、讀取與合并配置信息
通常,在我們的工程的根目錄下,會(huì)有一個(gè) webpack.config.js
作為 webpack
的配置來(lái)源;
除此之外,還有一種是通過(guò) webpak bin cli 命令進(jìn)行打包時(shí),命令行上攜帶的參數(shù)也會(huì)作為 webpack 的配置。
在配置文件中包含了我們要讓 webpack 打包處理的入口模塊、輸出位置、以及各種 loader、plugin 等;
在命令行上也同樣可以指定相關(guān)的配置,且權(quán)重高于配置文件。(下面將模擬 webpack cli 參數(shù)合并處理)
所以,我們?cè)?webpack 入口文件這里將先做一件事情:合并配置文件與命令行的配置。
// lib/webpack.js function webpack(options) { // 1、合并配置項(xiàng) const mergeOptions = _mergeOptions(options); ... } function _mergeOptions(options) { const shellOptions = process.argv.slice(2).reduce((option, argv) => { // argv -> --mode=production const [key, value] = argv.split('='); if (key && value) { const parseKey = key.slice(2); option[parseKey] = value; } return option; }, {}); return { ...options, ...shellOptions }; } module.exports = webpack;
2.2、創(chuàng)建編譯器(compiler)對(duì)象
好的程序結(jié)構(gòu)離不開(kāi)一個(gè)實(shí)例對(duì)象,webpack 同樣也不甘示弱,其編譯運(yùn)轉(zhuǎn)是由一個(gè)叫做 compiler
的實(shí)例對(duì)象來(lái)驅(qū)動(dòng)運(yùn)轉(zhuǎn)。
在 compiler
實(shí)例對(duì)象上會(huì)記錄我們傳入的配置參數(shù),以及一些串聯(lián)插件進(jìn)行工作的 hooks
API。
同時(shí),還提供了 run
方法啟動(dòng)打包構(gòu)建,emitAssets
對(duì)打包產(chǎn)物進(jìn)行輸出磁盤(pán)寫(xiě)入。這部分內(nèi)容后面介紹。
// lib/webpack.js const Compiler = require('./compiler'); function webpack(options) { // 1、合并配置項(xiàng) const mergeOptions = _mergeOptions(options); // 2、創(chuàng)建 compiler const compiler = new Compiler(mergeOptions); ... return compiler; } module.exports = webpack;
Compiler
構(gòu)造函數(shù)基礎(chǔ)結(jié)構(gòu)如下:
// core/compiler.js const fs = require('fs'); const path = require('path'); const { SyncHook } = require('tapable'); // 串聯(lián) compiler 打包流程的訂閱與通知鉤子 const Compilation = require('./compilation'); // 編譯構(gòu)造函數(shù) class Compiler { constructor(options) { this.options = options; this.context = this.options.context || process.cwd().replace(/\\/g, '/'); this.hooks = { // 開(kāi)始編譯時(shí)的鉤子 run: new SyncHook(), // 模塊解析完成,在向磁盤(pán)寫(xiě)入輸出文件時(shí)執(zhí)行 emit: new SyncHook(), // 在輸出文件寫(xiě)入完成后執(zhí)行 done: new SyncHook(), }; } run(callback) { ... } emitAssets(compilation, callback) { ... } } module.exports = Compiler;
當(dāng)需要進(jìn)行編譯時(shí),調(diào)用 compiler.run
方法即可:
compiler.run((err, stats) => { ... });
2.3、插件注冊(cè)
有 compiler 實(shí)例對(duì)象后,就可以注冊(cè)配置文件中的一個(gè)個(gè)插件,在合適的時(shí)機(jī)來(lái)干預(yù)打包構(gòu)建。
插件需要接收 compiler
對(duì)象作為參數(shù),以此來(lái)對(duì)打包過(guò)程及產(chǎn)物產(chǎn)生 side effect
。
插件的格式可以是函數(shù)或?qū)ο?,如果為?duì)象,需要自定義提供一個(gè) apply
方法。常見(jiàn)的插件結(jié)構(gòu)如下:
class WebpackPlugin { apply(compiler) { ... } }
注冊(cè)插件邏輯如下:
// lib/webpack.js function webpack(options) { // 1、合并配置項(xiàng) const mergeOptions = _mergeOptions(options); // 2、創(chuàng)建 compiler const compiler = new Compiler(mergeOptions); // 3、注冊(cè)插件,讓插件去影響打包結(jié)果 if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); // 當(dāng)插件為函數(shù)時(shí) } else { plugin.apply(compiler); // 如果插件是一個(gè)對(duì)象,需要提供 apply 方法。 } } } return compiler; }
到這里,webpack 的初始工作已經(jīng)完成,接下來(lái)是調(diào)用 compiler.run()
進(jìn)入編譯構(gòu)建階段。
三、編譯階段
編譯工作的起點(diǎn)是在 compiler.run
,它會(huì):
- 發(fā)起構(gòu)建通知,觸發(fā)
hooks.run
通知相關(guān)插件; - 創(chuàng)建
compilation
編譯對(duì)象; - 讀取 entry 入口文件;
- 編譯 entry 入口文件;
3.1、創(chuàng)建 compilation 編譯對(duì)象
模塊的打包(build
)和 代碼生成(seal
)都是由 compilation
來(lái)實(shí)現(xiàn)。
// lib/compiler.js class Compiler { ... run(callback) { // 觸發(fā) run hook this.hooks.run.call(); // 創(chuàng)建 compilation 編譯對(duì)象 const compilation = new Compilation(this); ... } }
compilation
實(shí)例上記錄了構(gòu)建過(guò)程中的 entries
、module
、chunks
、assets
等編譯信息,同時(shí)提供 build
和 seal
方法進(jìn)行代碼構(gòu)建和代碼生成。
// lib/compilation.js const fs = require('fs'); const path = require('path'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generator = require('@babel/generator').default; const t = require('@babel/types'); const { tryExtensions, getSourceCode } = require('./utils'); class Compilation { constructor(compiler) { this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; // 記錄當(dāng)前 module code this.moduleCode = null; // 保存所有依賴模塊對(duì)象 this.modules = new Set(); // 保存所有入口模塊對(duì)象 this.entries = new Map(); // 所有的代碼塊對(duì)象 this.chunks = new Set(); // 存放本次產(chǎn)出的文件對(duì)象(與 chunks 一一對(duì)應(yīng)) this.assets = {}; } build() {} seal() {} }
有了 compilation
對(duì)象后,通過(guò)執(zhí)行 compilation.build
開(kāi)始模塊構(gòu)建。
// lib/compiler.js class Compiler { ... run(callback) { // 觸發(fā) run hook this.hooks.run.call(); // 創(chuàng)建 compilation 編譯對(duì)象 const compilation = new Compilation(this); // 編譯模塊 compilation.build(); } }
3.2、讀取 entry 入口文件
構(gòu)建模塊首先從 entry 入口模塊開(kāi)始,此時(shí)首要工作是根據(jù)配置文件拿到入口模塊信息。
entry 配置的方式多樣化,如:可以不傳(有默認(rèn)值)、可以傳入 string,也可以傳入對(duì)象指定多個(gè)入口。
所以讀取入口文件需要考慮并兼容這幾種靈活配置方式。
// lib/compilation.js class Compilation { ... build() { // 1、讀取配置入口 const entry = this.getEntry(); ... } getEntry() { let entry = Object.create(null); const { entry: optionsEntry } = this.options; if (!optionsEntry) { entry['main'] = 'src/index.js'; // 默認(rèn)找尋 src 目錄進(jìn)行打包 } else if (typeof optionsEntry === 'string') { entry['main'] = optionsEntry; } else { entry = optionsEntry; // 視為對(duì)象,比如多入口配置 } // 相對(duì)于項(xiàng)目啟動(dòng)根目錄計(jì)算出相對(duì)路徑 Object.keys(entry).forEach((key) => { entry[key] = './' + path.posix.relative(this.context, entry[key]); }); return entry; } }
3.3、編譯 entry 入口文件
拿到入口文件后,依次對(duì)每個(gè)入口進(jìn)行構(gòu)建。
// lib/compilation.js class Compilation { ... build() { // 1、讀取配置入口 const entry = this.getEntry(); // 2、構(gòu)建入口模塊 Object.keys(entry).forEach((entryName) => { const entryPath = entry[entryName]; const entryData = this.buildModule(entryName, entryPath); this.entries.set(entryName, entryData); }); } }
構(gòu)建階段執(zhí)行如下操作:
- 通過(guò)
fs
模塊讀取 entry 入口文件內(nèi)容; - 調(diào)用
loader
來(lái)轉(zhuǎn)換(更改)文件內(nèi)容; - 為模塊創(chuàng)建
module
對(duì)象,通過(guò) AST 解析源代碼收集依賴模塊,并改寫(xiě)依賴模塊的路徑; - 如果存在依賴模塊,遞歸進(jìn)行上述三步操作;
讀取文件內(nèi)容:
// lib/compilation.js class Compilation { ... buildModule(moduleName, modulePath) { // 1. 讀取文件原始代碼 const originSourceCode = fs.readFileSync(modulePath, 'utf-8'); this.moduleCode = originSourceCode; ... } }
調(diào)用 loader 轉(zhuǎn)換源代碼:
// lib/compilation.js class Compilation { ... buildModule(moduleName, modulePath) { // 1. 讀取文件原始代碼 const originSourceCode = fs.readFileSync(modulePath, 'utf-8'); this.moduleCode = originSourceCode; // 2. 調(diào)用 loader 進(jìn)行處理 this.runLoaders(modulePath); ... } }
loader
本身是一個(gè) JS 函數(shù),接收模塊文件的源代碼作為參數(shù),經(jīng)過(guò)加工改造后返回新的代碼。
// lib/compilation.js class Compilation { ... runLoaders(modulePath) { const matchLoaders = []; // 1、找到與模塊相匹配的 loader const rules = this.options.module.rules; rules.forEach((loader) => { const testRule = loader.test; if (testRule.test(modulePath)) { // 如:{ test:/\.js$/g, use:['babel-loader'] }, { test:/\.js$/, loader:'babel-loader' } loader.loader ? matchLoaders.push(loader.loader) : matchLoaders.push(...loader.use); } }); // 2. 倒序執(zhí)行 loader for (let i = matchLoaders.length - 1; i >= 0; i--) { const loaderFn = require(matchLoaders[i]); // 調(diào)用 loader 處理源代碼 this.moduleCode = loaderFn(this.moduleCode); } } }
執(zhí)行 webpack 模塊編譯邏輯:
// lib/compilation.js class Compilation { ... buildModule(moduleName, modulePath) { // 1. 讀取文件原始代碼 const originSourceCode = fs.readFileSync(modulePath, 'utf-8'); this.moduleCode = originSourceCode; // 2. 調(diào)用 loader 進(jìn)行處理 this.runLoaders(modulePath); // 3. 調(diào)用 webpack 進(jìn)行模塊編譯 為模塊創(chuàng)建 module 對(duì)象 const module = this.handleWebpackCompiler(moduleName, modulePath); return module; // 返回模塊 } }
- 創(chuàng)建
module
對(duì)象; - 對(duì) module code 解析為
AST
語(yǔ)法樹(shù); - 遍歷 AST 去識(shí)別
require
模塊語(yǔ)法,將模塊收集在module.dependencies
之中,并改寫(xiě)require
語(yǔ)法為__webpack_require__
; - 將修改后的 AST 轉(zhuǎn)換為源代碼;
- 若存在依賴模塊,深度遞歸構(gòu)建依賴模塊。
// lib/compilation.js class Compilation { ... handleWebpackCompiler(moduleName, modulePath) { // 1、創(chuàng)建 module const moduleId = './' + path.posix.relative(this.context, modulePath); const module = { id: moduleId, // 將當(dāng)前模塊相對(duì)于項(xiàng)目啟動(dòng)根目錄計(jì)算出相對(duì)路徑 作為模塊ID dependencies: new Set(), // 存儲(chǔ)該模塊所依賴的子模塊 entryPoint: [moduleName], // 該模塊所屬的入口文件 }; // 2、對(duì)模塊內(nèi)容解析為 AST,收集依賴模塊,并改寫(xiě)模塊導(dǎo)入語(yǔ)法為 __webpack_require__ const ast = parser.parse(this.moduleCode, { sourceType: 'module', }); // 遍歷 ast,識(shí)別 require 語(yǔ)法 traverse(ast, { CallExpression: (nodePath) => { const node = nodePath.node; if (node.callee.name === 'require') { const requirePath = node.arguments[0].value; // 尋找模塊絕對(duì)路徑 const moduleDirName = path.posix.dirname(modulePath); const absolutePath = tryExtensions( path.posix.join(moduleDirName, requirePath), this.options.resolve.extensions, requirePath, moduleDirName ); // 創(chuàng)建 moduleId const moduleId = './' + path.posix.relative(this.context, absolutePath); // 將 require 變成 __webpack_require__ 語(yǔ)句 node.callee = t.identifier('__webpack_require__'); // 修改模塊路徑(參考 this.context 的相對(duì)路徑) node.arguments = [t.stringLiteral(moduleId)]; if (!Array.from(this.modules).find(module => module.id === moduleId)) { // 在模塊的依賴集合中記錄子依賴 module.dependencies.add(moduleId); } else { // 已經(jīng)存在模塊集合中。雖然不添加進(jìn)入模塊編譯 但是仍要在這個(gè)模塊上記錄被依賴的入口模塊 this.modules.forEach((module) => { if (module.id === moduleId) { module.entryPoint.push(moduleName); } }); } } }, }); // 3、將 ast 生成新代碼 const { code } = generator(ast); module._source = code; // 4、深度遞歸構(gòu)建依賴模塊 module.dependencies.forEach((dependency) => { const depModule = this.buildModule(moduleName, dependency); // 將編譯后的任何依賴模塊對(duì)象加入到 modules 對(duì)象中去 this.modules.add(depModule); }); return module; } }
通常我們 require 一個(gè)模塊文件時(shí)習(xí)慣不去指定文件后綴,默認(rèn)會(huì)查找 .js 文件。
這跟我們?cè)谂渲梦募兄付ǖ?resolve.extensions
配置有關(guān),在 tryExtensions
方法中會(huì)嘗試為每個(gè)未填寫(xiě)后綴的 Path 應(yīng)用 resolve.extensions
:
// lib/utils.js const fs = require('fs'); function tryExtensions( modulePath, extensions, originModulePath, moduleContext ) { // 優(yōu)先嘗試不需要擴(kuò)展名選項(xiàng)(用戶如果已經(jīng)傳入了后綴,那就使用用戶填入的,無(wú)需再應(yīng)用 extensions) extensions.unshift(''); for (let extension of extensions) { if (fs.existsSync(modulePath + extension)) { return modulePath + extension; } } // 未匹配對(duì)應(yīng)文件 throw new Error( `No module, Error: Can't resolve ${originModulePath} in ${moduleContext}` ); } module.exports = { tryExtensions, ... }
至此,「編譯階段」到此結(jié)束,接下來(lái)是「生成階段」 seal
。
四、生成階段
在「編譯階段」會(huì)將一個(gè)個(gè)文件構(gòu)建成 module
存儲(chǔ)在 this.modules
之中。
在「生成階段」,會(huì)根據(jù) entry
創(chuàng)建對(duì)應(yīng) chunk
并從 this.modules
中查找被 entry
所依賴的 module
集合。
最后,結(jié)合 runtime
webpack 模塊機(jī)制運(yùn)行代碼,經(jīng)過(guò)拼接生成最終的 assets
產(chǎn)物。
// lib/compiler.js class Compiler { ... run(callback) { // 觸發(fā) run hook this.hooks.run.call(); // 創(chuàng)建 compilation 編譯對(duì)象 const compilation = new Compilation(this); // 編譯模塊 compilation.build(); // 生成產(chǎn)物 compilation.seal(); ... } }
entry + module
--> chunk
--> assets
過(guò)程如下:
// lib/compilation.js class Compilation { ... seal() { // 1、根據(jù) entry 創(chuàng)建 chunk this.entries.forEach((entryData, entryName) => { // 根據(jù)當(dāng)前入口文件和模塊的相互依賴關(guān)系,組裝成為一個(gè)個(gè)包含當(dāng)前入口所有依賴模塊的 chunk this.createChunk(entryName, entryData); }); // 2、根據(jù) chunk 創(chuàng)建 assets this.createAssets(); } // 根據(jù)入口文件和依賴模塊組裝chunks createChunk(entryName, entryData) { const chunk = { // 每一個(gè)入口文件作為一個(gè) chunk name: entryName, // entry build 后的數(shù)據(jù)信息 entryModule: entryData, // entry 的所依賴模塊 modules: Array.from(this.modules).filter((i) => i.entryPoint.includes(entryName) ), }; // add chunk this.chunks.add(chunk); } createAssets() { const output = this.options.output; // 根據(jù) chunks 生成 assets this.chunks.forEach((chunk) => { const parseFileName = output.filename.replace('[name]', chunk.name); // 為每一個(gè) chunk 文件代碼拼接 runtime 運(yùn)行時(shí)語(yǔ)法 this.assets[parseFileName] = getSourceCode(chunk); }); } }
getSourceCode
是將 entry
和 modules
組合而成的 chunk
,接入到 runtime
代碼模板之中。
// lib/utils.js function getSourceCode(chunk) { const { entryModule, modules } = chunk; return ` (() => { var __webpack_modules__ = { ${modules .map((module) => { return ` '${module.id}': (module) => { ${module._source} } `; }) .join(',')} }; var __webpack_module_cache__ = {}; function __webpack_require__(moduleId) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = (__webpack_module_cache__[moduleId] = { exports: {}, }); __webpack_modules__[moduleId](module, module.exports, __webpack_require__); return module.exports; } (() => { ${entryModule._source} })(); })(); `; }
到這里,「生成階段」處理完成,這也意味著 compilation
編譯工作的完成,接下來(lái)我們回到 compiler
進(jìn)行最后的「產(chǎn)物輸出」。
五、寫(xiě)入階段
「寫(xiě)入階段」比較容易理解,assets
上已經(jīng)擁有了最終打包后的代碼內(nèi)容,最后要做的就是將代碼內(nèi)容寫(xiě)入到本地磁盤(pán)之中。
// lib/compiler.js class Compiler { ... run(callback) { // 觸發(fā) run hook this.hooks.run.call(); // 創(chuàng)建 compilation 編譯對(duì)象 const compilation = new Compilation(this); // 編譯模塊 compilation.build(); // 生成產(chǎn)物 compilation.seal(); // 輸出產(chǎn)物 this.emitAssets(compilation, callback); } emitAssets(compilation, callback) { const { entries, modules, chunks, assets } = compilation; const output = this.options.output; // 調(diào)用 Plugin emit 鉤子 this.hooks.emit.call(); // 若 output.path 不存在,進(jìn)行創(chuàng)建 if (!fs.existsSync(output.path)) { fs.mkdirSync(output.path); } // 將 assets 中的內(nèi)容寫(xiě)入文件系統(tǒng)中 Object.keys(assets).forEach((fileName) => { const filePath = path.join(output.path, fileName); fs.writeFileSync(filePath, assets[fileName]); }); // 結(jié)束之后觸發(fā)鉤子 this.hooks.done.call(); callback(null, { toJSON: () => { return { entries, modules, chunks, assets, }; }, }); } }
至此,webpack 的打包流程就以完成。
接下來(lái)我們完善配置文件中未實(shí)現(xiàn)的 loader
和 plugin
,然后調(diào)用測(cè)試用例,測(cè)試一下上面的實(shí)現(xiàn)。
六、編寫(xiě) loader
在 webpack.config.js
中我們?yōu)?.js
文件類(lèi)型配置了一個(gè)自定義 loader 來(lái)轉(zhuǎn)換文件內(nèi)容:
// webpack.config.js module: { rules: [ { test: /\.js/, use: [ path.resolve(__dirname, './loaders/transformArrowFnLoader.js'), ], }, ], },
loader 本身是一個(gè)函數(shù),接收文件模塊內(nèi)容作為參數(shù),經(jīng)過(guò)改造處理返回新的文件內(nèi)容。
下面我們?cè)?loaders/transformArrowFnLoader.js
中,對(duì)文件中使用到的箭頭函數(shù),轉(zhuǎn)換為普通函數(shù),來(lái)理解 webpack loader
的作用。
// loaders/transformArrowFnLoader.js const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generator = require('@babel/generator').default; const t = require('@babel/types'); function transformArrowLoader(sourceCode) { const ast = parser.parse(sourceCode, { sourceType: 'module' }); traverse(ast, { ArrowFunctionExpression(path, state) { const node = path.node; const body = path.get('body'); const bodyNode = body.node; if (bodyNode.type !== 'BlockStatement') { const statements = []; statements.push(t.returnStatement(bodyNode)); node.body = t.blockStatement(statements); } node.type = "FunctionExpression"; } }); const { code } = generator(ast); return code; } module.exports = transformArrowLoader;
最終,箭頭函數(shù)經(jīng)過(guò)處理后變成如下結(jié)構(gòu):
const start = () => 'start'; || || const start = function () { return 'start'; };
七、編寫(xiě)插件
從上面介紹我們了解到,每個(gè)插件都需要提供一個(gè) apply
方法,此方法接收 compiler
作為參數(shù)。
通過(guò) compiler
可以去訂閱 webpack
工作期間不同階段的 hooks
,以此來(lái)影響打包結(jié)果或者做一些定制操作。
下面我們編寫(xiě)自定義插件,綁定兩個(gè)不同時(shí)機(jī)的 compiler.hooks
來(lái)擴(kuò)展 webpack 打包功能:
hooks.emit.tap
綁定一個(gè)函數(shù),在webpack
編譯資源完成,輸出寫(xiě)入磁盤(pán)前執(zhí)行(可以做清除output.path
目錄操作);hooks.done.tap
綁定一個(gè)函數(shù),在webpack
寫(xiě)入磁盤(pán)完成之后執(zhí)行(可以做一些靜態(tài)資源copy
操作)。
// plugins/custom-webpack-plugins const fs = require('fs-extra'); const path = require('path'); class CustomWebpackPlugin { apply(compiler) { const outputPath = compiler.options.output.path; const hooks = compiler.hooks; // 清除 build 目錄 hooks.emit.tap('custom-webpack-plugin', (compilation) => { fs.removeSync(outputPath); }); // copy 靜態(tài)資源 const otherFilesPath = path.resolve(__dirname, '../src/otherfiles'); hooks.done.tap('custom-webpack-plugin', (compilation) => { fs.copySync(otherFilesPath, path.resolve(outputPath, 'otherfiles')); }); } } module.exports = CustomWebpackPlugin;
現(xiàn)在,我們通過(guò) node build.js
運(yùn)行文件,最終會(huì)在 webpack-demo
下生成 build
目錄以及入口打包資源。
文末
到此這篇關(guān)于Webpack完整打包流程的文章就介紹到這了,更多相關(guān)Webpack打包流程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS實(shí)現(xiàn)把鼠標(biāo)放到鏈接上出現(xiàn)滾動(dòng)文字的方法
這篇文章主要介紹了JS實(shí)現(xiàn)把鼠標(biāo)放到鏈接上出現(xiàn)滾動(dòng)文字的方法,涉及JavaScript響應(yīng)鼠標(biāo)事件動(dòng)態(tài)操作頁(yè)面元素的相關(guān)技巧,需要的朋友可以參考下2016-04-04js實(shí)現(xiàn)一個(gè)簡(jiǎn)易計(jì)算器
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)一個(gè)簡(jiǎn)易計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07TypeError: Cannot set properties of 
這篇文章主要介紹了TypeError: Cannot set properties of undefined (setting ‘xx‘)的問(wèn)題,本文給大家分享完美解決方案,需要的朋友可以參考下2023-09-09js創(chuàng)建表單元素并使用submit進(jìn)行提交
這篇文章主要介紹了js創(chuàng)建表單元素并使用submit進(jìn)行提交,需要的朋友可以參考下2014-08-08Cordova(ionic)項(xiàng)目實(shí)現(xiàn)雙擊返回鍵退出應(yīng)用
這篇文章主要為大家詳細(xì)介紹了Cordova項(xiàng)目實(shí)現(xiàn)雙擊返回鍵退出應(yīng)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09CocosCreator通用框架設(shè)計(jì)之資源管理
這篇文章主要介紹了CocosCreator通用框架設(shè)計(jì)之資源管理,對(duì)性能優(yōu)化感興趣的同學(xué),一定要看一下2021-04-04javascript eval函數(shù)深入認(rèn)識(shí)
發(fā)現(xiàn)為本文起一個(gè)合適的標(biāo)題還不是那么容易,呵呵,所以在此先說(shuō)明下本文的兩個(gè)目的2009-02-02