[译] webpack 5 之持久化缓存

原文连接前端

继 webpack v5-beta0 发布后,官方又发布了持久化缓存指南。node

Opt-in

首先,要注意的是默认状况下不会启用持久化缓存。你能够自行选择启用。webpack

为什么如此? webpack 旨在注重构建安全而非性能。 咱们没有打算默认启用这一功能,主要缘由在于此功能虽然有 95% 概率提高性能,但仍有 5% 的概率中断你的应用程序/工做流/构建。web

这可能听起来很糟,但相信我它并不是如此。 只不过须要开发人员进行额外的操做来配置它。typescript

序列化与反序列化功能具备无需配置的开箱即用体验,但开箱即用的部分可能导致缓存失效。数据库

什么是缓存失效? webpack 须要确认 entry 的缓存什么时候会失效,并在失效时再也不将其用于构建。 所以,当你应用程序修改文件时,就会发生此状况。npm

示例:修改 magic.js。 webpack 必须让 entry 为 magic.js 的缓存失效。 构建将从新处理该文件,即运行 babel,typescript 诸如此类工具,从新解析文件并运行代码生成。 webpack 可能还会导致 entry 为 bundle.js 的缓存失效。 而后根据原模块从新构建此文件。json

为此,webpack 追踪了每一个模块的 fileDependencies contextDependencies 以及 missingDependencies,并建立了文件系统快照。 此快照会与真实文件系统进行比较,当检测到差别时,将触发对应模块的从新构建。缓存

webpack 给 bundle.js 的缓存 entry 设置了一个 etag,它为全部贡献者的 hash 值。 比较这个 etag,只有当它与缓存 entry 匹配时才能使用。安全

webpack 4 中的内存缓存也依赖上述这些。 从开发人员角度来讲,这些都可以开箱即用,无需额外配置。 但对于 webpack 5 的持久化缓存来讲,却充满着挑战。

如下操做均会让 webpack 使 entry 缓存失效:

  • 当 npm 升级 loader 或 plugin 时
  • 当更改配置时
  • 当更改在配置中读取的文件时
  • 当 npm 升级配置中使用的 dependencies 时
  • 当不一样命令行参数传递给 build 脚本时
  • 当有自定义构建脚本并进行更改时

这变得很是棘手。 开箱即用的状况下,webpack 没法处理全部这些状况。 这就是咱们为何选择安全的方式,并将持久化缓存变为可选特性的缘由。 咱们但愿读者能够学习如何启用持久化缓存,觉得你提供正确的提示。 咱们但愿你知道须要使用哪一种配置来处理你自定义的构建脚本。

构建依赖(dependencies),缓存版本(version)和缓存名(name)

为了处理构建过程当中的依赖关系,webpack 提供了三个新工具:

构建依赖(Build dependencies)

此为全新的配置项 cache.buildDependencies,它能够指定构建过程当中的代码依赖。 为了使它更简易,webpack 负责解析并遵循配置值的依赖。

值类型有两种:文件和目录。 目录类型必须以斜杠(/)结尾。其余全部内容都解析为文件类型。

对于目录类型来讲,会解析其最近的 package.json 中的 dependencies。 对于文件类型来讲,咱们将查看 node.js 模块缓存以寻找其依赖。

示例:构建一般取决于 webpack 自己的 lib 文件夹: 你能够这样配置:

cache.buildDependencies: {
    defaultWebpack: ["webpack/lib/"]
}
复制代码

webpack/lib 或 webpack 依赖的库(如,watchpackenhanced-resolved 等)发生任何变化时,其缓存将失效。 webpack/lib 已经是默认值,默认状况下无需配置。

另外一个示例:构建依旧取决于你的配置文件。 具体配置以下:

cache.buildDependencies: {
    config: [__filename]
}
复制代码

__filename 变量指向 node.js 中的当前文件。

当配置文件或配置文件中经过 require 依赖的任何内容发生更改时,也会使得持久化缓存失效。 当配置文件经过 require() 引用了全部使用过的插件时,它们也会成为构建依赖项。

若是配置文件经过 fs.readFile 读取文件,则将不会成为构建依赖项,由于 webpack 仅遵循 require()。 你须要手动将此类文件添加到 buildDependencies 中。

