深入webpack打包原理及l(fā)oader和plugin的實(shí)現(xiàn)
本文討論的核心內(nèi)容如下:
webpack進(jìn)行打包的基本原理- 如何自己實(shí)現(xiàn)一個(gè)
loader和plugin
注: 本文使用的 webpack 版本是 v4.43.0 , webpack-cli 版本是 v3.3.11 , node 版本是 v12.14.1 , npm 版本 v6.13.4 (如果你喜歡 yarn 也是可以的),演示用的 chrome 瀏覽器版本 81.0.4044.129(正式版本) (64 位)
1. webpack打包基本原理
webpack的一個(gè)核心功能就是把我們寫(xiě)的模塊化的代碼,打包之后,生成可以在瀏覽器中運(yùn)行的代碼,我們這里也是從簡(jiǎn)單開(kāi)始,一步步探索webpack的打包原理
1.1 一個(gè)簡(jiǎn)單的需求
我們首先建立一個(gè)空的項(xiàng)目,使用 npm init -y 快速初始化一個(gè) package.json ,然后安裝 webpack webpack-cli
接下來(lái),在根目錄下創(chuàng)建 src 目錄, src 目錄下創(chuàng)建 index.js , add.js , minus.js ,根目錄下創(chuàng)建 index.html ,其中 index.html 引入 index.js ,在 index.js 引入 add.js , minus.js ,
目錄結(jié)構(gòu)如下:

文件內(nèi)容如下:
// add.js
export default (a, b) => {
return a + b
}
// minus.js
export const minus = (a, b) => {
return a - b
}
// index.js
import add from './add.js'
import { minus } from './minus.js'
const sum = add(1, 2)
const division = minus(2, 1)
console.log('sum>>>>>', sum)
console.log('division>>>>>', division)
<!--index.html--> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>demo</title> </head> <body> <script src="./src/index.js"></script> </body> </html>
這樣直接在 index.html 引入 index.js 的代碼,在瀏覽器中顯然是不能運(yùn)行的,你會(huì)看到這樣的錯(cuò)誤
Uncaught SyntaxError: Cannot use import statement outside a module
是的,我們不能在 script 引入的 js 文件里,使用 es6 模塊化語(yǔ)法
1.2 實(shí)現(xiàn)webpack打包核心功能
我們首先在項(xiàng)目根目錄下再建立一個(gè)bundle.js,這個(gè)文件用來(lái)對(duì)我們剛剛寫(xiě)的模塊化 js 代碼文件進(jìn)行打包
我們首先來(lái)看webpack官網(wǎng)對(duì)于其打包流程的描述:
it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack會(huì)在內(nèi)部構(gòu)建一個(gè) 依賴(lài)圖(dependency graph),此依賴(lài)圖會(huì)映射項(xiàng)目所需的每個(gè)模塊,并生成一個(gè)或多個(gè) bundle)
在正式開(kāi)始之前,結(jié)合上面 webpack 官網(wǎng)說(shuō)明進(jìn)行分析,明確我們進(jìn)行打包工作的基本流程如下:
首先,我們需要讀到入口文件里的內(nèi)容(也就是index.js的內(nèi)容) 其次,分析入口文件,遞歸的去讀取模塊所依賴(lài)的文件內(nèi)容,生成依賴(lài)圖 最后,根據(jù)依賴(lài)圖,生成瀏覽器能夠運(yùn)行的最終代碼 1. 處理單個(gè)模塊(以入口為例) 1.1 獲取模塊內(nèi)容
既然要讀取文件內(nèi)容,我們需要用到 node.js 的核心模塊 fs ,我們首先來(lái)看讀到的內(nèi)容是什么:
// bundle.js
const fs = require('fs')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
}
getModuleInfo('./src/index.js')
我們定義了一個(gè)方法 getModuleInfo ,這個(gè)方法里我們讀出文件內(nèi)容,打印出來(lái),輸出的結(jié)果如下圖:

我們可以看到,入口文件 index.js 的所有內(nèi)容都以字符串形式輸出了,我們接下來(lái)可以用正則表達(dá)式或者其它一些方法,從中提取到 import 以及 export 的內(nèi)容以及相應(yīng)的路徑文件名,來(lái)對(duì)入口文件內(nèi)容進(jìn)行分析,獲取有用的信息。但是如果 import 和 export 的內(nèi)容非常多,這會(huì)是一個(gè)很麻煩的過(guò)程,這里我們借助 babel
提供的功能,來(lái)完成入口文件的分析
1.2 分析模塊內(nèi)容
我們安裝 @babel/parser ,演示時(shí)安裝的版本號(hào)為 ^7.9.6
這個(gè)babel模塊的作用,就是把我們js文件的代碼內(nèi)容,轉(zhuǎn)換成js對(duì)象的形式,這種形式的js對(duì)象,稱(chēng)做 抽象語(yǔ)法樹(shù)(Abstract Syntax Tree, 以下簡(jiǎn)稱(chēng)AST)
// bundle.js
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
// 表示我們要解析的是es6模塊
sourceType: 'module'
})
console.log(ast)
console.log(ast.program.body)
}
getModuleInfo('./src/index.js')
使用 @babel/parser 的 parse 方法把入口文件轉(zhuǎn)化稱(chēng)為了 AST ,我們打印出了 ast ,注意文件內(nèi)容是在 ast.program.body 中,如下圖所示:

入口文件內(nèi)容被放到一個(gè)數(shù)組中,總共有六個(gè) Node 節(jié)點(diǎn),我們可以看到,每個(gè)節(jié)點(diǎn)有一個(gè) type 屬性,其中前兩個(gè)的 type 屬性是 ImportDeclaration ,這對(duì)應(yīng)了我們?nèi)肟谖募膬蓷l import 語(yǔ)句,并且,每一個(gè) type 屬性是 ImportDeclaration 的節(jié)點(diǎn),其 source.value
屬性是引入這個(gè)模塊的相對(duì)路徑,這樣我們就得到了入口文件中對(duì)打包有用的重要信息了。
接下來(lái)要對(duì)得到的ast做處理,返回一份結(jié)構(gòu)化的數(shù)據(jù),方便后續(xù)使用。
1.3 對(duì)模塊內(nèi)容做處理
對(duì) ast.program.body 部分?jǐn)?shù)據(jù)的獲取和處理,本質(zhì)上就是對(duì)這個(gè)數(shù)組的遍歷,在循環(huán)中做數(shù)據(jù)處理,這里同樣引入一個(gè)babel的模塊 @babel/traverse 來(lái)完成這項(xiàng)工作。
安裝 @babel/traverse ,演示時(shí)安裝的版本號(hào)為 ^7.9.6
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
console.log(deps)
}
getModuleInfo('./src/index.js')
創(chuàng)建一個(gè)對(duì)象 deps ,用來(lái)收集模塊自身引入的依賴(lài),使用 traverse 遍歷 ast ,我們只需要對(duì) ImportDeclaration 的節(jié)點(diǎn)做處理,注意我們做的處理實(shí)際上就是把相對(duì)路徑轉(zhuǎn)化為絕對(duì)路徑,這里我使用的是 Mac 系統(tǒng),如果是 windows 系統(tǒng),注意斜杠的區(qū)別
獲取依賴(lài)之后,我們需要對(duì) ast 做語(yǔ)法轉(zhuǎn)換,把 es6 的語(yǔ)法轉(zhuǎn)化為 es5 的語(yǔ)法,使用 babel 核心模塊 @babel/core 以及 @babel/preset-env 完成
安裝 @babel/core @babel/preset-env ,演示時(shí)安裝的版本號(hào)均為 ^7.9.6
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
console.log(moduleInfo)
return moduleInfo
}
getModuleInfo('./src/index.js')
如下圖所示,我們最終把一個(gè)模塊的代碼,轉(zhuǎn)化為一個(gè)對(duì)象形式的信息,這個(gè)對(duì)象包含文件的絕對(duì)路徑,文件所依賴(lài)模塊的信息,以及模塊內(nèi)部經(jīng)過(guò) babel 轉(zhuǎn)化后的代碼

2. 遞歸的獲取所有模塊的信息
這個(gè)過(guò)程,也就是獲取 依賴(lài)圖(dependency graph) 的過(guò)程,這個(gè)過(guò)程就是從入口模塊開(kāi)始,對(duì)每個(gè)模塊以及模塊的依賴(lài)模塊都調(diào)用 getModuleInfo 方法就行分析,最終返回一個(gè)包含所有模塊信息的對(duì)象
const parseModules = file => {
// 定義依賴(lài)圖
const depsGraph = {}
// 首先獲取入口的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍歷模塊的依賴(lài),遞歸獲取模塊信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
console.log(depsGraph)
return depsGraph
}
parseModules('./src/index.js')
獲得的depsGraph對(duì)象如下圖:

