31

webpack笔记——在html-webpack-plugin插件中提供给其它插件是使用的hooks

 4 years ago
source link: https://www.daozhao.com/8851.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近在这段时间刚好在温故下webpack源码,webpack5都出来了,4还不再学习下?

这次顺便学习下webpack的常用插件html-webpack-plugin。

发现这个插件里面还额外加入了自己的hooks,方便其它插件来实现自己的功能,不得不说作者真是个好人。

部分代码如下

<code>// node_modules/html-webpack-plugin/index.js
app(compiler) {
// setup hooks for webpack 4
    if (compiler.hooks) {
      compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', compilation => {
        const SyncWaterfallHook = require('tapable').SyncWaterfallHook;
        const AsyncSeriesWaterfallHook = require('tapable').AsyncSeriesWaterfallHook;
        compilation.hooks.htmlWebpackPluginAlterChunks = new SyncWaterfallHook(['chunks', 'objectWithPluginRef']);
        compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration = new AsyncSeriesWaterfallHook(['pluginArgs']);
        compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
        compilation.hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['pluginArgs']);
        compilation.hooks.htmlWebpackPluginAfterHtmlProcessing = new AsyncSeriesWaterfallHook(['pluginArgs']);
        compilation.hooks.htmlWebpackPluginAfterEmit = new AsyncSeriesWaterfallHook(['pluginArgs']);
      });
    }
    ...

    // Backwards compatible version of: compiler.plugin.emit.tapAsync()
    (compiler.hooks ? compiler.hooks.emit.tapAsync.bind(compiler.hooks.emit, 'HtmlWebpackPlugin') : compiler.plugin.bind(compiler, 'emit'))((compilation, callback) => {
      const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
      // Get chunks info as json
      // Note: we're excluding stuff that we don't need to improve toJson serialization speed.
      const chunkOnlyConfig = {
        assets: false,
        cached: false,
        children: false,
        chunks: true,
        chunkModules: false,
        chunkOrigins: false,
        errorDetails: false,
        hash: false,
        modules: false,
        reasons: false,
        source: false,
        timings: false,
        version: false
      };
      const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
      // Filter chunks (options.chunks and options.excludeCHunks)
      let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
      // Sort chunks
      chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
      // Let plugins alter the chunks and the chunk sorting
      if (compilation.hooks) {
        chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
      } else {
        // Before Webpack 4
        chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
      }
      // Get assets
      const assets = self.htmlWebpackPluginAssets(compilation, chunks);
      // If this is a hot update compilation, move on!
      // This solves a problem where an </code><code>index.html</code> file is generated for hot-update js files
      // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
      if (self.isHotUpdateCompilation(assets)) {
        return callback();
      }

      // If the template and the assets did not change we don't have to emit the html
      const assetJson = JSON.stringify(self.getAssetFiles(assets));
      if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
        return callback();
      } else {
        self.assetJson = assetJson;
      }

      Promise.resolve()
        // Favicon
        .then(() => {
          if (self.options.favicon) {
            return self.addFileToAssets(self.options.favicon, compilation)
              .then(faviconBasename => {
                let publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
                if (publicPath && publicPath.substr(-1) !== '/') {
                  publicPath += '/';
                }
                assets.favicon = publicPath + faviconBasename;
              });
          }
        })
        // Wait for the compilation to finish
        .then(() => compilationPromise)
        .then(compiledTemplate => {
          // Allow to use a custom function / string instead
          if (self.options.templateContent !== undefined) {
            return self.options.templateContent;
          }
          // Once everything is compiled evaluate the html factory
          // and replace it with its content
          return self.evaluateCompilationResult(compilation, compiledTemplate);
        })
        // Allow plugins to make changes to the assets before invoking the template
        // This only makes sense to use if <code>inject</code> is <code>false</code>
        .then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
          assets: assets,
          outputName: self.childCompilationOutputName,
          plugin: self
        })
      .then(() => compilationResult))
        // Execute the template
        .then(compilationResult => typeof compilationResult !== 'function'
        ? compilationResult
        : self.executeTemplate(compilationResult, chunks, assets, compilation))
        // Allow plugins to change the html before assets are injected
        .then(html => {
          const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
          return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
        })
        .then(result => {
          const html = result.html;
          const assets = result.assets;
          // Prepare script and link tags
          const assetTags = self.generateHtmlTags(assets);
          const pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
          // Allow plugins to change the assetTag definitions
          return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', true, pluginArgs)
            .then(result => self.postProcessHtml(html, assets, { body: result.body, head: result.head })
            .then(html => _.extend(result, {html: html, assets: assets})));
        })
        // Allow plugins to change the html after assets are injected
        .then(result => {
          const html = result.html;
          const assets = result.assets;
          const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
          return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', true, 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());
          // Prevent caching
          self.hash = null;
          return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
        })
        .then(html => {
          // Replace the compilation result with the evaluated html code
          compilation.assets[self.childCompilationOutputName] = {
            source: () => html,
            size: () => html.length
          };
        })
        .then(() => applyPluginsAsyncWaterfall('html-webpack-plugin-after-emit', false, {
          html: compilation.assets[self.childCompilationOutputName],
          outputName: self.childCompilationOutputName,
          plugin: self
        }).catch(err => {
          console.error(err);
          return null;
        }).then(() => null))
        // Let webpack continue with it
        .then(() => {
          callback();
        });
    });
}