缓存版本(Version)

构建的某些依赖项不能单纯的依靠对文件的引用,如,从数据库读取的值,环境变量或命令行上传递的值。 对于这些值,咱们给出了新的配置项 cache.version

cache.version 类型为 string。传递不一样的字符串将使持久化缓存失效。

示例:你的配置中可能会读取环境变量中的 GIT_REV 并将其与 DefinePlugin 一块儿使用以将其嵌入到 bundle 中。 这使得 GIT_REV 成为你构建的依赖项。 具体配置以下:

cache: {
    version: `${process.env.GIT_REV}`
}
复制代码

缓存名(Name)

在某些状况下,依赖关系会在多个不一样的值间切换,而且对于每一个值更改都会使得持久化缓存失效,这显然是浪费资源的。 对于这类值,咱们给出了新的配置项 cache.name

cache.name 类型为 string。传递值将建立一个隔离且独立的持久化缓存。

cache.name 被用于对文件名进行持久化缓存。确保仅传递短小且 fs-safe 的名称。

示例:你的配置可使用 --env.target mobile|desktop 参数为移动端或 PC 用户建立不一样的构建。 具体配置以下:

cache: {
    name: `${env.target}`
}
复制代码

性能优化

对大部分 node_modules 进行哈希处理并加盖时间戳以生存构建和常规依赖项,其代价很是昂贵,而且还会大大下降 webpack 的执行速度。 为避免这种状况出现,webpack 引入了相关的性能优化,默认状况下会跳过 node_modules,并使用 package.json 中的 versionname 做为数据源。

此优化将用于配置项 cache.managedPaths 中的全部 path。 它默认为 webpack 安装了 node_modules 目录。

启用此优化后,请勿手动编辑 node_modules。 你可使用 cache.managedPaths: [] 禁用它。

当使用 Yarn PnP 时,将启用另外一个优化。 因为缓存内容不可变,yarn 缓存中的全部文件都将彻底跳过哈希和时间戳的操做(甚至不会追踪 versionname)。

此操做由配置项 cache.immutablePaths 控制。 启用 Yarn PnP 时,默认为安装了 webpack 的 yarn 缓存。

不要手动编辑 yarn 缓存,由于这根本不可行。

使用持久化缓存

确保你已阅读并理解以上信息!

此为启用持久化缓存的典型配置:

cache: {
    type: "filesystem",
    buildDependencies: {
        config: [ __filename ] // 当你 CLI 自动添加它时,你能够忽略它
    }
}
复制代码

Watching

持久化缓存可用于单独构建和连续构建(watch)。

当设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。 从缓存读取时,会先查看内存缓存,若是内存缓存未找到,则降级到文件系统缓存。 写入缓存将同时写入内存缓存和文件系统缓存。

文件系统缓存不会直接将对磁盘写入的请求进行序列化。它将等到编译过程完成且编译器处于空闲状态才会执行。 如此处理的缘由是序列化和磁盘写入会占用资源,而且咱们不想额外延迟编译过程。

针对单一构建,其工做流为:

  • Loading cache
  • Building
  • Emitting
  • Display results (stats)
  • Persisting cache (if changed)
  • Process exits

针对连续构建(watch),其工做流为:

  • Loading cache
  • Building
  • Emitting
  • Display results (stats)
  • Attach filesystem watchers
  • Wait cache.idleTimeoutForInitialStore
  • Persisting cache (if changed)
  • On change:
    • Building
    • Emitting
    • Display results (stats)
    • Wait cache.idleTimeout
    • Persisting cache (if changed)

你会发现两个新的配置项 cache.idleTimeoutcache.idleTimeoutForInitialStore,它们控制着持久化缓存以前编译器必须空闲的时长。 cache.idleTimeout 默认为 60s,cache.idleTimeoutForInitialStore 默认为 0s。 因为序列化阻止了事件循环,所以在序列化缓存时不进行缓存检测。 此延迟尝试避免因为快速编辑文件,而在 watch 模式下致使从新编译形成的延迟,同时尝试为下一次冷启动保持持久化缓存的最新状态。 这是一个折中的解决方案,能够设置适合你工做流的值。较小的值会缩短冷启动时间,但会增长延迟从新构建的风险。

