• 关于webpack(v5.74.0)的html-webpack-plugin原理


    html-webpack-plugin插件可以将现有的资源添加进html文件,同时也可以监听该插件的一些hooks进行自定义操作,接下来我们看看具体实现原理。

    使用

    module.exports = {
      entry: {
        testxx: './src/index.js',
      },
      mode: 'development',
      plugins: [
        new HtmlWebpackPlugin({
          template: './index.html',
          filename: 'index.html',
        })
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    先看看该插件的apply方法:

    apply(compiler) {
        // Wait for configuration preset plugions to apply all configure webpack defaults
        compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
          const userOptions = this.userOptions;
    
          // Default options
          const defaultOptions = {
            template: 'auto',
            templateContent: false,
            templateParameters: templateParametersGenerator,
            filename: 'index.html',
            publicPath: userOptions.publicPath === undefined ? 'auto' : userOptions.publicPath,
            hash: false,
            inject: userOptions.scriptLoading === 'blocking' ? 'body' : 'head',
            scriptLoading: 'defer',
            compile: true,
            favicon: false,
            minify: 'auto',
            cache: true,
            showErrors: true,
            chunks: 'all',
            excludeChunks: [],
            chunksSortMode: 'auto',
            meta: {},
            base: false,
            title: 'Webpack App',
            xhtml: false
          };
    
          /** 合并默认options */
          const options = Object.assign(defaultOptions, userOptions);
          this.options = options;
    
          // entryName to fileName conversion function
          const userOptionFilename = userOptions.filename || defaultOptions.filename;
          const filenameFunction = typeof userOptionFilename === 'function'
            ? userOptionFilename
            // Replace '[name]' with entry name
            : (entryName) => userOptionFilename.replace(/\[name\]/g, entryName);
    
          /** output filenames for the given entry names */
          const entryNames = Object.keys(compiler.options.entry);
          const outputFileNames = new Set((entryNames.length ? entryNames : ['main']).map(filenameFunction));
    
          /** Option for every entry point */
          const entryOptions = Array.from(outputFileNames).map((filename) => ({
            ...options,
            filename
          }));
    
          // Hook all options into the webpack compiler
          entryOptions.forEach((instanceOptions) => {
            hookIntoCompiler(compiler, instanceOptions, this);
          });
        });
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    apply方法合并了用户传入的options,主要执行hookIntoCompiler方法。

    ...
    // 初始化childCompiler并添加入口
    const childCompilerPlugin = new CompileFilePlugins(compiler);
    if (!options.templateContent) {
      childCompilerPlugin.addEntry(options.template);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    再来看看怎么获取到资源的:

    compilation.hooks.processAssets.tapAsync(
            {
              name: 'HtmlWebpackPlugin',
              stage:
                /**
                 * Generate the html after minification and dev tooling is done
                 */
                webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
            },
            /**
             * Hook into the process assets hook
             * @param {WebpackCompilation} compilationAssets
             * @param {(err?: Error) => void} callback
             */
            (compilationAssets, callback) => {
              // Get all entry point names for this html file
              const entryNames = Array.from(compilation.entrypoints.keys());
              const filteredEntryNames = filterChunks(entryNames, options.chunks, options.excludeChunks);
              const sortedEntryNames = sortEntryChunks(filteredEntryNames, options.chunksSortMode, compilation);
              // 获取编译结果
              const templateResult = options.templateContent
                ? { mainCompilationHash: compilation.hash }
                : childCompilerPlugin.getCompilationEntryResult(options.template);
    
              // 如果在上一次主编译运行期间未执行子编译
              // it is a cached result
              const isCompilationCached = templateResult.mainCompilationHash !== compilation.hash;
    
              /** The public path used inside the html file */
              const htmlPublicPath = getPublicPath(compilation, options.filename, options.publicPath);
    
              /** 获取输出的js和css列表 */
              const assets = htmlWebpackPluginAssets(compilation, sortedEntryNames, htmlPublicPath);
    
              // If the template and the assets did not change we don't have to emit the html
              const newAssetJson = JSON.stringify(getAssetFiles(assets));
              if (isCompilationCached && options.cache && assetJson === newAssetJson) {
                previousEmittedAssets.forEach(({ name, html }) => {
                  compilation.emitAsset(name, new webpack.sources.RawSource(html, false));
                });
                return callback();
              } else {
                previousEmittedAssets = [];
                assetJson = newAssetJson;
              }
              // html webpack插件使用html标签的对象表示,这些标签将被注入以允许更容易地更改。就在它们被转换之前,第三方插件作者可能会更改顺序和内容
              // The html-webpack plugin uses a object representation for the html-tags which will be injected
              // to allow altering them more easily
              // 执行beforeAssetTagGeneration的hook
              const assetsPromise = getFaviconPublicPath(options.favicon, compilation, assets.publicPath)
                .then((faviconPath) => {
                  assets.favicon = faviconPath;
                  return getHtmlWebpackPluginHooks(compilation).beforeAssetTagGeneration.promise({
                    assets: assets,
                    outputName: options.filename,
                    plugin: plugin
                  });
                });
    
              // 将css和js生成node模式
              const assetTagGroupsPromise = assetsPromise // 插入html的脚本进行分组后触发的钩子
                // And allow third-party-plugin authors to reorder and change the assetTags before they are grouped
                .then(({ assets }) => getHtmlWebpackPluginHooks(compilation).alterAssetTags.promise({
                  assetTags: {
                    scripts: generatedScriptTags(assets.js),
                    styles: generateStyleTags(assets.css),
                    meta: [
                      ...generateBaseTag(options.base),
                      ...generatedMetaTags(options.meta),
                      ...generateFaviconTags(assets.favicon)
                    ]
                  },
                  outputName: options.filename,
                  publicPath: htmlPublicPath,
                  plugin: plugin
                }))
                .then(({ assetTags }) => {
                  // Inject scripts to body unless it set explicitly to head
                  const scriptTarget = options.inject === 'head' ||
                    (options.inject !== 'body' && options.scriptLoading !== 'blocking') ? 'head' : 'body';
                  // 生成 `head` and `body` 数组
                  const assetGroups = generateAssetGroups(assetTags, scriptTarget);
                  // Allow third-party-plugin authors to reorder and change the assetTags once they are grouped
                  return getHtmlWebpackPluginHooks(compilation).alterAssetTagGroups.promise({
                    headTags: assetGroups.headTags,
                    bodyTags: assetGroups.bodyTags,
                    outputName: options.filename,
                    publicPath: htmlPublicPath,
                    plugin: plugin
                  });
                });
    
              // 在沙盒中执行打包后的html模块
              const templateEvaluationPromise = Promise.resolve()
                .then(() => {
                  if ('error' in templateResult) {
                    return options.showErrors ? prettyError(templateResult.error, compiler.context).toHtml() : 'ERROR';
                  }
                  // Allow to use a custom function / string instead
                  if (options.templateContent !== false) {
                    return options.templateContent;
                  }
                  // Once everything is compiled evaluate the html factory
                  // 在沙盒中执行html打包后的文件并输出资源
                  return ('compiledEntry' in templateResult)
                    ? plugin.evaluateCompilationResult(templateResult.compiledEntry.content, htmlPublicPath, options.template)
                    : Promise.reject(new Error('Child compilation contained no compiledEntry'));
                });
              // 1.钩子 2.将css和js生成node模式 3.在沙盒中执行打包后的html模块
              const templateExectutionPromise = Promise.all([assetsPromise, assetTagGroupsPromise, templateEvaluationPromise])
                // Execute the template
                .then(([assetsHookResult, assetTags, compilationResult]) => typeof compilationResult !== 'function'
                  ? compilationResult
                  : executeTemplate(compilationResult, assetsHookResult.assets, { headTags: assetTags.headTags, bodyTags: assetTags.bodyTags }, compilation));
              // 2.将css和js生成node模式 3.在沙盒中执行打包后的html模块
              const injectedHtmlPromise = Promise.all([assetTagGroupsPromise, templateExectutionPromise])
                // 添加afterTemplateExecution钩子,此时已生成html,执行postProcessHtml
                .then(([assetTags, html]) => {
                  const pluginArgs = { html, headTags: assetTags.headTags, bodyTags: assetTags.bodyTags, plugin: plugin, outputName: options.filename };
                  return getHtmlWebpackPluginHooks(compilation).afterTemplateExecution.promise(pluginArgs);
                })
                .then(({ html, headTags, bodyTags }) => {
                  // 将script和css插入html
                  return postProcessHtml(html, assets, { headTags, bodyTags });
                });
              // 触发钩子并且将script和css插入html后
              const emitHtmlPromise = injectedHtmlPromise
                // 添加beforeEmit钩子
                .then((html) => {
                  const pluginArgs = { html, plugin: plugin, outputName: options.filename };
                  return getHtmlWebpackPluginHooks(compilation).beforeEmit.promise(pluginArgs)
                    .then(result => result.html);
                })
                .catch(err => {
                  // In case anything went wrong the promise is resolved
                  // with the error message and an error is logged
                  compilation.errors.push(prettyError(err, compiler.context).toString());
                  return options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
                })
                .then(html => {
                  // filename警告
                  const filename = options.filename.replace(/\[templatehash([^\]]*)\]/g, require('util').deprecate(
                    (match, options) => `[contenthash${options}]`,
                    '[templatehash] is now [contenthash]')
                  );
                  // 替换contenthash
                  const replacedFilename = replacePlaceholdersInFilename(filename, html, compilation);
                  // 将html code添加到webpack assets
                  compilation.emitAsset(replacedFilename.path, new webpack.sources.RawSource(html, false), replacedFilename.info);
                  previousEmittedAssets.push({ name: replacedFilename.path, html });
                  return replacedFilename.path;
                })// afterEmit的钩子
                .then((finalOutputName) => getHtmlWebpackPluginHooks(compilation).afterEmit.promise({
                  outputName: finalOutputName,
                  plugin: plugin
                }).catch(err => {
                  console.error(err);
                  return null;
                }).then(() => null));
    
              // Once all files are added to the webpack compilation
              // let the webpack compiler continue
              emitHtmlPromise.then(() => {
                callback();
              });
            });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166

    获取资源主要调用htmlWebpackPluginAssets函数

    /** 获取打包的js和css列表
       * The htmlWebpackPluginAssets extracts the asset information of a webpack compilation
       * for all given entry names
       * @param {WebpackCompilation} compilation
       * @param {string[]} entryNames
       * @param {string | 'auto'} publicPath
       * @returns {{
          publicPath: string,
          js: Array,
          css: Array,
          manifest?: string,
          favicon?: string
        }}
       */
      function htmlWebpackPluginAssets(compilation, entryNames, publicPath) {
        const compilationHash = compilation.hash;
        /**
         * @type {{
            publicPath: string,
            js: Array,
            css: Array,
            manifest?: string,
            favicon?: string
          }}
         */
        const assets = {
          // The public path
          publicPath,
          // Will contain all js and mjs files
          js: [],
          // Will contain all css files
          css: [],
          // Will contain the html5 appcache manifest files if it exists
          manifest: Object.keys(compilation.assets).find(assetFile => path.extname(assetFile) === '.appcache'),
          // Favicon
          favicon: undefined
        };
    
        // Append a hash for cache busting
        if (options.hash && assets.manifest) {
          assets.manifest = appendHash(assets.manifest, compilationHash);
        }
    
        // Extract paths to .js, .mjs and .css files from the current compilation
        const entryPointPublicPathMap = {};
        const extensionRegexp = /\.(css|js|mjs)(\?|$)/;
        for (let i = 0; i < entryNames.length; i++) {
          const entryName = entryNames[i];
          /** 获取所有的输出资源js和css文件等 */
          const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles();
    
          const entryPointFiles = entryPointUnfilteredFiles.filter((chunkFile) => {
            // compilation.getAsset was introduced in webpack 4.4.0
            // once the support pre webpack 4.4.0 is dropped please
            // remove the following guard:
            const asset = compilation.getAsset && compilation.getAsset(chunkFile);
            if (!asset) {
              return true;
            }
            // Prevent hot-module files from being included:
            const assetMetaInformation = asset.info || {};
            return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development);
          });
    
          // Prepend the publicPath and append the hash depending on the
          // webpack.output.publicPath and hashOptions
          // E.g. bundle.js -> /bundle.js?hash
          const entryPointPublicPaths = entryPointFiles
            .map(chunkFile => {
              const entryPointPublicPath = publicPath + urlencodePath(chunkFile);
              return options.hash
                ? appendHash(entryPointPublicPath, compilationHash)
                : entryPointPublicPath;
            });
    
          entryPointPublicPaths.forEach((entryPointPublicPath) => {
            const extMatch = extensionRegexp.exec(entryPointPublicPath);
            // Skip if the public path is not a .css, .mjs or .js file
            if (!extMatch) {
              return;
            }
            // Skip if this file is already known
            // (e.g. because of common chunk optimizations)
            if (entryPointPublicPathMap[entryPointPublicPath]) {
              return;
            }
            entryPointPublicPathMap[entryPointPublicPath] = true;
            // ext will contain .js or .css, because .mjs recognizes as .js
            const ext = extMatch[1] === 'mjs' ? 'js' : extMatch[1];
            assets[ext].push(entryPointPublicPath);
          });
        }
        return assets;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    然后开始设置当前插件的钩子:beforeAssetTagGeneration、将css和js生成node模式-alterAssetTags、生成 head and body 数组-alterAssetTagGroups、在沙盒中执行后获取html-afterTemplateExecution、将script和css插入html、beforeEmit、替换contenthash并将html code添加到webpack assets、afterEmit。到此整个插件就执行完毕。

  • 相关阅读:
    基础-MVP标定-H矩阵变换算子
    RFSoC应用笔记 - RF数据转换器 -07- RFSoC关键配置之RF-DAC内部解析(一)
    【JAVA进阶篇教学】第三篇:JDK8中Stream API使用
    paddleocr-营业执照识别项目实战
    3. 实战入门
    MAC认证
    2022年最新《Java八股文面试宝典》全网独一份!(效率最高、知识最新、包含各个技术栈)
    <线性回归算法(Linear regression)>——《机器学习算法初识》
    AndroidStudio编译很慢问题解决
    php伪协议详解
  • 原文地址:https://blog.csdn.net/qq_35094120/article/details/127978320