如何使用require.ensure在Webpack中拆分代码以用于生产环境?

11

我正在使用Webpack,希望将客户端代码分成多个部分,并在用户需要时加载它们。

这是我的文件结构:

app.js       <-- Entry point as introduced to Webpack
Module.js    <-- To be loaded dynamically

app.jsModule.js 之间没有直接的连接,相反,第二个文件是通过以下方式由第一个文件加载的:

require.ensure([], (require) => {
    let path = "Module";
    let module = require("./" + path).default;
});

我使用"./" + path,只是为了确保Webpack在静态解析路径时不会聪明地去处理它。无论如何,在webpack-dev-server的开发模式中这个代码可以工作。我的意思是在我触发上述代码运行事件之前,Module.js尚未被下载。之后,它被加载并完美地执行。

但是当我将项目打包为生产环境时,它停止工作,并且在我触发下载事件时就出现以下错误(在浏览器中),而不甚至尝试发送请求:

Uncaught Error: Cannot find module './Module'.

此外,当我动态组合路径时(就像上面的代码一样),构建过程会发出以下警告:

WARNING in ./src/app/app.js Critical dependencies: 74:34-47 the request of a dependency is an expression

如何正确配置Webpack以支持代码分割以便可以动态下载?


我已经测试了@wollnyst提供的解决方案,以下是结果。当我像这样实现时,它有效:

require.ensure(["./Module"], (require) => {
    let path = "Module";
    let module = require("./" + path).default;
});

但是我不想这样,我想要像这样:

let path = "Module";
require.ensure(["./" + path], (require) => {
    let module = require("./" + path).default;
});

现在它在浏览器中出现了运行时错误:

Uncaught TypeError: webpack_require(...).ensure 不是一个函数

仍然没有运气!


你不需要使模块路径动态化,只是为了使其动态加载。require.ensure会处理这个问题。在你的情况下,静态路径就可以正常工作。 - raiyan
1
@RaiyanMohammed 对不起,我不明白你的意思!在上面的代码中,我动态生成路径,因为在现实生活中我就是这样做的。编写代码时,模块的路径是未知的。只有在运行时,路径才会被指定到一个变量中。 - Mehran
1
这是问题:与 node-require 不同,webpack 是静态的。它只在构建时运行一次。因此,webpack 无法评估动态 requires。你确切拥有什么结构?也许定义一个自己的 context 可能会有帮助。 - wollnyst
@wollnyst 我已经测试了你的建议,它可以工作,但问题是它没有分割代码!上下文是否应该分割代码?你有一个关于如何动态加载分割代码的工作示例吗?文档有点模糊! - Mehran
可能查看一下你的webpack.config.js文件是个好主意。当我开始使用webpack时,我也遇到过这种情况,但可能与此无关。请查看output配置选项,其中有一个publicPath设置,告诉webpack在哪个路径下查找文件。根据文档,output.publicPath:此选项指定在浏览器中引用输出目录的公共URL。 - Gun
2个回答

2
require.ensure数组的第一个参数中动态放置您想要要求的路径是错误的,不应该这样做。这个数组旨在用于您想要动态加载的模块的依赖项,并且这些依赖项是需要异步代码在回调中安全运行的。
理解webpack如何处理代码拆分的关键部分是,您不能进行完全动态的语句,因为webpack需要一些位置信息来解析您想要要求的模块,因此出现了dependency is an expression警告。即使您可以像智能地预先添加./一样聪明,最好还是重复完整的ensure语句,使用静态模块路径字符串,即使您有很多要动态加载的模块,并且有点令人讨厌,以便您不会遇到任何问题。
另一种方法是创建一个专门用于此目的的文件夹,例如splits,并从此处解析所有模块。但这意味着您在此目录中要求的每个模块都将位于相同的分割点,这并不是任何人都想要的,除非是特定用例。
关于配置,您将需要使用namedModulesPluginsCommonsChunkPlugin。我个人的做法是拥有一个包含所有常见源代码的main.js,一个包含所有常见node_modulesvendor.js,以及一个由webpack需要的runtime.js。然后,块被分开,如果一个块依赖于特定的供应商依赖项,则该依赖项将添加到此特定块而不是常见的vendor.js中。
plugins: [
  new webpack.NamedModulesPlugin(),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: module => module.context && module.context.indexOf('node_modules'),
  }),
  new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime',
  }),
],

你可以确保有类似以下代码的output.filename属性:
output: {
  filename: '[name]-[chunkhash].js',
}

否则,每当您的源代码发生变化时,vendor.js 的哈希值都会改变,即使您没有修改依赖项,这对于缓存来说是不好的。最近的Webpack可视化工具是一个非常好的工具,可以检查您的捆绑和块的外观,并验证事物是否正常或检测可以更好地分块的依赖关系。如果使用StatsWriterPlugin来收集统计信息,则可能需要稍微更改配置以处理您的块:
new StatsWriterPlugin({
  fields: null,
  transform: (data, opts) => {
    const stats = opts.compiler.getStats().toJson({ chunkModules: true })
    return JSON.stringify(stats, null, 2)
  },
}),

需要注意的一点是,require.ensure 是特定于 webpack 的,并且正在被 import() 所取代。由于这个问题来自 2016 年,可能没有我们现在使用的 webpack 2 中的相同元素,因此我会稍微扩展一下它。
现在,动态导入提案正在进入 ES,你可以在 webpack 中使用它。您需要一个Promise polyfill和类似 babel syntax-dynamic-import 插件(现在应该在 stage-3 preset)或者 dynamic-import-webpack 如果这对您还不起作用(这基本上将import() 转换为 ensures)。
如果您想每个模块获取一个 chunk,仍然需要像 ensure 一样指定完整路径,但这是更好的语法并且更加面向未来。
有很多其他资源可供您在新的 webpack 文档页面上查看关于代码分割的信息。

1
此外,在动态导入中,模板字符串可以在模块路径中使用,请确保向Webpack提供一些路径信息(就像您所提到的那样)。 - Eliran Malka
确实,我没有明确说明,因为这只是(酷)es6替代字符串连接的方式。 - Preview
当然。我没有看到任何关于字符串连接的提及,所以我想提一下它。 - Eliran Malka
@EliranMalka 还有其他问题吗? - Preview
你几乎都解决了它们 :) 谢谢 - Eliran Malka

1
你需要在 require.ensure 的第一个参数中传入你想要引用的模块:
require.ensure(['./Module'], function(require) {
    const module = require('./Module');
});

1
你的解决方案有点起作用,但也有点不行!我已经在问题中包含了结果。谢谢。 - Mehran

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