错误处理

发生错误要恢复持久化缓存的方式,能够经过删除整个缓存并进行全新的构建,或者经过删除有问题的缓存 entry 并使得该项目保持未缓存状态来进行。

在这种状况下,webpack 的 logger 会发出警告。 欲了解更多,请参阅 infrastructureLogging 的配置项。


Details

正常使用不须要如下信息。

使用 webpack 的高级工具指南

封装 webpack 的工具能够选择其余默认值。 当不容许使用自定义扩展的 webpack 时,因为能够彻底控制全部构建的依赖项,所以能够默认打开持久化存储。

CLI 指南

默认状况下,使用 webpack 的 CLI 可能会添加一些构建依赖关系,而 webpack 自己不会。

  • 默认状况下,CLI 会将 cache.buildDependencies.defaultConfig 设置为所用的配置文件
  • CLI 会将命令行参数附加到 cache.version
  • 使用命令行参数时,CLI 可能会在 cache.name 中添加注释。

调试信息

使用以下配置,将输出额外的调试信息:

infrastructureLogging: {
    debug: /webpack\.cache/
}
复制代码

内部工做流

  • webpack 读取缓存文件。
    • 没有缓存文件 -> 未构建缓存
    • 缓存文件中的 versioncache.version 不匹配 -> 没有构建缓存
  • webpack 将解析快照(resolve snapshot)与文件系统进行对比
    • 匹配到 -> 继续后续流程
    • 没有匹配到:
      • 再次解析全部解析结果(resolve results
        • 没有匹配到 -> 未构建缓存
        • 匹配到 -> 继续后续流程
  • webpack 将构建依赖快照(build dependencies snapshot)与文件系统进行对比
    • 没有匹配到 -> 未构建缓存
    • 匹配到 -> 继续后续流程
  • 对缓存 entry 进行反序列化(在构建过程当中对较大的缓存 entry 进行延迟反序列化)
  • 构建运行(有缓存或没有缓存)
    • 追踪构建依赖关系
      • 追踪 cache.buildDependencies
      • 追踪已使用的 loader
  • 新的构建依赖关系已解析完成
    • 解析依赖关系已追踪
    • 解析结果已追踪
  • 建立来自全部新解析依赖项的快照
  • 建立来自全部新构建依赖项的快照
  • 持久化缓存文件序列化到磁盘

序列化

全部支持序列化的 class 都须要注册一个序列化器:

webpack.util.serialization.register(Constructor, request, name, serializer);
复制代码

Constructor 应为一个 class 或构造器函数。 对于任何须要序列化的对象的 object.constructor 将被用于查找序列化器(serializer)。

request 将被用于加载调用 register 模块。 它应指向当前模块。 它将以这种方式使用:require(request)

name 被用于区分具备相同 request 的多个 register 调用。

serializer 是至少拥有 serializedeserialize 两个方法的对象。

当需序列化对象时,请调用 serializer.serialize(object, context)context 是至少拥有一个 write(anything) 方法的对象 此方法将内容写入输出流。 传递的值也会被序列化。

当须要反序列化对象时,请调用 serializer.deserialize(context)context 是至少拥有一个 read(): anything 方法的对象。 此方法会反序列化输入流中的某些内容。 deserialize 必须返回反序列化后的对象。

serializedeserialize 应以相同的顺序读取和写入相同的对象。

示例:

// some-module/lib/MyClass.js
class MyClass {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        this.c = undefined;
    }
}

register(MyClass, "some-module/lib/MyClass", null, {
    seralize(obj, { write }) {
        write(obj.a);
        write(obj.b);
        write(obj.c);
    }
    deserialize({ read }) {
        const obj = new MyClass(read(), read());
        obj.c = read();
        return obj;
    }
});
复制代码

基本数据类型和引用数据类型的序列化器都已被注册,即 string,number,Array,Set,Map,RegExp,plain objects,Error。

勘误

如对译文有疑问,欢迎评论。

关注咱们

相关文章会在公众号首发,扫描下方二维码关注咱们,咱们将提供前端相关最新最优含量的资讯。

相关文章
相关标签/搜索