自动插入external script标签的插件:

    1. const { ExternalModule } = require('webpack');
    2. const HtmlWebpackPlugin = require('html-webpack-plugin');
    3. class AutoExternalPlugin {
    4. constructor(options) {
    5. this.options = options;
    6. ///可以进行外部依赖的模块数组
    7. this.externalModules = Object.keys(this.options);//['lodash']
    8. //存放着所有的实际用到的外部依赖
    9. this.importedModules = new Set();//[]
    10. }
    11. /**
    12. * 1.收集依赖,我需要知道这个项目中一共到底用到了哪些外部依赖模块,放到importedModules里面
    13. * 2.拦截生成模块的流程,如果它是一个外部模块话,就不要走原始的打包流程了,而用一个外部模块进行替代
    14. * 3.把实用到的依赖模块对应的CDN脚本插入到输出的index.html里面去
    15. * @param {*} compiler
    16. */
    17. apply(compiler) {
    18. //获取普通模块的工厂
    19. compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {
    20. normalModuleFactory.hooks.parser
    21. .for('javascript/auto')
    22. .tap('AutoExternalPlugin', parser => {
    23. //parser会负责把源代码转成AST语法树,并且进行遍历,如果发现了import语句话,就触发回调
    24. parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
    25. if (this.externalModules.includes(source)) {
    26. this.importedModules.add(source);//如果走到了这里,就表示代码中实际用到了lodash这个模块
    27. }
    28. });
    29. //监听CallExpression语法树节点,如果方法名是require的话
    30. parser.hooks.call.for('require').tap('AutoExternalPlugin', (callExpression) => {
    31. let source = callExpression.arguments[0].value;
    32. if (this.externalModules.includes(source)) {
    33. this.importedModules.add(source);//如果走到了这里,就表示代码中实际用到了lodash这个模块
    34. }
    35. });
    36. })
    37. normalModuleFactory.hooks.factorize.tapAsync('AutoExternalPlugin', (resolveData, callback) => {
    38. let { request } = resolveData;//lodash
    39. if (this.importedModules.has(request)) {
    40. let { globalVariable } = this.options[request];//_
    41. //如果返回的是一个外部模块,则不需要按正常模块生产流程执行
    42. callback(null, new ExternalModule(globalVariable));
    43. } else {
    44. //读取模块源代码,传递给loader再返回JS模块,再解析依赖,再返回此模块
    45. callback(null);//NormalModule 普通模块
    46. }
    47. });
    48. });
    49. compiler.hooks.compilation.tap('AutoExternalPlugin', (compilation) => {
    50. //1.HtmlWebpackPlugin内部会向compilation对象上添加额外的钩子
    51. //2.可以通过HtmlWebpackPlugin.getHooks取现这些钩子
    52. //3.改变标签
    53. HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('AutoExternalPlugin', (htmlData, callback) => {
    54. [...this.importedModules].forEach(key => {
    55. htmlData.assetTags.scripts.unshift({
    56. tagName: 'script',
    57. voidTag: false,
    58. meta: { plugin: 'html-webpack-plugin' },
    59. attributes: { src: this.options[key].url }
    60. });
    61. });
    62. callback(null, htmlData);
    63. });
    64. });
    65. }
    66. }
    67. module.exports = AutoExternalPlugin;

    在第38行,用到了normalModuleFactory对象的factorize钩子,这个钩子是AsyncSeriesBailHook类型的,这种类型的钩子在依次执行各个回调时,会同时检测回调执行的返回值,如果找到了返回值,则后面的回调就都不执行了
    盲猜这个钩子是用来获取模块或模块定义的(此处需要再详细地跟踪源码查看),当我们给它一个回调,返回我们自己定义的外部模块的对象定义时,就不会再往后执行其他回调了