IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Webpack 长缓存(Long Term Cache)的一些探索

    雨浣潇湘发表于 2019-07-28 03:54:22
    love 0

    本文基于 Webpack@4.28.3 版本

    在现代前端工程中,为了更有效的利用浏览器和 CDN 的缓存,我们通常会给静态资源设置一个比较长的缓存时间(cache-control等),在有内容更新的时候,通过在静态资源路径中加入 hash 的方式来实现。具体可以参考这里: 大公司里怎样开发和部署前端代码?。

    但是在 Webpack 中,实现可靠的长期缓存并不容易。在 github 上,关于这个问题的 issue 已经讨论了三年之久。在 Webpack 4 时代,我们终于看到了一点希望。

    我们从一个最基础的 Webpack 配置开始:

    基础配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // webpack.config.js 
    const path = require('path');
    const webpack = require('webpack');
    module.exports = {
    entry: {
    main: './src/app',
    },
    output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].[hash:8].js'
    }
    };

    构建一下,得到如下结果:

    1
    2
    3
              Asset       Size  Chunks             Chunk Names
    app.23b699d1.js 115 KiB 0 [emitted] app
    index.html 253 bytes [emitted]

    看起来好像很完美。

    Vendor Chunks

    在实际项目中,我们通常会选择将公共模块提取成一个单独的文件,在 Webpack 3 时代,我们一般会选用 CommonsChunkPlugin,在 Webpack 4 中,我们则可以使用 splitChunks 来实现这一需求。这里我们将来自 node_modules 的模块提取成 vendors.js:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // webpack.config.js 
    const path = require('path');
    const webpack = require('webpack');
    module.exports = {
    entry: {
    main: './src/main',
    },
    output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].[hash:8].js'
    },
    optimization: {
    splitChunks: {
    cacheGroups: {
    commons: {
    test: /[\\/]node_modules[\\/]/,
    name: "vendors",
    chunks: "all"
    }
    }
    }
    }
    };

    打包结果如下:

    1
    2
    3
    4
                  Asset       Size  Chunks             Chunk Names
    app.6105d149.js 2.3 KiB 0 [emitted] app
    index.html 319 bytes [emitted]
    vendors.6105d149.js 114 KiB 1 [emitted] vendors

    你可能已经注意到了,我们的 app 和 vendors 的 hash 是相同的。这样打包,对 app 模块的任何更改同时也会导致我们的 vendors 模块的 hash 失效。

    要解决这个问题,我们必须在文件名中把 hash 改成 chunkhash。这是因为 hash 会为我们构建的所有内容生成一个全局哈希,而 chunkhash 只会使用它自己的模块中的内容来生成哈希值。

    修改 webpack 配置如下,再次构建,我们得到了两个不同的哈希值:

    1
    2
    3
    4
    5
    6
    7
    // webpack.config.js 
    // ...
    output: {
    path: path.join(__dirname, 'build'),
    filename: '[name].[chunkhash:8].js',
    },
    // ...
    1
    2
    3
    4
    Asset       Size  Chunks             Chunk Names
    app.41d05796.js 2.3 KiB 0 [emitted] app
    index.html 319 bytes [emitted]
    vendors.c2375741.js 114 KiB 1 [emitted] vendors

    现在,更改 app 模块中的内容, vendor 模块不会再受影响了(最新版本的 Webpack 中,不再需要为了 hash 的稳定单独提取出 runtime 了)。

    我们来添加一行代码测试一下:

    1
    2
    3
    // app.js
    // ...
    console.log("Hello World");

    打包之后,很完美:

    1
    2
    3
    4
     Asset       Size  Chunks             Chunk Names
    app.2a740abf.js 2.33 KiB 0 [emitted] app
    index.html 319 bytes [emitted]
    vendors.c2375741.js 114 KiB 1 [emitted] vendors

    增加依赖

    随着项目的增长,我们不可避免的出现了更多的依赖:

    1
    2
    3
    // app.js
    // ...
    import hello from './hello';

    我们再来构建一次。在这次构建中,显然我们只希望 app.js 的 hash 值更新,但是事情总是不尽如人意的:

    1
    2
    3
    4
    Asset       Size  Chunks             Chunk Names
    app.adcbdfaa.js 2.44 KiB 0 [emitted] app
    index.html 319 bytes [emitted]
    vendors.a7e68620.js 114 KiB 1 [emitted] vendors

    尽管我们的 vendors 模块没有任何变化,它的 hash 还是又一次改变了。原因又是一个 Webpack 中的细节: Webpack 会为每个 chunk 按顺序给出一个依次增加的 chunk id,随着新依赖的增加,chunk 顺序也可能会发生变化,于是 chunk id 也会随之更新。

    为了解决这个问题,我们引入了一个新的插件: NamedChunksPlugin。这是在 Webpack 2.4 版本中加入的一项特性,借助它我们可以让模块有自己的名字,而不是冷冰冰的数字(Webpack 4 中, development mode下已默认开启)。

    1
    2
    3
    4
    5
    // weback.config.js 
    // ...
    plugins: [
    new webpack.NamedChunksPlugin(),
    ]

    这样配置,Webpack 将使用唯一的 chunk 名称而不是其 id 来标识一个 chunk。

    在最新版的 Webpack 中,我们也可以使用 optimization.chunkIds 来达成同样的效果:

    1
    2
    3
    4
    5
    // weback.config.js 
    // ...
    optimization: {
    chunkIds: 'named'
    }

    我们在添加和不添加 hello.js 的情况下分别再构建一次,应该就可以看到,vendor 块的哈希是保持不变的了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 不添加 hello.js
    Asset Size Chunks Chunk Names
    app.25a10650.js 2.43 KiB app [emitted] app
    index.html 319 bytes [emitted]
    vendors.83fb4100.js 114 KiB vendors [emitted] vendors
    // 添加 hello.js
    Asset Size Chunks Chunk Names
    app.e66a4b27.js 2.34 KiB app [emitted] app
    index.html 319 bytes [emitted]
    vendors.1a4b3a3e.js 114 KiB vendors [emitted] vendors

    好吧,并没有。

    这是因为和 chunk id 类似,Webpack 同样会对 module 使用自增数字命名。类似地,我们可以使用 NamedModulesPlugin 或 HashedModuleIdsPlugin 来命名 module,从而使 hash 固定。其中 NamedModulesPlugin 使用 module 的路径来命名,生成的名字更可读,但是和使用 module 路径生成的4位(可能出现重复的情况下位数会增加) hash 命名 module 的 HashedModuleIdsPlugin 相比,会明显增大文件体积,适合用于开发环境,生产环境适合使用 HashedModuleIdsPlugin。

    和 chunkIds 类似,在最新版本的 Webpack 中,我们也可以使用 optimization.moduleIds 来配置这一功能(development 模式下 optimization.namedModules 已默认开启)。

    继续更新我们的 Webpack 配置:

    1
    2
    3
    4
    5
    6
    // webpack.config.js 
    // ...
    optimization: {
    moduleIds: 'hashed'
    }
    // ...

    再次更改 app 模块的内容打包,我们可以看到,更新前后其余文件的 hash 是不变的了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 不添加 hello.js
    Asset Size Chunks Chunk Names
    app.ca09737b.js 10.6 KiB app [emitted] app
    index.html 1.38 KiB [emitted]
    vendors.9d671f37.js 335 KiB vendors [emitted] [big] vendors
    // 添加 hello.js
    Asset Size Chunks Chunk Names
    app.9dc8a07a.js 10.8 KiB app [emitted] app
    index.html 1.38 KiB [emitted]
    vendors.9d671f37.js 335 KiB vendors [emitted] [big] vendors

    异步模块

    为了首屏性能等需求,我们不可避免的需要在项目中使用异步模块。我们添加一个异步模块再次打包:

    1
    2
    3
    4
    // router.js
    // ...
    path: '/async',
    component: () => import('./async.vue')
    1
    2
    3
    4
    5
     Asset      Size   Chunks                    Chunk Names
    0.03d30b56.js 2.04 KiB 0 [emitted]
    app.63c81cf2.js 13.6 KiB app [emitted] app
    index.html 1.38 KiB [emitted]
    vendors.2a461436.js 335 KiB vendors [emitted] [big] vendors

    可以看到 vendors 的 hash 又更新了。打开对应的文件,发现这样一段内容:

    1
    window["webpackJsonp"] || []).push([[0],

    显然,对新加入的这个异步模块的命名失效了,又变成了从 0 开始的自增序列。

    查看源码可以得知,NamedChunksPlugin 仅对有 name 的 chunk 有效,但是可以通过自定义 nameResolver 的方式来实现我们需要的功能:

    1
    2
    3
    4
    5
    6
    new webpack.NamedChunksPlugin(chunk => {
    if (chunk.name) {
    return chunk.name;
    }
    return Array.from(chunk.modulesIterable, m => m.id).join("_");
    });

    还有一个更简单的方案则是使用 Webpack 的魔法注释来给异步模块命名:

    1
    2
    3
    4
    // router.js
    // ...
    path: '/async',
    component: () => import(/* webpackChunkName: "async" */ './async.vue')

    打包结果如下:

    1
    2
    3
    4
    5
    Asset      Size   Chunks                    Chunk Names
    app.d10d68b9.js 13.6 KiB app [emitted] app
    async.4e35edb1.js 2.05 KiB async [emitted] async
    index.html 1.38 KiB [emitted]
    vendors.9d671f37.js 335 KiB vendors [emitted] [big] vendors

    可以看到,vendors 的 hash 恢复到了之前的状态。

    是不是感觉好像还缺点什么?

    CSS模块

    在生产环境中,我们通常会将 css 模块打包为独立文件。我们增加一个 css 模块来看一下:

    1
    2
    3
    // app.js
    // ...
    import './hello.css';
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // webpack.config.js 
    // ...
    plugins: [
    new MiniCssExtractPlugin({
    filename: "[name].[chunkhash:8].css",
    chunkFilename: "[name].[chunkhash:8].css"
    })
    ]
    // ...

    改变一下 hello.css 的内容,打包两次,对比如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     Asset      Size   Chunks                    Chunk Names
    app.884f7d50.css 28 bytes app [emitted] app
    app.884f7d50.js 13.8 KiB app [emitted] app
    async.4e35edb1.js 2.05 KiB async [emitted] async
    index.html 1.43 KiB [emitted]
    vendors.9d671f37.js 335 KiB vendors [emitted] [big] vendors


    Asset Size Chunks Chunk Names
    app.1bedc677.css 29 bytes app [emitted] app
    app.1bedc677.js 13.8 KiB app [emitted] app
    async.4e35edb1.js 2.05 KiB async [emitted] async
    index.html 1.43 KiB [emitted]
    vendors.9d671f37.js 335 KiB vendors [emitted] [big] vendors

    只修改了 CSS 文件,app.js 的 hash 值又变了。很容易理解,毕竟它们属于同一 chunk。
    为了解决这个问题,Webpack 4.3 中引入了一个新的概念: contenthash。
    修改 Webpack 配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // webpack.config.js 
    // ...
    output: {
    filename: "app.[contenthash:8].js",
    chunkFilename: "[name].[contenthash:8].js"
    },
    plugins: [
    new MiniCssExtractPlugin({
    filename: "[name].[contenthash:8].css",
    chunkFilename: "[name].[contenthash:8].css"
    })
    ]
    // ...

    再次打包对比更改 css 文件前后的变化,只有 css 文件的 hash 改变了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Asset      Size   Chunks                    Chunk Names
    app.a8ec5b81.js 13.8 KiB app [emitted] app
    app.eb9b9ca0.css 28 bytes app [emitted] app
    async.ca0780b4.js 2.05 KiB async [emitted] async
    index.html 1.43 KiB [emitted]
    vendors.bf514953.js 335 KiB vendors [emitted] [big] vendors

    Asset Size Chunks Chunk Names
    app.a8ec5b81.js 13.8 KiB app [emitted] app
    app.da6259d4.css 29 bytes app [emitted] app
    async.ca0780b4.js 2.05 KiB async [emitted] async
    index.html 1.43 KiB [emitted]
    vendors.bf514953.js 335 KiB vendors [emitted] [big] vendors

    external 模块

    有些场景下,我们需要从 CDN 引入一个模块,以 jQuery 为例,一般会如下配置:

    1
    2
    <!-- index.html -->
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    1
    2
    3
    4
    5
    6
    //webpack.config.js
    module.exports = {
    externals: {
    jquery: 'jQuery'
    }
    };
    1
    2
    // app.js
    import $ from 'jquery';

    和异步模块类似,NamedChunksPlugin 也不会对 externals 模块起作用,我们可以参考这里,使用 NameAllModulesPlugin 来做一些相关的配置,从而为它命名。

    展望

    目前,Webpack 已经发布了 v5.0.0-alpha.3 版本。

    在 Webpack 5 中,采用了全新的算法来生成 chunkIds 和 moduleIds,在打包后的文件大小和控制缓存之间有了一个更好的平衡。

    在 Webpack 4 的更新说明中,开发团队提到了 Webpack 5 中会有开箱即用的长缓存配置,在这个 alpha 版本中,我们也看到了 cache: { type: "filesystem" } 这样的配置,然而,这个策略依然是实验性质的。希望在正式版本中能一劳永逸的解决这个问题。

    参考

    https://medium.com/webpack/predictable-long-term-caching-with-webpack-d3eee1d3fa31
    https://webpack.js.org/guides/caching/
    https://zhuanlan.zhihu.com/p/38456425
    https://segmentfault.com/a/1190000016355127
    https://segmentfault.com/a/1190000015919928
    https://github.com/pigcan/blog/issues/9



沪ICP备19023445号-2号
友情链接