external-webpack-plugin
一、认识
通常如果在业务代码中,如果我们需要将某些内部依赖模块不进行打包而是使用 externals
形式作为 CDN
进行引入,我们需要经历一下二个步骤:
-
webpack
配置中进行externals
配置。 -
生成的
html
文件中注入externals
中的CDN
配置外部链接
这样存在的问题是: 我们使用需要将依赖模块转变为 CDN
形式的话每次都要在 externals
和生成的 html
文件中进行同步修改,这无疑增加了步骤的繁琐。其次,可能会存在 CDN
冗余加载的问题。可能我并没有使用 lodash
但是并没法保证该项目内其他开发者有没有使用 lodash
,当我在 externals
中配置 lodash
时就必须在 html
文件中引入 lodash
的CDN
。但其实有可能最终项目内并没有使用 lodash
,但是我们在 html
中仍然冗余的引入了它的 CDN
。
external-webpack-plugin
功能是:
-
基于
externals
配置对应CDN
地址的Map
对象,external-webpack-plugin
会动态注入到HTML
中 -
在
external-webpack-plugin
中通过AST
抽象抽象语法树的分析保存仅仅在代码中使用到的外部依赖模块,在生成html
文件时仅注入这些使用到的模块CDN
链接
external-webpack-plugin
主要逻辑为:
-
将匹配到的模块转化为外部依赖: 通过
normalModuleFactory.hooks.factorize
钩子在初始化解析之前阶段, 获取resolveData
数据, 其中resolveData.request
为需要解析的依赖模块名requireModuleName
, 我们判断需要解析的模块是否需要被处理成为外部模块, 如果需要处理成为外部模块, 调用callback()
函数时传入第二个参数new ExternalModule(模块名, 'window', 全局变量名)
告诉webpack
这个模块不需要被编译, 直接当做外部依赖处理。 -
this.usedLibrary
保存代码中使用到的外部依赖库名称: 在进行模块解析时, 通过normalModuleFactory.hooks.parser
钩子可以获取Ast
内容。当解析到import
语句时获得事件函数调用时传入的source
值,判断当前引入模块是否存在this.transformLibrary
,如果存在,我们将它加入this.usedLibrary
中去; 当解析到require
语句时获得事件函数调用时传入的expression
值,其中模块名为expression.arguments[0].value
, 判断当前引入模块是否存在this.transformLibrary
,如果存在,我们将它加入this.usedLibrary
中去; -
根据
this.usedLibrary
的内容在生成最终的html
文件时插入对应使用到的模块的CDN
外部链接: 通过HtmlWebpackPlugin
提供给compilation
对象的额外拓展钩子alterAssetTags
循环this.usedLibrary
进而循环添加外部CDN
链接进入HTML
中
二、实现
const pluginName = 'ExternalWebpackPlugin';
const { ExternalModule } = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
function importHandler(parser) {
parser.hooks.import.tap(pluginName, (statement, source) => {
if (this.transformLibrary.includes(source)) {
this.usedLibrary.add(source);
}
});
}
function requireHandler(parser) {
parser.hooks.call.for('require').tap(pluginName, expression => {
const moduleName = expression.arguments[0].value;
if (this.transformLibrary.includes(moduleName)) {
this.usedLibrary.add(moduleName);
}
});
}
class ExternalWebpackPlugin {
constructor(options) {
this.options = options;
this.usedLibrary = new Set();
this.transformLibrary = Object.keys(options);
}
apply(compiler) {
compiler.hooks.normalModuleFactory.tap(pluginName, normalModuleFactory => {
normalModuleFactory.hooks.factorize.tapAsync(
pluginName,
(resolveData, callback) => {
const requireModuleName = resolveData.request;
if (this.transformLibrary.includes(requireModuleName)) {
const externalModuleName =
this.options[requireModuleName].variableName;
callback(
null,
new ExternalModule(
externalModuleName,
'window',
externalModuleName
)
);
} else {
callback();
}
}
);
normalModuleFactory.hooks.parser
.for('javascript/auto')
.tap(pluginName, parser => {
importHandler.call(this, parser);
requireHandler.call(this, parser);
});
});
compiler.hooks.compilation.tap(pluginName, compilation => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tap(
pluginName,
data => {
const scriptTag = data.assetTags.scripts;
this.usedLibrary.forEach(library => {
scriptTag.unshift({
voidTag: false,
tagName: 'script',
attributes: {
defer: true,
type: undefined,
src: this.options[library].src
},
meta: { plugin: pluginName }
});
});
}
);
});
}
}
module.exports = ExternalWebpackPlugin;
三、测试
3.1 package.json
"external-webpack": "webpack --config ./plugins/externalWebpackPlugin/example/webpack.config.js"
3.2 webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExternalsWebpackPlugin = require('../core/index.js');
module.exports = {
devtool: false,
mode: 'development',
entry: {
main: path.resolve(__dirname, './index.js')
},
externals: {
vue: 'Vue',
lodash: '_',
jquery: '$'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin(),
new ExternalsWebpackPlugin({
vue: {
variableName: 'Vue',
src: 'https://cdn.jsdelivr.net/npm/vue@3.3.11/dist/vue.global.min.js'
},
lodash: {
variableName: '_',
src: 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js'
},
jquery: {
variableName: '$',
src: 'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js'
}
})
]
};