从零到一学会webpack 03-缓存

438 阅读5分钟

接上一篇 从零到一学会webpack 02-搭建开发环境和热更新

这一篇主要讲缓存,内容基本与webpack的文档部分-指南-缓存保持一致~已经了解过的同学不必要再看了

我们使用 webpack 来打包我们的模块化后的应用程序,webpack 会生成一个可部署的 /dist 目录,然后把打包后的内容放置在此目录中。只要 /dist 目录中的内容部署到 server 上,client(通常是浏览器)就能够访问此 server 的网站及其资源。而最后一步获取资源是比较耗费时间的,这就是为什么浏览器使用一种名为 缓存 的技术。可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。

此指南的重点在于通过必要的配置,以确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件。

是的,上面这段文字,我是直接复制的官网~

看前面两篇文章应该已经看到了,我在output配置中,已经使用了如下表示:

  output: {
    path: path.resolve(__dirname, "dist/"),
    filename: "[name].bundle.js"
  },

这里的[name] 我们称之为:substitutions --- 也就是一个占位符,执行build后被会替换。

在客户端向服务端请求资源时候,对应的资源匹配符一般是 url/path/filename,因此,当filename不发生变化时候,客户端会命中缓存,当我们文件发生修改,构建后的文件名字就需要更改,这样客户端就不会使用缓存文件,我们回到最简单的代码和配置。

├── dist
├── public
│   └── index.html
├── src
│   ├── App.css
│   ├── App.jsx
│   └── index.js
├── webpack.config.js
├── package.json
└── yarn.lock
├── .babelrc

webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');


module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist/"),
    filename: "index.js"
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
        options: { presets: ["@babel/env"] }
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  },
  resolve: { extensions: ["*", ".js", ".jsx"] },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: '缓存',
      template: './public/index.html'
    }),
  ]
};

以上对于单入口的配置,会生成固定的fimename为index.js的文件,但是通过多个入口起点(entry point)、代码拆分(code splitting)或各种插件(plugin)创建多个 bundle,应该使用以下一种替换方式,来赋予每个 bundle 一个唯一的名称

设置output filename

  1. 使用入口名称:
module.exports = {
  //...
  output: {
    filename: '[name].bundle.js',
  },
};
  1. 使用内部 chunk id
module.exports = {
  //...
  output: {
    filename: '[id].bundle.js',
  },
};
  1. 使用由生成的内容产生的 hash:
module.exports = {
  //...
  output: {
    filename: '[contenthash].bundle.js',
  },
};
  1. 结合多个替换组合使用:
module.exports = {
  //...
  output: {
    filename: '[name].[contenthash].bundle.js',
  },
};
  1. 使用函数返回 filename:
module.exports = {
  //...
  output: {
    filename: (pathData) => {
      return pathData.chunk.name === 'main' ? '[name].js' : '[name]/[name].js';
    },
  },
};

更复杂和更高级的用法,参见webpack的文档:outputfilename

你可以简单地对以上四种方式进行设置看看build后结果。

这里我们设置为:

module.exports={
    //...
    output: {
    path: path.resolve(__dirname, "dist/"),
    filename: "[name].[contenthash].js"
  },
}

build后输出结果为:

├── index.html
├── main.2a5c2d475953a2c073fc.js
└── main.2a5c2d475953a2c073fc.js.LICENSE.txt // 不用管

可以看到,生成的文件名是根据我们配置的方式产生的

提取引导模板(extracting boilerplate)

webpack 还提供了一个优化功能,可使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 来为所有 chunk 创建一个 runtime bundle

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const MiniCssExtractPlugin = require('mini-css-extract-plugin');


module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist/"),
    filename: "[name].[contenthash].js"
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        loader: "babel-loader",
        options: { presets: ["@babel/env"] }
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"]
      }
    ]
  },
  resolve: { extensions: ["*", ".js", ".jsx"] },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: '管理输出',
      template: './public/index.html'
    }),
  ],
  optimization: {
    runtimeChunk: 'single',
  },
};

build后文件为:

├── index.html
├── main.e015ebc8a50a50e241af.js
├── main.e015ebc8a50a50e241af.js.LICENSE.txt
└── runtime.fb3fdb31b8fba3a87894.js

可以看到,新生成了一个runtime.fb3fdb31b8fba3a87894.js文件。

我们继续:将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源,同时还能保证 client 代码和 server 代码版本一致,这可以通过使用 SplitChunksPlugin 示例 2 中演示的 SplitChunksPlugin 插件的 cacheGroups 选项来实现

    // ...
    optimization: {
        runtimeChunk: 'single',
        splitChunks: {
        cacheGroups: {
        vendor: {
           test: /[\\/]node_modules[\\/]/,
           name: 'vendors',
           chunks: 'all',
         },
       },
     },
    },

生成的代码文件为,仔细看看main的代码大小:main中不包含任何依赖相关代码了

├── index.html
├── main.7f74a4145b9a5f89c983.js
├── runtime.fb3fdb31b8fba3a87894.js
├── vendors.60580f0863c3f8e70856.js
└── vendors.60580f0863c3f8e70856.js.LICENSE.txt

接下来,我们再src目录新增一个print.js

export function print() {
  console.log('print!!!')
}

再App.jsx中引入print方法并执行

import React from "react";
import print from './print';
import "./App.css";

function App() {
  print();
  return (
    <div className="App">
      <h1> Hello, World </h1>
    </div>
  );
}
export default App;

build后生成的文件:

├── index.html
├── main.ecda747780e86fe47ecf.js
├── runtime.b8a0cf32c66c8e3d96c6.js
├── vendors.ed2678b03f97207cadab.js
└── vendors.ed2678b03f97207cadab.js.LICENSE.txt

我们期望的是main的filename发生修改:因为我们修改的只是打包进入main文件的代码,但是事实上发现,生成的所有文件名都发生了修改~

这是因为每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。因此,简要概括:

main bundle 会随着自身的新增内容的修改,而发生变化。 vendor bundle 会随着自身的 module.id 的变化,而发生变化。 manifest runtime 会因为现在包含一个新模块的引用,而发生变化。 第一个和最后一个都是符合预期的行为,vendor hash 发生变化是我们要修复的。我们将 optimization.moduleIds 设置为 'deterministic':

  optimization: {
    runtimeChunk: 'single',
    moduleIds: 'deterministic',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },

再次执行build一次、再删除print.js和print相关代码,会发现,vendor hashd都没有发生变化~~~

下一篇,我们尝试一下同构应用 从零到一学会webpack 04-构建同构应用