我在node_modules里面搜了下,还真有一些插件使用这些hooks呢

2QvUzuZ.png!web

在百度上搜了下,还有朋友提过这样的问题 html-webpack-plugin中定义的钩子在什么时候被call

3uyi2aR.png!web

那我就带着这个目的看下html-webpack-plugin的源码里面是怎么call的。

首先我们看到在compiler的compilation的hooks里面加入了html-webpack-plugin自己的6个hooks,所以我们在使用这些hooks需要注意时机,得等加入后才能使用。

这6个hooks在compiler的emit时期调用,这一点怎么看出来的呢?

我们往下看还真能看到这个

chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });

这个比较明显,直接调用的,但是其它5个hooks呢?它们就没有这么容易看出来了。

我们继续往下面看,发现有个html-webpack-plugin-before-html-generation,这个是不是跟 compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration 很像,没错,它只是 htmlWebpackPluginBeforeHtmlGeneration 的另一种命名书写方式而已。

在html-webpack-plugin是利用 trainCaseToCamelCasehtml-webpack-plugin-before-html-generation 转为 htmlWebpackPluginBeforeHtmlGeneration 的,先忽略这些细枝末节,我们继续在emit这个hooks里面看看它的自定义插件的调用流程。

apply(compiler) {
    ...
    const applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
    ...
    .then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
          assets: assets,
          outputName: self.childCompilationOutputName,
          plugin: self
        })
    ...
}

applyPluginsAsyncWaterfall (compilation) {
    if (compilation.hooks) {
      return (eventName, requiresResult, pluginArgs) => {
        const ccEventName = trainCaseToCamelCase(eventName);
        if (!compilation.hooks[ccEventName]) {
          compilation.errors.push(
            new Error('No hook found for ' + eventName)
          );
        }

        return compilation.hooks[ccEventName].promise(pluginArgs);
      };
    }

上面的 applyPluginsAsyncWaterfall 常量就是支持三个参数的函数,利用闭包,保留了 compilation 的引用,执行 applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {}) 的时候, compilation.hooks[ccEventName].promise(pluginArgs) 就执行了,我们上面的自定义的hooks的回调就得到了调用。通过前面的webpack分析文章中我们知道,这些回调是放在this._taps数组里面,执行这些回调的方式有三种, callpromisecallAsync ,我们不能老是局限于最常用的 call 方法,另外的5个hooks本身就是 AsyncSeriesWaterfallHook 类型的,所以用 promise 调用合情合理。

前面网友提的问题 html-webpack-plugin中定义的钩子在什么时候被call 也就有了答案。

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
     (adsbygoogle = window.adsbygoogle || []).push({
          google_ad_client: "ca-pub-3013839362871866",
          enable_page_level_ads: true
     });
</script>

html-webpack-plugin的核心功能就是通过 compilation.getStats() 获取到chunks。

const allChunks = compilation.getStats().toJson(chunkOnlyConfig).chunks;
      // Filter chunks (options.chunks and options.excludeCHunks)
      let chunks = self.filterChunks(allChunks, self.options.chunks, self.options.excludeChunks);
      // Sort chunks
      chunks = self.sortChunks(chunks, self.options.chunksSortMode, compilation);
      // Let plugins alter the chunks and the chunk sorting
      if (compilation.hooks) {
        chunks = compilation.hooks.htmlWebpackPluginAlterChunks.call(chunks, { plugin: self });
      } else {
        // Before Webpack 4
        chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
      }
      // Get assets
      const assets = self.htmlWebpackPluginAssets(compilation, chunks);

在一切准备就绪后,再执行自己的自定义hooks。那需要准备就绪的是什么呢?

  1. 上面的chunks
  2. 确保插件传入的template内容已经编译就绪

其中用一个变量保存了compiler的make里面的一个promise

compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
        .catch(err => {
          compilation.errors.push(prettyError(err, compiler.context).toString());
          return {
            content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
            outputName: self.options.filename
          };
        })
        .then(compilationResult => {
          // If the compilation change didnt change the cache is valid
          isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash;
          self.childCompilerHash = compilationResult.hash;
          self.childCompilationOutputName = compilationResult.outputName;
          callback();
          return compilationResult.content;
        });

childCompiler.compileTemplate 里面创建了子compiler,用它来编译我们的传入的 template (也就是准备当成模板的那个html文件)内容

