关于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',
    })
  ]
}

先看看该插件的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);
      });
    });
  }

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

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

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

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();
          });
        });

获取资源主要调用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<string>,
      css: Array<string>,
      manifest?: string,
      favicon?: string
    }}
   */
  function htmlWebpackPluginAssets(compilation, entryNames, publicPath) {
    const compilationHash = compilation.hash;
    /**
     * @type {{
        publicPath: string,
        js: Array<string>,
        css: Array<string>,
        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;
  }

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Young soul2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值