2021 年路线图 (2020-12-08)

webpack 5 正式发布已经快两个月了。由于赞助情况,我们无法像我们希望的那样投入时间到 webpack 上。仅就我个人而言(@sokra),我享受了短暂的休息,并做了一些副项目。具有讽刺意味的是,当我使用 webpack 5 及其所有尖端功能(资产模块、worker 支持、持久缓存)时,我发现 webpack 5 中还有一些 bug,人们在将项目升级到 webpack 5 时可能会遇到这些 bug,这导致了很多工作投入到 bug 修复中。以下是一些简要总结

到目前为止发生了什么?

webpack 中暴露了一些更多内容,包括类型方面和运行时方面。还进行了一些低级别性能改进。以前,没有分号的代码在某些情况下会生成一些无效/不正确的代码,现在已更正。无副作用代码 + 连接模块 + 重新导出组合导致了一些边缘情况,这些情况已修复(至少已知的情况)。

但是,用户报告的一个 bug 导致需要一个全新的内部功能。如果您觉得 webpack 内部机制很无聊或过于复杂,可以跳过它,直接进入下一章。

要触发该 bug,我们需要三个要素

  • 从 webpack 5 开始,production 模式下的优化将对每个运行时(通常与入口点相同)运行已使用导出分析(Tree Shaking),这意味着 webpack 可以单独优化每个运行时(或入口点)。
  • 自定义 optimization.splitChunks 配置允许强制将模块合并到单个块中。这是通过传递 name 选项来完成的。例如,{ test: /node_modules/, name: "vendors" } 将来自 node_modules 的模块合并到单个块中。虽然这通常不建议这样做,但它是可行的,并且在某些情况下可能是有意义的。整个过程都是关于权衡取舍的,选择将所有供应商合并到单个块中对于长期缓存该块以供重复访问或在多个入口点之间使用可能是有益的。
  • 当无副作用模块的导出未使用时,整个模块将从模块图中省略,并且import语句根本不会生成任何运行时代码。

在一个边缘情况下会发生问题,即来自两个入口点的模块被合并到一个单独的块中,并且它们引用了一个无副作用模块,该模块不在共享块中,因为只有一个入口点使用来自无副作用模块的导出。共享块中的模块被两个入口点使用,因此它们需要包含任何入口点使用的导出。这意味着它将生成引用无副作用模块的代码,该模块在上述边缘情况下对另一个入口点在运行时不可用,这会导致运行时出现undefined is not a functioncannot read property 'call' of undefined错误。

一个可能的解决方法是在所有入口点中包含无副作用模块,但由于该模块实际上并不需要,因此这将浪费捆绑包大小。因此,我们采用了另一种方法,这需要开发一项新功能:运行时依赖代码生成。这允许生成根据其执行的运行时而表现不同的代码。

换句话说,我们将一些生成的代码包装在if块中,因此它们只在一个运行时执行。在这个例子中,这将影响引用无副作用模块的import语句。导入仅对其中一个入口点执行。这避免了包含不必要的模块,并且还避免了即使代码可用也会执行不必要的代码。因此,即使您将所有代码合并到一个单独的块中,也只执行真正使用的代码。

到目前为止,关于这次旅行,我希望它不那么无聊……

2021 年路线图

因此,假设我们可以解决我们的赞助情况,以下计划在 2021 年进行

进一步稳定

我们的首要任务仍然是稳定 webpack 5。到目前为止,情况看起来相当不错。最近报告的大多数严重错误都影响了一些边缘情况。所以我想 webpack 5 应该适用于一般情况。但是处理边缘情况是(并且应该保持)webpack 的优势之一,因此我们希望继续努力修复这些问题。我们认为许多 webpack 用户需要为他们的构建定制一些东西,而 webpack 通过可配置性和丰富的插件系统提供了这些东西。

EcmaScript 模块

EcmaScript 模块 (ESM) 正在逐渐获得广泛采用。在编写方面,它们已经是事实上的标准。在浏览器支持方面,它看起来也相当不错(除了 IE11 和一些较旧的移动浏览器)。浏览器在支持 WebWorkers 的 ESM 方面仍然有些不足。

也可以生成在type=module脚本标签中运行的包,但目前好处不多。

Webpack 中有多个方面可以改进 ESM 支持。

ESM 作为块加载机制

当目标是 Web 时,Webpack 通过script标签加载块。当目标是 Node.js 时,Webpack 通过requirefs + vm加载块。当目标是 WebWorkers 时,Webpack 通过importScripts加载块。

在不远的将来,所有这些环境都将支持 ESM,更重要的是支持动态import()函数。因此,基于import()的块加载机制可以统一所有这些环境,同时甚至需要更少的运行时代码。

自执行块