我們最終得到的模塊分析數(shù)據(jù)如上圖所示,接下來(lái),我們就要根據(jù)這里獲得的模塊分析數(shù)據(jù),來(lái)生產(chǎn)最終瀏覽器運(yùn)行的代碼。
3. 生成最終代碼
在我們實(shí)現(xiàn)之前,觀察上一節(jié)最終得到的依賴(lài)圖,可以看到,最終的code里包含exports以及require這樣的語(yǔ)法,所以,我們?cè)谏勺罱K代碼時(shí),要對(duì)exports和require做一定的實(shí)現(xiàn)和處理
我們首先調(diào)用之前說(shuō)的parseModules方法,獲得整個(gè)應(yīng)用的依賴(lài)圖對(duì)象:
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
}
接下來(lái)我們應(yīng)該把依賴(lài)圖對(duì)象中的內(nèi)容,轉(zhuǎn)換成能夠執(zhí)行的代碼,以字符串形式輸出。 我們把整個(gè)代碼放在自執(zhí)行函數(shù)中,參數(shù)是依賴(lài)圖對(duì)象
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
return exports
}
require('${file}')
})(${depsGraph})`
}
接下來(lái)內(nèi)容其實(shí)很簡(jiǎn)單,就是我們?nèi)〉萌肟谖募腸ode信息,去執(zhí)行它就好了,使用eval函數(shù)執(zhí)行,初步寫(xiě)出代碼如下:
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
(function(code){
eval(code)
})(graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
上面的寫(xiě)法是有問(wèn)題的,我們需要對(duì)file做絕對(duì)路徑轉(zhuǎn)化,否則 graph[file].code 是獲取不到的,定義adsRequire方法做相對(duì)路徑轉(zhuǎn)化為絕對(duì)路徑
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
接下來(lái),我們只需要執(zhí)行bundle方法,然后把生成的內(nèi)容寫(xiě)入一個(gè)JavaScript文件即可
const content = bundle('./src/index.js')
// 寫(xiě)入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
最后,我們?cè)趇ndex.html引入這個(gè) ./dist/bundle.js 文件,我們可以看到控制臺(tái)正確輸出了我們想要的結(jié)果

4. bundle.js的完整代碼
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
const ast = parser.parse(body, {
sourceType: 'module'
})
// console.log(ast.program.body)
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
return moduleInfo
}
const parseModules = file => {
// 定義依賴(lài)圖
const depsGraph = {}
// 首先獲取入口的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍歷模塊的依賴(lài),遞歸獲取模塊信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
// console.log(depsGraph)
return depsGraph
}
// 生成最終可以在瀏覽器運(yùn)行的代碼
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}
const build = file => {
const content = bundle(file)
// 寫(xiě)入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
}
build('./src/index.js')
2. 手寫(xiě) loader 和 plugin
2.1 如何自己實(shí)現(xiàn)一個(gè) loader
loader本質(zhì)上就是一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在我們?cè)谖覀兗虞d一些文件時(shí)執(zhí)行
2.1.1 如何實(shí)現(xiàn)一個(gè)同步 loader
首先我們初始化一個(gè)項(xiàng)目,項(xiàng)目結(jié)構(gòu)如圖所示:

其中index.js和webpack.config.js的文件內(nèi)容如下:
// index.js
console.log('我要學(xué)好前端,因?yàn)閷W(xué)好前端可以: ')
// webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
我們?cè)诟夸浵聞?chuàng)建 syncLoader.js ,用來(lái)實(shí)現(xiàn)一個(gè)同步的loader,注意這個(gè)函數(shù)必須返回一個(gè) buffer 或者 string
// syncloader.ja
module.exports = function (source) {
console.log('source>>>>', source)
return source
}
同時(shí),我們?cè)?webpack.config.js 中使用這個(gè) loader ,我們這里使用 resolveLoader 配置項(xiàng),指定 loader 查找文件路徑,這樣我們使用 loader 時(shí)候可以直接指定 loader 的名字
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
resolveLoader: {
// loader路徑查找順序從左往右
modules: ['node_modules', './']
},
module: {
rules: [
{
test: /\.js$/,
use: 'syncLoader'
}
]
}
}
接下來(lái)我們運(yùn)行打包命令,可以看到命令行輸出了source內(nèi)容,也就是loader作用文件的內(nèi)容。

接著我們改造我們的loader:
module.exports = function (source) {
source += '升值加薪'
return source
}
我們?cè)俅芜\(yùn)行打包命令,去觀察打包后的代碼:

這樣,我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的loader,為我們的文件增加一條信息。 我們可以嘗試在 loader 的函數(shù)里打印 this ,發(fā)現(xiàn)輸出結(jié)果是非常長(zhǎng)的一串內(nèi)容, this 上有很多我們可以在 loader 中使用的有用信息,所以,對(duì)于 loader 的編寫(xiě),一定不要使用箭頭函數(shù),那樣會(huì)改變 this
的指向。
一般來(lái)說(shuō),我們會(huì)去使用官方推薦的 loader-utils 包去完成更加復(fù)雜的 loader 的編寫(xiě)
我們繼續(xù)安裝 loader-utils ,版本是 ^2.0.0
我們首先改造 webpack.config.js :
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
resolveLoader: {
// loader路徑查找順序從左往右
modules: ['node_modules', './']
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'syncLoader',
options: {
message: '升值加薪'
}
}
}
]
}
}
注意到,我們?yōu)槲覀兊?loader 增加了 options 配置項(xiàng),接下來(lái)在loader函數(shù)里使用loader-utils獲取配置項(xiàng)內(nèi)容,拼接內(nèi)容,我們依然可以得到與之前一樣的打包結(jié)果
// syncLoader.js
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const options = loaderUtils.getOptions(this)
console.log(options)
source += options.message
// 可以傳遞更詳細(xì)的信息
this.callback(null, source)
}


這樣,我們就完成了一個(gè)簡(jiǎn)單的同步 loader 的編寫(xiě)
2.1.2 如何實(shí)現(xiàn)一個(gè)異步 loader
和同步loader的編寫(xiě)方式非常相似,我們?cè)诟夸浵陆⒁粋€(gè)asyncLoader.js的文件,內(nèi)容如下:
const loaderUtils = require('loader-utils')
module.exports = function (source) {
const options = loaderUtils.getOptions(this)
const asyncfunc = this.async()
setTimeout(() => {
source += '走上人生顛覆'
asyncfunc(null, res)
}, 200)
}
注意這里的 this.async() ,用官方的話來(lái)說(shuō)就是 Tells the loader-runner that the loader intends to call back asynchronously. Returns this.callback. 也就是讓webpack知道這個(gè)loader是異步運(yùn)行,返回的是和同步使用時(shí)一致的 this.callback
接下來(lái)我們修改webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
resolveLoader: {
// loader路徑查找順序從左往右
modules: ['node_modules', './']
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'syncLoader',
options: {
message: '走上人生巔峰'
}
},
{
loader: 'asyncLoader'
}
]
}
]
}
}
注意loader執(zhí)行順序是從下網(wǎng)上的,所以首先為文本寫(xiě)入‘升值加薪',然后寫(xiě)入‘走上人生巔峰'

到此,我們簡(jiǎn)單介紹了如何手寫(xiě)一個(gè) loader ,在實(shí)際項(xiàng)目中,可以考慮一部分公共的簡(jiǎn)單邏輯,可以通過(guò)編寫(xiě)一個(gè) loader 來(lái)完成(比如國(guó)際化文本替換)
2.2 如何自己實(shí)現(xiàn)一個(gè) plugin
plugin 通常是在 webpack 在打包的某個(gè)時(shí)間節(jié)點(diǎn)做一些操作,我們使用 plugin 的時(shí)候,一般都是 new Plugin() 這種形式使用,所以,首先應(yīng)該明確的是, plugin 應(yīng)該是一個(gè)類(lèi)。
我們初始化一個(gè)與上一接實(shí)現(xiàn)loader時(shí)候一樣的項(xiàng)目,根目錄下創(chuàng)建一個(gè) demo-webpack-plugin.js 的文件,我們首先在 webpack.config.js 中使用它
const path = require('path')
const DemoWebpackPlugin = require('./plugins/demo-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new DemoWebpackPlugin()
]
}
再來(lái)看 demo-webpack-plugin.js 的實(shí)現(xiàn)
class DemoWebpackPlugin {
constructor () {
console.log('plugin init')
}
apply (compiler) {
}
}
module.exports = DemoWebpackPlugin
我們?cè)?DemoWebpackPlugin 的構(gòu)造函數(shù)打印一條信息,當(dāng)我們執(zhí)行打包命令時(shí),這條信息就會(huì)輸出, plugin 類(lèi)里面需要實(shí)現(xiàn)一個(gè) apply 方法, webpack 打包時(shí)候,會(huì)調(diào)用 plugin 的 aplly 方法來(lái)執(zhí)行 plugin 的邏輯,這個(gè)方法接受一個(gè) compiler 作為參數(shù),這個(gè) compiler 是 webpack 實(shí)例
plugin的核心在于,apply方法執(zhí)行時(shí),可以操作webpack本次打包的各個(gè)時(shí)間節(jié)點(diǎn)(hooks,也就是生命周期勾子),在不同的時(shí)間節(jié)點(diǎn)做一些操作
關(guān)于webpack編譯過(guò)程的各個(gè)生命周期勾子,可以參考 Compiler Hooks
同樣,這些hooks也有同步和異步之分,下面演示 compiler hooks 的寫(xiě)法,一些重點(diǎn)內(nèi)容可以參考注釋?zhuān)?/p>
class DemoWebpackPlugin {
constructor () {
console.log('plugin init')
}
// compiler是webpack實(shí)例
apply (compiler) {
// 一個(gè)新的編譯(compilation)創(chuàng)建之后(同步)
// compilation代表每一次執(zhí)行打包,獨(dú)立的編譯
compiler.hooks.compile.tap('DemoWebpackPlugin', compilation => {
console.log(compilation)
})
// 生成資源到 output 目錄之前(異步)
compiler.hooks.emit.tapAsync('DemoWebpackPlugin', (compilation, fn) => {
console.log(compilation)
compilation.assets['index.md'] = {
// 文件內(nèi)容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
fn()
})
}
}
module.exports = DemoWebpackPlugin
我們的這個(gè) plugin 的作用就是,打包時(shí)候自動(dòng)生成一個(gè) md 文檔,文檔內(nèi)容是很簡(jiǎn)單的一句話
上述異步hooks的寫(xiě)法也可以是以下兩種:
// 第二種寫(xiě)法(promise)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', (compilation) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
console.log(compilation.assets)
compilation.assets['index.md'] = {
// 文件內(nèi)容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
})
})
// 第三種寫(xiě)法(async await)
compiler.hooks.emit.tapPromise('DemoWebpackPlugin', async (compilation) => {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
})
console.log(compilation.assets)
compilation.assets['index.md'] = {
// 文件內(nèi)容
source: function () {
return 'this is a demo for plugin'
},
// 文件尺寸
size: function () {
return 25
}
}
})
最終的輸出結(jié)果都是一樣的,在每次打包時(shí)候生成一個(gè)md文檔

到此為止,本文介紹了webpack打包的基本原理,以及自己實(shí)現(xiàn)loader和plugin的方法。希望本文內(nèi)容能對(duì)大家對(duì)webpack的學(xué)習(xí),使用帶來(lái)幫助。更多相關(guān)webpack打包loader和plugin內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- vue-cli5.0?webpack?采用?copy-webpack-plugin?打包復(fù)制文件的方法
- vue 解決uglifyjs-webpack-plugin打包出現(xiàn)報(bào)錯(cuò)的問(wèn)題
- html-webpack-plugin修改頁(yè)面的title的方法
- webpack DllPlugin xxx is not defined解決辦法
- 用npm安裝vue和vue-cli,并使用webpack創(chuàng)建項(xiàng)目的方法
- vue項(xiàng)目webpack中Npm傳遞參數(shù)配置不同域名接口
- 詳解node.js中的npm和webpack配置方法
- 如何基于webpack創(chuàng)建plugin并發(fā)布npm包
相關(guān)文章
JavaScript 中如何實(shí)現(xiàn)大文件并行下載
本文將介紹如何利用 async-pool 這個(gè)庫(kù)提供的 asyncPool 函數(shù)來(lái)實(shí)現(xiàn)大文件的并行下載。2021-05-05
uniapp抖音小程序一鍵獲取用戶手機(jī)號(hào)的示例代碼
文章介紹了如何在uniapp抖音小程序中通過(guò)點(diǎn)擊按鈕一鍵獲取用戶手機(jī)號(hào),encryptedData和iv通過(guò)點(diǎn)擊按鈕回傳,后端部分通過(guò)解密獲取手機(jī)號(hào),感興趣的朋友一起看看吧2024-12-12
TypeScript裝飾器與反射元數(shù)據(jù)實(shí)例詳解
TypeScript的裝飾器為我們提供了一種強(qiáng)大的工具,可以在運(yùn)行時(shí)改變類(lèi)的行為,通過(guò)理解裝飾器的工作原理,我們可以創(chuàng)造更加強(qiáng)大、靈活且易于維護(hù)的應(yīng)用,這篇文章主要介紹了TypeScript裝飾器與反射元數(shù)據(jù),需要的朋友可以參考下2023-09-09
獲取JS中網(wǎng)頁(yè)各種高寬與位置的方法總結(jié)
本文詳細(xì)羅列了如何在javascript獲取網(wǎng)頁(yè)各種高寬及位置,內(nèi)容比較全面,有需要的可以參考一下。2016-07-07
JS實(shí)現(xiàn)一個(gè)秒表計(jì)時(shí)器
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)一個(gè)秒表計(jì)時(shí)器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03

