在 webpack 中,loader 和 plugin 的区分是很清楚的,针对文件模块转换要做的使用 loader,而其他构建过程,干涉构建内容的可以使用 plugin。
24 个实例入门并掌握「Webpack4」(一)
24 个实例入门并掌握「Webpack4」(二)
24 个实例入门并掌握「Webpack4」(三)
编写一个 loader
webpack-plugin webpack-loader大全
新建文件夹,npm init -y
,npm i webpack webpack-cli -D
,新建 src/index.js,写入 console.log('hello world')
新建 loaders/replaceLoader.js
文件1
2
3module.exports = function(source) {
return source.replace('world', 'loader')
}
source 参数就是我们的源代码,这里是将源码中的 world 替换成 loader
新建 webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /.js/,
use: [path.resolve(__dirname, './loaders/replaceLoader.js')] // 引入自定义 loader
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
目录结构:
打包后打开 dist/main.js 文件,在最底部可以看到 world 已经被改为了 loader,一个最简单的 loader 就写完了
添加 optiions 属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'xh'
}
}
]
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
修改 replaceLoader.js 文件,保存后打包,输出看看效果1
2
3
4module.exports = function(source) {
console.log(this.query)
return source.replace('world', this.query.name)
}
打包后生成的文件也改为了 options 中定义的 name
更多的配置见官网 API,找到 Loader Interface,里面有个 this.query
如果你的 options 不是一个对象,而是按字符串形式写的话,可能会有一些问题,这里官方推荐使用 loader-utils 来获取 options 中的内容
充分利用 loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。
安装 npm i loader-utils -D
,修改 replaceLoader.js1
2
3
4
5
6
7const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
console.log(options)
return source.replace('world', options.name)
}
console.log(options)
与 console.log(this.query)
输出内容一致
如果你想传递额外的信息出去,return 就不好用了,官网给我们提供了 this.callback API,用法如下1
2
3
4
5
6this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
)
修改 replaceLoader.js1
2
3
4
5
6
7
8const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const result = source.replace('world', options.name)
this.callback(null, result)
}
目前没有用到 sourceMap(必须是此模块可解析的源映射)、meta(可以是任何内容(例如一些元数据)) 这两个可选参数,只将 result 返回回去,保存重新打包后,效果和 return 是一样的
如果在 loader 中写异步代码,会怎么样1
2
3
4
5
6
7
8
9
10const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
setTimeout(() => {
const result = source.replace('world', options.name)
return result
}, 1000)
}
报错 loader 没有返回,这里使用 this.async 来写异步代码1
2
3
4
5
6
7
8
9
10
11
12const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const callback = this.async()
setTimeout(() => {
const result = source.replace('world', options.name)
callback(null, result)
}, 1000)
}
模拟一个同步 loader 和一个异步 loader
新建一个 replaceLoaderAsync.js
文件,将之前写的异步代码放入,修改 replaceLoader.js
为同步代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// replaceLoaderAsync.js
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const callback = this.async()
setTimeout(() => {
const result = source.replace('world', options.name)
callback(null, result)
}, 1000)
}
// replaceLoader.js
module.exports = function(source) {
return source.replace('xh', 'world')
}
修改 webpack.config.js
,loader 的执行顺序是从下到上,先执行异步代码,将 world 改为 xh,再执行同步代码,将 xh 改为 world1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18module: {
rules: [
{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js')
},
{
loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
options: {
name: 'xh'
}
}
]
}
]
}
保存后打包,在 mian.js 中可以看到已经改为了 hello world
,使用多个 loader 也完成了
如果有多个自定义 loader,每次都通过 path.resolve(__dirname, xxx)
这种方式去写,有没有更好的方法?
使用 resolveLoader
,定义 modules,当你使用 loader 的时候,会先去 node_modules
中去找,如果没找到就会去 ./loaders
中找1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [
{
test: /.js/,
use: [
{
loader: 'replaceLoader.js'
},
{
loader: 'replaceLoaderAsync.js',
options: {
name: 'xh'
}
}
]
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
编写一个 plugin
使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。
首先新建一个文件夹,npm 起手式操作一番,具体的在前几节已经说了,不再赘述
在根目录下新建 plugins 文件夹,新建 copyright-webpack-plugin.js
,一般我们用的都是 xxx-webpack-plugin
,所以我们命名也按这样来,plugin 的定义是一个类1
2
3
4
5
6
7
8classCopyrightWebpackPlugin{
constructor() {
console.log('插件被使用了')
}
apply(compiler) {}
}
module.exports = CopyrightWebpackPlugin
在 webpack.config.js 中使用,所以每次使用 plugin 都要使用 new,因为本质上 plugin 是一个类1
2
3
4
5
6
7
8
9
10
11
12
13
14const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [new CopyrightWebpackPlugin()],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
保存后打包,插件被使用了,只不过我们什么都没干
如果我们要传递参数,可以这样1
2
3new CopyrightWebpackPlugin({
name: 'xh'
})
同时在 copyright-webpack-plugin.js
中接收1
2
3
4
5
6
7
8
9classCopyrightWebpackPlugin{
constructor(options) {
console.log('插件被使用了')
console.log('options = ', options)
}
apply(compiler) {}
}
module.exports = CopyrightWebpackPlugin
我们先把 constructor 注释掉,在即将要把打包的结果,放入 dist 目录之前的这个时刻,我们来做一些操作
apply(compiler) {}
compiler 可以看作是 webpack 的实例,具体见官网 compiler-hooks
hooks 是钩子,像 vue、react 的生命周期一样,找到 emit
这个时刻,将打包结果放入 dist 目录前执行,这里是个 AsyncSeriesHook
异步方法1
2
3
4
5
6
7
8
9
10
11
12
13class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, cb) => {
console.log(11)
cb()
}
)
}
}
module.exports = CopyrightWebpackPlugin
因为 emit 是异步的,可以通过 tapAsync 来写,当要把代码放入到 dist 目录之前,就会触发这个钩子,走到我们定义的函数里,如果你用 tapAsync 函数,记得最后要用 cb() ,tapAsync 要传递两个参数,第一个参数传递我们定义的插件名称
保存后再次打包,我们写的内容也输出了
compilation 这个参数里存放了这次打包的所有内容,可以输出一下 compilation.assets
看一下
返回结果是一个对象,main.js
是 key,也就是打包后生成的文件名及文件后缀,我们可以来仿照一下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22classCopyrightWebpackPlugin{
apply(compiler) {
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, cb) => {
// 生成一个 copyright.txt 文件
compilation.assets['copyright.txt'] = {
source: function() {
return'copyright by xh'
},
size: function() {
return15// 上面 source 返回的字符长度
}
}
console.log('compilation.assets = ', compilation.assets)
cb()
}
)
}
}
module.exports = CopyrightWebpackPlugin
在 dist 目录下生成了 copyright.txt
文件
之前介绍的是异步钩子,现在使用同步钩子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27classCopyrightWebpackPlugin{
apply(compiler) {
// 同步钩子
compiler.hooks.compile.tap('CopyrightWebpackPlugin', compilation => {
console.log('compile')
})
// 异步钩子
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, cb) => {
compilation.assets['copyright.txt'] = {
source: function() {
return'copyright by xh'
},
size: function() {
return15// 字符长度
}
}
console.log('compilation.assets = ', compilation.assets)
cb()
}
)
}
}
module.exports = CopyrightWebpackPlugin
参考博客
写一个插件,去除webpack打包生成js中多余的注释
怎么写一个插件?
参照webpack官方教程Writing a Plugin
一个webpack plugin由一下几个步骤组成:
- 一个JavaScript类函数。
- 在函数原型 (prototype)中定义一个注入
compiler
对象的apply
方法。 apply
函数中通过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象- 使用compilation操纵修改webapack内部实例数据。
- 异步插件,数据处理完后使用callback回调
完成插件初始架构
在之前说Tapable的时候,写了一个MyPlugin类函数,它已经满足了webpack plugin结构的前两点(一个JavaScript类函数,在函数原型 (prototype)中定义一个注入compiler
)
现在我们要让Myplugin满足后三点。首先,使用compiler指定的事件钩子。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyPlugin{
constructor() {
}
apply(conpiler){
conpiler.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));
conpiler.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
conpiler.hooks.calculateRoutes.tapAsync("calculateRoutes tapAsync", (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source}${target}${routesList}`)
callback();
}, 2000)
});
}
}
编写插件
1 | class MyPlugin { |
第一步,使用compiler的emit钩子
emit事件是将编译好的代码发射到指定的stream中触发,在这个钩子执行的时候,我们能从回调函数返回的compilation对象上拿到编译好的stream。1
compiler.hooks.emit.tap('xxx',(compilation)=>{})
第二步,访问compilation对象,我们用绑定提供了编译 compilation 引用的emit钩子函数,每一次编译都会拿到新的 compilation 对象。这些 compilation 对象提供了一些钩子函数,来钩入到构建流程的很多步骤中。
其中,我们需要的是compilation.assets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57assetsCompilation {
assets:
{ 'js/index/main.js':
CachedSource {
_source: [Object],
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {} } },
errors: [],
warnings: [],
children: [],
dependencyFactories:
ArrayMap {
keys:
[ [Object],
[Function: MultiEntryDependency],
[Function: SingleEntryDependency],
[Function: LoaderDependency],
[Object],
[Function: ContextElementDependency],
values:
[ NullFactory {},
[Object],
NullFactory {} ] },
dependencyTemplates:
ArrayMap {
keys:
[ [Object],
[Object],
[Object] ],
values:
[ ConstDependencyTemplate {},
RequireIncludeDependencyTemplate {},
NullDependencyTemplate {},
RequireEnsureDependencyTemplate {},
ModuleDependencyTemplateAsRequireId {},
AMDRequireDependencyTemplate {},
ModuleDependencyTemplateAsRequireId {},
AMDRequireArrayDependencyTemplate {},
ContextDependencyTemplateAsRequireCall {},
AMDRequireDependencyTemplate {},
LocalModuleDependencyTemplate {},
ModuleDependencyTemplateAsId {},
ContextDependencyTemplateAsRequireCall {},
ModuleDependencyTemplateAsId {},
ContextDependencyTemplateAsId {},
RequireResolveHeaderDependencyTemplate {},
RequireHeaderDependencyTemplate {} ] },
fileTimestamps: {},
contextTimestamps: {},
name: undefined,
_currentPluginApply: undefined,
fullHash: 'f4030c2aeb811dd6c345ea11a92f4f57',
hash: 'f4030c2aeb811dd6c345',
fileDependencies: [ '/Users/mac/web/src/js/index/main.js' ],
contextDependencies: [],
missingDependencies: [] }
优化所有 chunk 资源(asset)。资源(asset)会以key-value的形式被存储在
compilation.assets
。
第三步,遍历assets。
1)assets数组对象中的key是资源名,在Myplugin插件中,遍历Object.key()我们拿到了1
2
3main.css
bundle.js
index.html
2)调用Object.source() 方法,得到资源的内容1
compilation.assets[data].source()
3)用正则,去除注释1
2
3
4
5
6 Object.keys(compilation.assets).forEach((data)=> {
let content = compilation.assets[data].source()
content = content.replace(reg, function (word) {
return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word;
})
});
第四步,更新compilation.assets[data]对象
1 | compilation.assets[data] = { |
第五步 在webpack中引用插件
webpack.config.js
1 | const path = require('path') |