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

