因为要使用cheerio库,需要安装
javascript
npm安装
npm install cheerio --save-dev
或使用 yarn安装
yarn add cheerio --dev
创建async-script-webpack-plugin.js
javascript
const cheerio = require('cheerio');
class AsyncScriptWebpackPlugin {
constructor(options = {}) {
this.options = {
// 默认添加 async 属性
async: true,
defer: false,
// 可选:指定需要添加异步属性的脚本文件名或正则表达式
include: [],
exclude: [],
...options
};
}
apply(compiler) {
// 监听 HTML 生成后的钩子
compiler.hooks.compilation.tap('AsyncScriptWebpackPlugin', (compilation) => {
// 检查是否存在 html-webpack-plugin
const HtmlWebpackPlugin = compiler.options.plugins.find(
(plugin) => plugin.constructor.name === 'HtmlWebpackPlugin'
);
if (!HtmlWebpackPlugin) {
console.warn('[AsyncScriptWebpackPlugin] 未检测到 HtmlWebpackPlugin,插件将不会生效');
return;
}
// 注册钩子到 html-webpack-plugin 处理 HTML 之后
if (compilation.hooks.htmlWebpackPluginAfterHtmlProcessing) {
// 兼容旧版本 html-webpack-plugin
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(
'AsyncScriptWebpackPlugin',
(data, cb) => {
data.html = this.processHtml(data.html);
cb(null, data);
}
);
} else if (compilation.hooks.htmlWebpackPluginAlterAssetTags) {
// 兼容新版本 html-webpack-plugin
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(
'AsyncScriptWebpackPlugin',
(data, cb) => {
data.body = this.processScripts(data.body);
data.head = this.processScripts(data.head);
cb(null, data);
}
);
}
});
}
// 处理 HTML 字符串中的 script 标签
processHtml(html) {
const $ = cheerio.load(html);
$('script').each((i, script) => {
this.modifyScriptTag($, script);
});
return $.html();
}
// 处理 html-webpack-plugin 提供的 script 标签数组
processScripts(scripts) {
return scripts.map((script) => {
if (script.tagName === 'script' && script.attributes && script.attributes.src) {
const src = script.attributes.src;
// 检查是否匹配包含/排除规则
if (this.shouldProcessScript(src)) {
if (this.options.async) script.attributes.async = true;
if (this.options.defer) script.attributes.defer = true;
}
}
return script;
});
}
// 判断是否应该处理某个脚本
shouldProcessScript(src) {
const { include, exclude } = this.options;
// 如果有排除规则且匹配,则不处理
if (exclude.length > 0 && this.matchesAny(src, exclude)) {
return false;
}
// 如果有包含规则且不匹配,则不处理
if (include.length > 0 && !this.matchesAny(src, include)) {
return false;
}
// 默认处理
return true;
}
// 检查字符串是否匹配任何规则(字符串或正则)
matchesAny(str, rules) {
return rules.some((rule) => {
if (typeof rule === 'string') {
return str.includes(rule);
}
if (rule instanceof RegExp) {
return rule.test(str);
}
return false;
});
}
// 修改 script 标签
modifyScriptTag($, script) {
const $script = $(script);
const src = $script.attr('src');
// 如果没有 src 属性,可能是内联脚本,跳过
if (!src) return;
// 检查是否应该处理这个脚本
if (!this.shouldProcessScript(src)) return;
// 添加异步加载属性
if (this.options.async) $script.attr('async', 'async');
if (this.options.defer) $script.attr('defer', 'defer');
}
}
module.exports = AsyncScriptWebpackPlugin;
使用方法
javascript
const AsyncScriptWebpackPlugin = require('./async-script-webpack-plugin');
module.exports = {
// 其他 Webpack 配置...
plugins: [
new AsyncScriptWebpackPlugin({
// 可选配置:
async: true, // 添加 async 属性(默认)
defer: false, // 不添加 defer 属性
include: [ // 只处理匹配的脚本
/vendor\.js$/,
'analytics.js'
],
exclude: [ // 排除匹配的脚本
'critical.js'
]
}),
// 其他插件...
]
};