0%

webpack plugin 实践

背景

项目里有用到inline-manifest-webpack-plugin,当把html-webpack-plugin升级到v4时,插件就报错了。原本想等插件适配,结果发现不维护了。所以自己动手实现一个吧!

webpack 构建流程

webpack的基本构建流程如下:

  1. webpack 初始化: 包括校验配置文件,初始化默认插件,创建Compiler实例等
  2. run: 执行 run 方法
  3. compilation:创建Compilation实例,回调compilation相关钩子
  4. emit:文件内容已经解析完毕,准备生成文件
  5. afterEmit:文件已经写入磁盘
  6. done:完成编译

webpack 插件实例

webpack-plugin需要实现一个apply方法,参数是Compiler实例,下面是一个简单的实例

1
2
3
4
5
6
7
8
9
class MyWebpackPlugin {
constructor(options) {}
apply(compiler) {
compiler.hooks.emit.tap("MyWebpackPlugin", (compilation) => {
console.log("MyWebpackPlugin");
});
}
}
module.exports = MyWebpackPlugin;

只需要在webpack的配置文件中,添加插件就能在控制台看见输出了

1
2
3
4
5
module.exports = {
plugins:[
new MyWebpackPlugin(),
]
};

要实现inline-manifest-webpack-plugin的功能,只需要在合适的hook里面实现我们的逻辑就行了

更多 hook

inline-manifest-webpack-plugin 的作用

将我们选择的chunk文件直接插入到html中,减少请求次数,方便做缓存等,一般用来抽离 webpack 的运行时文件(runtime.js)。它大概实现了一下三个功能:

  1. 删除index.html里的script标签
  2. 将删除标签所对应的内容直接插入index.html
  3. 删除原来的js文件

删除 index.html 里的 script 标签

inline-manifest-webpack-plugin是基于html-webpack-plugin上的插件,html-webpack-plugin为我们提供的 hook 如图所示

Events

在生成标签之前(beforeAssetTagGeneration)去掉指定 chunk 文件

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
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 在标签生成之前
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
const assets = htmlPluginData.assets;
const manifestAssetName = this.getAssetByName(compilation.chunks, this.chunkName);

if (manifestAssetName) {
const runtimeIndex = assets.js.indexOf(assets.publicPath + manifestAssetName);

// 缓存第二步里插入的内容和位子
this.chunkScript = {
tagName: "script",
closeTag: true,
attributes: {
type: "text/javascript",
},
innerHTML: sourceMappingURL.removeFrom(compilation.assets[manifestAssetName].source()),
};
this.chunkIndex = runtimeIndex;

// 从html中删除原标签
if (runtimeIndex !== -1) {
assets.js.splice(runtimeIndex, 1);
delete assets.js[this.chunkName];
}
}
cb(null, htmlPluginData);
}
);
});

将删除标签所对应的内容直接插入 index.html

生成标签的时候把第一步缓存的标签插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 插入标签
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
// 向html中注入
if (this.chunkScript && this.chunkIndex !== -1) {
htmlPluginData.assetTags.scripts.splice(this.chunkIndex, 0, this.chunkScript);
this.chunkScript = null;
this.chunkIndex = -1;
}

cb(null, htmlPluginData);
}
);
});

删除原来的 js 文件

在 webpack 生成所以文件之后,删除原本的文件

1
2
3
4
5
// 在emit阶段插入钩子函数
compiler.hooks.emit.tap("InlineManifestWebpackPlugin", (compilation) => {
// 删除原文件
delete compilation.assets[this.getAssetByName(compilation.chunks, this.chunkName)];
});

此时插件的功能基本上实现完成,一下是完整代码

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const sourceMappingURL = require("source-map-url");
const HtmlWebpackPlugin = require("html-webpack-plugin");

class InlineManifestWebpackPlugin {
constructor(chunkName) {
this.chunkName = chunkName || "runtime";
this.chunkScript = null;
this.chunkIndex = -1;
}

getAssetByName = (chunks, chunkName) => {
return (chunks.find((chunk) => chunk.name === chunkName) || { files: [] }).files[0];
};

apply(compiler) {
// 注入 html-webpack-plugin 的处理过程
compiler.hooks.compilation.tap("InlineManifestWebpackPlugin", (compilation) => {
// 在标签生成之前
HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
const assets = htmlPluginData.assets;
const manifestAssetName = this.getAssetByName(compilation.chunks, this.chunkName);

if (manifestAssetName) {
const runtimeIndex = assets.js.indexOf(assets.publicPath + manifestAssetName);

this.chunkScript = {
tagName: "script",
closeTag: true,
attributes: {
type: "text/javascript",
},
innerHTML: sourceMappingURL.removeFrom(compilation.assets[manifestAssetName].source()),
};
this.chunkIndex = runtimeIndex;

// 从html中删除原标签
if (runtimeIndex !== -1) {
assets.js.splice(runtimeIndex, 1);
delete assets.js[this.chunkName];
}
}
cb(null, htmlPluginData);
}
);

// 插入标签
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
"InlineManifestWebpackPlugin",
(htmlPluginData, cb) => {
// 向html中注入
if (this.chunkScript && this.chunkIndex !== -1) {
htmlPluginData.assetTags.scripts.splice(this.chunkIndex, 0, this.chunkScript);
this.chunkScript = null;
this.chunkIndex = -1;
}

cb(null, htmlPluginData);
}
);
});

// 在emit阶段插入钩子函数
compiler.hooks.emit.tap("InlineManifestWebpackPlugin", (compilation) => {
// 删除原文件
delete compilation.assets[this.getAssetByName(compilation.chunks, this.chunkName)];
});
}
}

module.exports = InlineManifestWebpackPlugin;