从运行时块加载生成的webpack代码块

4

我正在使用webpack将新的(React)代码部分更新到现有的Web应用程序中,并将所有东西绑定在一起以供生产使用。由于现有的HTML页面(实际上是XML转换为HTML)已经存在,因此我无法使用由HtmlWebpackPlugin生成的index.html

我的目标是使webpack生成一个小的runtime.bundle.js,它将动态加载其他生成的块(main.[contenthash]vendor.[contenthash]),而不是将这些条目作为script标签添加到index.html。这样runtime.bundle.js可以设置为nocache,而大型的其他块可以由浏览器缓存并在代码更改时正确获取。

以下是生成的index.html的body块的示例,请注意注释:

<html>
  <head>...</head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script type="text/javascript" src="runtime.bundle.js"></script>

    <!-- I want these two files below not injected as script tags, 
         but loaded from the runtime.bundle.js file above  -->

    <script type="text/javascript" src="vendors.31b8acd750477817012d.js"></script>
    <script type="text/javascript" src="main.1e4a456d496cdd2e1771.js"></script>
  </body>
</html>

运行时文件已经加载了通过以下代码动态从JS导入的不同块:

const App = React.lazy(() => import(/* webpackChunkName: "modulex" */ './App'));

这将在runtime.bundle.js中的某处创建以下片段。
          a = document.createElement('script');
        (a.charset = 'utf-8'),
          (a.timeout = 120),
          i.nc && a.setAttribute('nonce', i.nc),
          (a.src = (function(e) {
            return (
              i.p +
              '' +
              ({ 1: 'modulex' }[e] || e) +
              '.' +
              { 1: '0e0c4000d075e81c1e5b' }[e] +
              '.js'
            );

那么,vendorsmain代码块可以实现相同的效果吗?

我能想到的唯一另一个替代方案是使用WebpackManifestPlugin生成manifest.json并使用它将代码块注入到已经存在的HTML文件中。


我正在为完全相同的场景而苦恼。你找到了解决方案吗? - nevermind777
@nevermind777 我写了一个脚本,它会创建一个“运行时”JS文件,将哈希块作为脚本注入到HTML文档的头部中(使用由WebpackManifestPlugin创建的manifest.json)。这个脚本可以作为npm脚本调用。如果你需要的话,我可以分享给你... - Sjiep
谢谢@Sjiep,听起来很有趣,我想要查看你的解决方案。 - nevermind777
3个回答

3
最终,我通过创建一个脚本来解决了这个问题。该脚本使用由WebpackManifestPlugin生成的manifest.json来生成一个runtime.js脚本,在页面加载时动态地加载块并将此runtime.js插入到index.html的中。可以使用tasksfile npm包通过npm scripts部分调用它。
在webpack配置中,将插件添加到插件数组中:
{
  // your other webpack config
  plugins: [
    new ManifestPlugin(),
    // other webpack plugins you need
  ],
}

我有一个外部JS文件,可以通过npm脚本调用tasksfile npm包来调用该函数:

// The path where webpack saves the built files to (this includes manifest.json)
const buildPath = './build';
// The URL prefix where the file should be loaded
const urlPrefix = 'https://www.yourdomain.com';

function buildRuntime() {
  const manifest = require(`${buildPath}/manifest`);
  // Loop through each js file in manifest file and append as script element to the head
  // Execute within an IIFE such that we don't pollute global namespace
  let scriptsToLoad = Object.keys(manifest)
    .filter(key => key.endsWith('.js'))
    .reduce((js, key) => {
      return (
        js +
        `
        script = document.createElement('script');
        script.src = urlPrefix + "/js/${manifest[key]}";
        document.head.appendChild(script);`
      );
    }, `(function(){var script;`);
  scriptsToLoad += '})()';

  // Write the result to a runtime file that can be included in the head of an index file
  const filePath = `${buildPath}/runtime.js`;
  fs.writeFile(filePath, scriptsToLoad, err => {
    if (err) {
      return console.log('Error writing runtime.js: ', err);
    }
    console.log(`\n${filePath} succesfully built\n`);
  });
}

该函数基本上循环遍历manifest.json中的所有JS入口文件。
然后使用这些条目作为src属性创建脚本标签,将这些脚本标签添加到document.head作为子元素(触发条目的加载)。 最后,将此脚本保存到runtime.js文件并存储在生成目录中。
现在,您可以将此runtime.js文件包含在HTML文件中,如果所有路径设置正确,则应加载块。

1

HtmlWebpackPlugin提供了一个chunks选项,您可以使用它来选择性地包含webpack配置的entry对象中的某些条目。使用它,您实际上可以通过将其放入单独的src/dynamic-load.js文件中,并仅将其添加到插件配置中来简化大部分自定义脚本的逻辑:

entry: {
    runtimeLoader: './src/dynamic-load.js'
},
plugins: [
    new HtmlWebpackPlugin({
        // ...
        chunks: [ 'runtimeLoader' ]
    }),
]

另一个关于 chunks 使用的例子可以在这里看到。

他们内置的templateParameters甚至允许您将构建输出文件名放入变量中并在dynamic-load.js中读取它们。你需要为此制作自己的模板,但这可能是一种方法。你甚至可以看看他们建议的templateParameters示例如何做到的

如果那不起作用,您始终可以通过afterEmit钩子从webpack本身获取捆绑输出文件名,然后将其输出到dynamic-load.js会调用的JSON文件中。主要思路如下,但在那时,您只是在做与WebpackManifestPlugin相同的事情。

plugins: [
    {
        apply: compiler => {
            compiler.hooks.afterEmit.tap('DynamicRuntimeLoader', compilation => {
                const outputBundlePaths = Object.keys(compilation.assets)

                // output to dist/files.json
                saveToOutputDir('files.json', outputBundlePaths);
            });
        }
    },
    // ...
]

// dynamic-load.js

fetch('/files.json').then(res => res.json()).then(allFiles => {
    allFiles.forEach(file => {
        // document.createElement logic
    });
});

最后注意一点:WebpackManifestPlugin实际上是一个资产清单,并不会生成正确的manifest.json。他们应该更新他们的默认文件名assets-manifest.json,但我猜还没有人向他们指出这一点。


0

// https://github.com/webpack/webpack/issues/11816#issuecomment-716402552

declare var __webpack_get_script_filename__: any;
const oldFn = __webpack_get_script_filename__;

__webpack_get_script_filename__ = (chunkId) => {
  const filename = oldFn(chunkId);
  return filename + `?v=${environment.timestamp}`;
};

巴里跑... 跑起来... 用这个超能力脚本!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接