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

自 webpack 5 正式发布以来,已经过去了近 2 个月。由于赞助情况,我们无法像我们希望的那样投入大量时间在 webpack 上。仅就我个人而言(@sokra),我享受了短暂的休息,并做了一些副项目。讽刺的是,当我使用 webpack 5 及其所有尖端功能(资产模块、Worker 支持、持久化缓存)时,我发现了 webpack 5 中更多用户在将其项目升级到 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 function` 或 `cannot read property 'call' of undefined` 错误。

一个潜在的修复方法是将无副作用模块包含在所有入口点中,但由于这个模块实际上并非必需,这将浪费打包体积。因此,我们走了另一条路,这需要开发一个新功能:`运行时依赖的代码生成`。这允许生成根据其执行的运行时而表现不同的代码。

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

关于这个小插曲就到这里,希望它没有那么无聊...

2021 年路线图

因此,假设我们能够解决赞助问题,2021 年计划如下:

进一步稳定

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

EcmaScript 模块

EcmaScript 模块 (ESM) 正在缓慢普及。在编写代码方面,它们已经是事实上的标准。在浏览器支持方面,情况也相当不错(IE11 和一些较旧的移动浏览器除外)。浏览器在对 WebWorkers 的 ESM 支持方面仍有些不足。

也可以生成在 `type=module` 脚本标签中运行的打包文件,但目前这样做的好处很少。

webpack 中有多个可以改进 ESM 支持的领域:

ESM 作为代码块加载机制

当目标是 web 时,webpack 通过 `script` 标签加载代码块。当目标是 node.js 时,webpack 通过 `require` 或 `fs` + `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 外部模块(import)

Webpack 允许定义 `externals`,这些模块不会被打包,但在运行时存在。外部模块有许多类型,从全局变量到 CommonJs/AMD/System,再到从经典 script 标签加载。甚至可以使用 `import()` (`type: "import"`) 来加载外部模块,但 `import` (`type: "module"`) 尚不能使用。

有趣的是,尽管 `type: "module"` 尚未支持,但当写入例如 `import x from "https://example.com/module.js"` 时,webpack 已经将其用作默认值。默认选择是为了在不引入破坏性更改的情况下,无缝地添加对 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-loader`、`style-loader` 或 `mini-css-extract-plugin` 支持 CSS。这运作得相当好,但我认为通过在 webpack 中将 CSS 作为原生模块类型来支持,我们可以做得更多。

一个主要的好处将是开发者体验:`mini-css-extract-plugin` 的配置不是最简单的,摆脱它将大大简化开发者的工作。这并不意味着您不能在此之上添加额外的自定义。我看到许多开发者不使用原始 CSS,而是在其之上使用预处理器(有了原生 CSS 支持,它看起来会像这样:`{ test: /\.sass$/, type: "stylesheet", use: "sass-loader" }`)。

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

借助 webpack 对应用程序的全局了解,可以进行一些 CSS Modules 特有的潜在优化:CSS 规则可以被拆分成更小的规则,以避免重复公共属性。这可以大大减小有效载荷,因为输出的 CSS 包含更少的重复属性(原子 CSS)。

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

HTML 作为入口点

效仿 Parcel 的例子,我们也希望原生支持 HTML 作为入口点。支持这一点将符合 web 应用优化器的目标,因为 web 应用通常以 HTML 开始。对于初学者来说,这也是一个巨大的开发者体验改进,因为许多东西都可以从 HTML 中推断出来。

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

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

HTML 模块

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

SourceMap 性能

目前在 webpack 中使用(完整的)SourceMap 相当耗费资源,因为 SourceMap 处理的性能不是最好的。这是我们希望在 webpack 中研究的问题,也包括 terser,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