自动插入external script标签的插件:
const { ExternalModule } = require('webpack');const HtmlWebpackPlugin = require('html-webpack-plugin');class AutoExternalPlugin {constructor(options) {this.options = options;///可以进行外部依赖的模块数组this.externalModules = Object.keys(this.options);//['lodash']//存放着所有的实际用到的外部依赖this.importedModules = new Set();//[]}/*** 1.收集依赖,我需要知道这个项目中一共到底用到了哪些外部依赖模块,放到importedModules里面* 2.拦截生成模块的流程,如果它是一个外部模块话,就不要走原始的打包流程了,而用一个外部模块进行替代* 3.把实用到的依赖模块对应的CDN脚本插入到输出的index.html里面去* @param {*} compiler*/apply(compiler) {//获取普通模块的工厂compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', parser => {//parser会负责把源代码转成AST语法树,并且进行遍历,如果发现了import语句话,就触发回调parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {if (this.externalModules.includes(source)) {this.importedModules.add(source);//如果走到了这里,就表示代码中实际用到了lodash这个模块}});//监听CallExpression语法树节点,如果方法名是require的话parser.hooks.call.for('require').tap('AutoExternalPlugin', (callExpression) => {let source = callExpression.arguments[0].value;if (this.externalModules.includes(source)) {this.importedModules.add(source);//如果走到了这里,就表示代码中实际用到了lodash这个模块}});})normalModuleFactory.hooks.factorize.tapAsync('AutoExternalPlugin', (resolveData, callback) => {let { request } = resolveData;//lodashif (this.importedModules.has(request)) {let { globalVariable } = this.options[request];//_//如果返回的是一个外部模块,则不需要按正常模块生产流程执行callback(null, new ExternalModule(globalVariable));} else {//读取模块源代码,传递给loader再返回JS模块,再解析依赖,再返回此模块callback(null);//NormalModule 普通模块}});});compiler.hooks.compilation.tap('AutoExternalPlugin', (compilation) => {//1.HtmlWebpackPlugin内部会向compilation对象上添加额外的钩子//2.可以通过HtmlWebpackPlugin.getHooks取现这些钩子//3.改变标签HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('AutoExternalPlugin', (htmlData, callback) => {[...this.importedModules].forEach(key => {htmlData.assetTags.scripts.unshift({tagName: 'script',voidTag: false,meta: { plugin: 'html-webpack-plugin' },attributes: { src: this.options[key].url }});});callback(null, htmlData);});});}}module.exports = AutoExternalPlugin;
在第38行,用到了normalModuleFactory对象的factorize钩子,这个钩子是AsyncSeriesBailHook类型的,这种类型的钩子在依次执行各个回调时,会同时检测回调执行的返回值,如果找到了返回值,则后面的回调就都不执行了
盲猜这个钩子是用来获取模块或模块定义的(此处需要再详细地跟踪源码查看),当我们给它一个回调,返回我们自己定义的外部模块的对象定义时,就不会再往后执行其他回调了
