跳到主要内容

external-webpack-plugin

2024年04月06日
柏拉文
越努力,越幸运

一、认识


通常如果在业务代码中,如果我们需要将某些内部依赖模块不进行打包而是使用 externals 形式作为 CDN 进行引入,我们需要经历一下二个步骤:

  1. webpack 配置中进行 externals 配置。

  2. 生成的 html 文件中注入 externals 中的 CDN 配置外部链接

这样存在的问题是: 我们使用需要将依赖模块转变为 CDN 形式的话每次都要在 externals 和生成的 html 文件中进行同步修改,这无疑增加了步骤的繁琐。其次,可能会存在 CDN 冗余加载的问题。可能我并没有使用 lodash 但是并没法保证该项目内其他开发者有没有使用 lodash ,当我在 externals 中配置 lodash 时就必须在 html 文件中引入 lodashCDN 。但其实有可能最终项目内并没有使用 lodash ,但是我们在 html 中仍然冗余的引入了它的 CDN

external-webpack-plugin 功能是:

  1. 基于 externals 配置对应 CDN 地址的 Map 对象, external-webpack-plugin 会动态注入到 HTML

  2. external-webpack-plugin 中通过 AST 抽象抽象语法树的分析保存仅仅在代码中使用到的外部依赖模块,在生成 html 文件时仅注入这些使用到的模块 CDN 链接

external-webpack-plugin 主要逻辑为:

  1. 将匹配到的模块转化为外部依赖: 通过 normalModuleFactory.hooks.factorize 钩子在初始化解析之前阶段, 获取 resolveData 数据, 其中 resolveData.request 为需要解析的依赖模块名 requireModuleName, 我们判断需要解析的模块是否需要被处理成为外部模块, 如果需要处理成为外部模块, 调用 callback() 函数时传入第二个参数 new ExternalModule(模块名, 'window', 全局变量名) 告诉 webpack 这个模块不需要被编译, 直接当做外部依赖处理。

  2. this.usedLibrary 保存代码中使用到的外部依赖库名称: 在进行模块解析时, 通过 normalModuleFactory.hooks.parser 钩子可以获取 Ast 内容。当解析到 import 语句时获得事件函数调用时传入的 source 值,判断当前引入模块是否存在 this.transformLibrary,如果存在,我们将它加入 this.usedLibrary 中去; 当解析到 require 语句时获得事件函数调用时传入的 expression 值,其中模块名为 expression.arguments[0].value, 判断当前引入模块是否存在 this.transformLibrary,如果存在,我们将它加入 this.usedLibrary 中去;

  3. 根据 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'
}
})
]
};