目前,Webpack 中按需加载的块始终是模块的容器,并且从不直接执行模块代码。在模块中编写import("./module")时,这将编译成类似__webpack_load_chunk__("chunk-containing-module.js").then(() => __webpack_require__("./module"))的内容。在许多情况下,这无法更改(例如,当加载多个块或加载 CSS 时),但在某些情况下,Webpack 可以生成直接执行所包含模块的块。这会导致生成的代码更少,并且会避免块中的函数包装。

目前,我还不确定这是否值得,但至少值得研究一下。

ESM 导出

目前,无法通过output.library.type: "module"为包生成 ESM 导出。当将 Webpack 包集成到 ESM 加载环境或内联脚本中时,这可能很有用。

ESM 外部(导入)

Webpack 允许定义externals,它们是不捆绑但在运行时存在的模块。外部类型很多,从全局变量到 CommonJs/AMD/System,再到从经典脚本标签加载。甚至import()type: "import")也可以用于加载外部,但importtype: "module")尚不支持。

有趣的是,即使type: "module"尚不支持,Webpack 在编写例如import x from "https://example.com/module.js"时已经将其用作默认值。默认情况下,已选择无缝添加对 ESM 外部的支持,而不会引入重大更改。

import中的绝对 URL 可能有意义,例如,当使用将 API 作为 ESM 提供的外部服务时:import { event } from "https://analytics.company.com/api/v1.js"import("https://analytics.company.com/api/v1.js")可能更有意义,以便在依赖此外部服务时优雅地处理错误,但错误也可以在模块图中更高的地方捕获)。

与往常一样,externals配置允许将任何模块名称映射到外部。

export default {
  externalsType: 'module',
  externals: {
    analytics: 'https://analytics.company.com/api/v1.js',
    svelte: 'https://jspm.dev/svelte@3',
    react: 'https://cdn.skypack.dev/preact@10',
    'react-dom': 'https://esm.sh/[react,react-dom]/react-dom',
  },
};

ESM 库

当 ESM 导出和导入得到支持时,人们可能会认为捆绑库是有意义的,在某些情况下这可能是真的,但在许多情况下,原生捆绑会导致更糟糕的结果。最大的问题是 "sideEffects": false 标志。它影响每个文件的模块以跳过整个模块。当连接多个无副作用的模块时,不再可能跳过各个模块,这会导致在未使用库的所有导出时优化效果更差。

当输出应该是稍后由捆绑器处理的库时,需要考虑这一点。

我可以想到一种特殊模式,它不应用分块,而是发出通过 ESM 导入和导出(或 CommonJS require)连接的原始(处理过的)模块。所以这意味着加载器、模块图和资产优化会运行,但不会创建块图,并且模块图中的每个模块都会作为单独的文件发出。

严格模式警告

在生成 ESM 包时,所有包含的代码将被强制为严格模式。对于许多模块来说,这不是问题,但有一些较旧的包可能难以处理不同的语义。我们希望在这些情况下显示警告。

更多一等公民

Webpack 4 和 5 在支持非 JS 模块类型方面做了很多工作,webpack 5 已经默认支持一些模块类型:JS(ESM/CJS/AMD)、JSON、WebAssembly、Asset。自 webpack 5 以来,我们的长期目标之一是成为一个 Web 应用程序优化器,目标是支持浏览器支持的所有内容。因此,从技术上讲,一个普通的 Web 应用程序应该可以与 webpack 开箱即用,但会随时优化。

最初的 webpack 5 版本已经在这一方向上迈出了重大步伐:new Worker 原生支持。new URL(...) 原生支持(资产)。

WebAssembly 和 JSON 已经得到支持,即使提案尚未完成。

但完整的方案中仍然缺少两种资源类型:HTML 和 CSS。

CSS 作为模块

目前 webpack 通过 css-loaderstyle-loadermini-css-extract-plugin 支持 CSS。这工作得很好,但我认为我们可以通过在 webpack 中支持 CSS 作为原生模块类型来做得更多。

一个主要的好处是开发人员体验:mini-css-extract-plugin 配置并不容易,摆脱它会为开发人员简化很多。这并不意味着你不能在它的基础上添加额外的自定义。我看到许多开发人员不使用原始 CSS,而是使用预处理器(使用原生 CSS 支持,这将看起来像这样:{ test: /\.sass$/, type: "stylesheet", use: "sass-loader" })。

根据State of CSS 2020,CSS Modules 是一种流行的编写模块化 CSS 的方式,它作为 webpack 中的原生模块类型,可以从模块图优化中获益,例如 Tree Shaking(用于导出优化和副作用优化)。使用 CSS Modules 时,这意味着生成的 CSS 将只包含从应用程序中引用的 CSS 规则(就像从 JS Tree Shaking 中获得的一样)。