// node_modules/html-webpack-plugin/lib/compiler.js
module.exports.compileTemplate = function compileTemplate (template, context, outputFilename, compilation) {
    ...
    const childCompiler = compilation.createChildCompiler(compilerName, outputOptions);

    ....
    return new Promise((resolve, reject) => {
        childCompiler.runAsChild((err, entries, childCompilation) => {})
        ...
        resolve({
              // Hash of the template entry point
              hash: entries[0].hash,
              // Output name
              outputName: outputName,
              // Compiled code
              content: childCompilation.assets[outputName].source()
            });
    })
}

获取完template的编译内容,也就是返回的compilationResult.content,后面它被赋值给compiledTemplate,它的内容大致如下

j2MriiR.png!web

还有个重要步骤。

apply(compiler) {
    ...
    .then(compiledTemplate => {
          // Allow to use a custom function / string instead
          if (self.options.templateContent !== undefined) {
            return self.options.templateContent;
          }
          // Once everything is compiled evaluate the html factory
          // and replace it with its content
          return self.evaluateCompilationResult(compilation, compiledTemplate);
        })
        .then(compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
          assets: assets,
          outputName: self.childCompilationOutputName,
          plugin: self
        })
      .then(() => compilationResult))
        // Execute the template
        .then(compilationResult => typeof compilationResult !== 'function'
        ? compilationResult
        : self.executeTemplate(compilationResult, chunks, assets, compilation))
        // Allow plugins to change the html before assets are injected
        .then(html => {
          const pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
          return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
        })
    ...
}

evaluateCompilationResult (compilation, source) {
    if (!source) {
      return Promise.reject('The child compilation didn\'t provide a result');
    }

    // The LibraryTemplatePlugin stores the template result in a local variable.
    // To extract the result during the evaluation this part has to be removed.
    source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
    const template = this.options.template.replace(/^.+!/, '').replace(/\?.+$/, '');
    const vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
    const vmScript = new vm.Script(source, {filename: template});
    // Evaluate code and cast to string
    let newSource;
    try {
      newSource = vmScript.runInContext(vmContext);
    } catch (e) {
      return Promise.reject(e);
    }
    if (typeof newSource === 'object' && newSource.__esModule && newSource.default) {
      newSource = newSource.default;
    }
    return typeof newSource === 'string' || typeof newSource === 'function'
      ? Promise.resolve(newSource)
      : Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
  }

经过 vm 的一顿操作之后返回了 newSource ,这是一个函数,在后续的Promise里面叫 compilationResult ,它可以生成出模板内容的字符串。

仔细观察可以看到 compilationResult 并没有传递给自定义钩子html-webpack-plugin-before-html-generation来使用,在html-webpack-plugin-before-html-processing钩子之前执行 self.executeTemplate(compilationResult, chunks, assets, compilation)) 生成了对应的html内容。

rIfquei.png!web

小插曲

在看上面的几个自定义钩子执行时,我发现在html-webpack-plugin-before-html-generation之前 compilationResult (下面1号then的入参)是 self.evaluateCompilationResult(compilation, compiledTemplate) 返回的函数,但是怎么在经过html-webpack-plugin-before-html-generation之后,后面的准备使用html-webpack-plugin-before-html-processing的then方法(下面的3号the)里面入参 compilationResult 依然还是那个函数呢?

我在自己测试使用html-webpack-plugin-before-html-processing钩子时是这么使用的

compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tap('test', (data) => {
  console.log(' data-> ', data);
})

对,啥也没干,就一个console而已。

在调用对应的回调函数时,是这么进行的

(function anonymous(pluginArgs
) {
"use strict";
return new Promise((_resolve, _reject) => {
var _sync = true;
function _error(_err) {
if(_sync)
_resolve(Promise.resolve().then(() => { throw _err; }));
else
_reject(_err);
};
var _context;
var _x = this._x;
var _fn0 = _x[0];
var _hasError0 = false;
try {
var _result0 = _fn0(pluginArgs);
} catch(_err) {
_hasError0 = true;
_error(_err);
}
if(!_hasError0) {
if(_result0 !== undefined) {
pluginArgs = _result0;
}
_resolve(pluginArgs);
}
_sync = false;
});

})

Yn2u637.png!web 传入给我的回调函数里面的就是这个 pluginArgs ,由于我的回调函数里面,未对入参进行过任何修改,并且还返回的 undefined ,所有 compilation.hooks[ccEventName].promise(pluginArgs) 返回的这个Promise的值还是 pluginArgs,而并非之前的 compilationResult`那个函数啊

经过认真排查发现,原来是这一部分Promise回调太多,容易眼花。原来1号then里面的2号then是这样写的,并非直接链式写的 1号--applyPluginsAsyncWaterfall--2号--3号 ,而是 1号--(applyPluginsAsyncWaterfall--2号)--3号

.then(// 1

    compilationResult => applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
          assets: assets,
          outputName: self.childCompilationOutputName,
          plugin: self
        })
      .then( // 2
        () => compilationResult
      )

)
// Execute the template
 .then(compilationResult => typeof compilationResult !== 'function' //3
    ? compilationResult
    : self.executeTemplate(compilationResult, chunks, assets, compilation)
)

我将排版调整下,这样看的更清楚了,这样的话 compilationResult 的结果当然没有丢失。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK