最近在這段時間剛好在溫故下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呢

在百度上搜了下,還有朋友提過這樣的問題 html-webpack-plugin中定義的鉤子在什麼時候被call

那我就帶着這個目的看下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,它的內容大致如下

還有個重要步驟。

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內容。

小插曲

在看上面的幾個自定義鉤子執行時,我發現在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;
});

})

傳入給我的回調函數里面的就是這個 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 的結果當然沒有丟失。

相關文章