有一些潜在的 CSS Modules 特定优化,可以通过 webpack 对应用程序的全局了解来实现:CSS 规则可以拆分为更小的规则,以避免重复公共属性。这可以使输出 CSS 包含更少的重复属性(原子 CSS),从而导致更小的有效负载。

但这里有一个很大的“但是”:WebComponents 社区正在努力制定一个不同的“CSS Modules”提案,该提案计划在浏览器中获得原生支持。至少这是该提案的目标。遗憾的是,该提案与目前前端生态系统中使用的提案不同,但使用了类似的语法。通常,webpack 会与提案保持一致,所以这是一个需要考虑的问题。我们必须检查是否可以避免潜在的冲突。

HTML 作为入口点

遵循 Parcels 的示例,我们也希望原生支持 HTML 作为入口点。支持这一点将符合作为 Web 应用程序优化器的目标,因为 Web 应用程序通常从 HTML 开始。对于初学者来说,这也是一个巨大的开发体验改进,因为许多事情可以从 HTML 中推断出来。

控制生成的 HTML 也允许默认情况下更积极地优化。目前,我们默认情况下阻止重命名或拆分初始块,因为这需要为 HTML 生成额外的基础设施。

HTML 入口点也从 CSS 作为模块和资产模块中获益,因为这些资源也可以从 HTML 中引用(例如 <link rel=stylesheet /><img src="..." /><link rel=icon />)。

HTML 模块

关于在浏览器中原生支持导入 HTML 的提案也存在,我们会关注这一点,尤其是因为它与 HTML 入口点有很大的重叠。

SourceMap 性能

目前,在 webpack 中使用(完整)SourceMaps 非常昂贵,因为 SourceMap 处理的性能不是最佳。这是我们希望为 webpack 研究的,但也包括 terser,它默认情况下被 webpack 用作最小化器。

exports/imports package.json 字段

Node.js 14 已添加对 package.json 中 exports 字段的支持,以允许定义包的入口点。Webpack 5 遵循了这一点,甚至添加了额外的条件,例如 production/development

此后不久,Node.js 对此进行了进一步的补充,例如,它们还添加了 imports 字段用于私有导入。

这也是我们想要添加的内容。

改进 CommonJS 分析

虽然 ESM 是未来,但 npm 中仍然存在大量 CommonJS 包,并且仍在使用中。Webpack 5 为 CommonJS 模块添加了分析功能,以允许对大多数这些模块进行 Tree Shaking。

但我们还可以做得更多。虽然支持许多导出模式,但只支持少数导入模式。我们希望添加对更多模式的支持,以便为 CommonJS 模块提供更多优化。

模块联合的热模块替换

Webpack 5 添加了一项名为“模块联合”的新功能,它允许在运行时将多个构建集成在一起。目前,热模块替换 (HMR) 仅支持一次构建,并且更新无法在构建之间冒泡。我们希望在这里改进,并允许 HMR 更新在不同的构建之间冒泡,这将改善联合应用程序的开发。

提示系统

目前,webpack 向用户显示警告和错误。在构建过程中,有很多情况我们可以告诉用户一些事情,比如潜在的陷阱或优化机会,但它们不适合警告或错误,我们也不想用所有这些信息来轰炸输出。因此,我们希望添加另一个类别:提示。我们希望在构建过程中收集所有提示(插件也可以发出一些提示),但只在输出中显示有限数量的提示(默认情况下只显示一个)。这应该会为用户带来某种“你是否知道”的体验。

多线程

虽然持久缓存使缓存的构建“飞速”运行,但没有持久缓存的初始构建仍然有改进的空间。默认情况下,Node.js 中的 Javascript 执行是单线程的,但最近的添加允许使用worker_threads,这是一个类似于 WebWorkers 的 API。

这可以用于将工作分配到所有 CPU 上。Webpack 5 已经为此做了一些准备:例如,内部数据结构的序列化是可能的,并且工作队列支持插件。但其中一些部分仍然不清楚,需要进行实验。

这个项目已经在我们的投票列表中一段时间了,但投票的人并不多。这真的是人们需要的吗?

WebAssembly

目前,WebAssembly 处于实验阶段,默认情况下未启用。一旦提案达到第 4 阶段,我们就可以默认启用它。

这也有可能导致 WebAssembly 在生态系统中的更广泛采用。我认为我们可能会在 2021 年看到更多这方面的进展。

免责声明

此列表并非一成不变。Web 生态系统变化如此之快,以至于我们最终可能会实现完全不同的东西,而我们现在可能甚至没有意识到。考虑到我们目前的赞助情况,我们甚至不知道我们能在 webpack 上投入多少时间。

1 位贡献者

sokra