webpack学习[三]:代码分离和缓存

本文主要记录本人对于 webpack 代码分离 和 缓存的学习。

一、代码分离

代码分离能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常用的代码分离方法有三种:

  • 入口起点 :使用 entry 配置手动地分离代码。
  • 防止重复 :使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

1.1 入口起点

通过手动的方式配置多个入口,但是这种方式会存在如下隐患。

  • 如果入口 chunk 之间包含一些重复的模块,那么这些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能动态地拆分应用程序逻辑中的核心代码。

例如:现有如下两个入口文件index.js, another.js 都引用了lodash模块,代码如下:

index.js

javascript 复制代码
import _ from 'lodash';

console.log(_.join(['index', 'entry'], ' '));

another.js

javascript 复制代码
import _ from 'lodash';

console.log(_.join(['another', 'entry'], ' '));

webpack.config.js

javascript 复制代码
const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}

打包结果如下:

可以看出同一个模块lodash被重复打包到another.bundle.jsindex.bundle.js中。

1.2 防止重复

针对上述问题,这里介绍如何移除重复的模块。

(1)入口依赖

在配置文件中配置 dependOn 选项,以在多个 chunk 之间共享模块:

javascript 复制代码
entry: {
    index: {
      import: './src/index.js',
      dependOn: 'shared'
    },
    another: {
      import: './src/another.js',
      dependOn: 'shared'
    },
    shared: 'lodash'
},

可以看到,loadsh模块被单独打包到shared.bundle.js中,这样就防止了重复引用。

如果想要在一个 HTML 页面上使用多个入口,还需设置:

javascript 复制代码
module.exports {
    ...
    optimization: {
        runtimeChunk: 'single'
    }
}

因为不同入口文件引用了同一模块,这个模块可能会被实例化多次,从而导致程序出现错误,具体可见Multiple Entry Points Per Page

可以看到,除了 shared.bundle.jsindex.bundle.jsanother.bundle.js 之外,还生成了一个 runtime.bundle.js 文件。HMTL文件中引入该文件即可。

尽管 webpack 允许每个页面使用多入口,但在可能的情况下,应该避免使用多入口,而使用具有多个导入的单入口:entry: { page: ['./analytics', './app'] }。这样可以获得更好的优化效果,并在使用异步脚本标签时保证执行顺序一致。

多入口:Webpack 会从每个入口点开始打包,生成独立的包。这意味着每个入口点都会生成一个独立的bundle。

单入口多个导入:多个模块打包到同一个bundle中。

例如:entry: { page: ['./analytics', './app'] } 它表示的含义是数组最后一个文件./app是资源的入口文件,数组其余文件会预先构建到入口文件。等价于 ./app 文件在先引入 ./analyticsimport './analytics';),然后设置入口entry: { page: './app' }

(2)SplitChunksPlugin

如果存在大量公共模块的话,通过手动方式去配置 shared , 不仅繁琐,而且会有遗漏。对此,SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。

将配置修改如下:

javascript 复制代码
module.exports = {
  entry: {
    index: './src/index.js',
    another: './src/another.js'
  },
  ...
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

再进行打包,可以看到公共模块lodash被分离成单独的chunk了

1.3 动态导入

webpack 提供了两个类似的技术实现动态拆分代码。第一种,也是推荐选择的方式,是使用符合 ECMAScript 提案import() 语法 实现动态导入。第二种则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure

例如,我们将 index.js内容修改如下:

javascript 复制代码
import('lodash').then(({default: _}) => {
  console.log(_.join(['index', 'entry'], ' '));
})

webpack配置修改如下:

css 复制代码
module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
  },
  ...
}

可以看到打包后的结果中,lodash会被分离到一个单独的bundle中。

1.4 预获取/预加载模块

Webpack v4.6.0+ 增加了对预获取(prefetch)和预加载(preload)的支持。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出"resource hint",来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

(1)预获取

试想一下下面的场景:现在有一个 HomePage 组件,该组件内部渲染了一个 LoginButton 组件,点击后按钮后可以按需加载 LoginModal 组件。

go 复制代码
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

上面的代码在构建时会生成
<link rel="prefetch" href="login-modal-chunk.js">

并追加到页面头部,指示浏览器在闲置时间预获取 login-modal-chunk.js 文件。

只要父 chunk 完成加载,webpack 就会添加预获取提示。

(2)预加载

预加载是一种预先加载文件或资源的技术,通常用于在需要时快速访问它们。

预加载的优点是,当用户需要访问已预加载的文件或资源时,它们可以立即被使用,从而减少加载时间。预加载的缺点是,它会增加页面的整体加载时间,因为需要预先加载所有文件或资源

预获取和预加载的区别

  • 预加载 chunk 会在父 chunk 加载时以并行方式开始加载;而预获取 chunk 会在父 chunk 加载结束后开始加载。
  • 预加载 chunk 具有中等优先级,并会立即下载;而预获取 chunk 则在浏览器闲置时下载。
  • 预加载 chunk 会在父 chunk 中立即请求,用于当下时刻;而预获取 chunk 则用于未来的某个时刻。
  • 浏览器支持程度不同。

预加载和懒加载的区别

  • 预加载是加载所有资源,在页面加载时就已经把所有图片、数据等都加载完成,在页面打开后就可以直接使用,无需等待加载。
  • 懒加载是当页面打开后,对页面上的图片等附件进行预先加载,当所有图片都加载完成,再显示该页面。懒加载可以减少不必要资源的加载,只在需要时才加载资源。

二、缓存

通常,webpack打包后的内容部署到服务器上后,浏览器便可访问服务器获取该资源,但这通常比较耗时。对此,浏览器通常会采用缓存技术。命中缓存可以降低网络流量,使网站加载速度更快。

然而,如果在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。对此,webpack提供了一些配置确保 webpack 编译生成的文件能够被客户端缓存;而在文件内容变化后,能够请求到新的文件。

2.1 配置输出文件文件名

通常情况下,我们会输出文件名设置为main.jsbundle.js这类静态字符串。但是这样每次打包后的资源文件名不变,这样可能会让浏览器认为其未更新,从而使用缓存版本。对此,webpack 提供了一种称为 substitution(可替换模板字符串 的方式,通过带括号字符串来模板化文件名。

例如,webpack 提供了如下模替换模板字符串(通过 webpack 内部的TemplatedPathPlugin):

可在 chunk 层面进行替换的内容:

模板 描述
[id] 此 chunk 的 ID
[name] 如果设置,则为此 chunk 的名称,否则使用 chunk 的 ID
[chunkhash] 此 chunk 的 hash 值,包含该 chunk 的所有元素
[contenthash] 此 chunk 的 hash 值,只包括该内容类型的元素,受 optimization.realContentHash 影响)

可在文件层面替换的内容:

模板 描述
[file] filename 和路径,不含 query 或 fragment
[query] 带前缀 ? 的 query
[fragment] 带前缀 # 的 fragment
[base] 只有 filename(包含扩展名),不含 path
[path] 只有 path,不含 filename
[name] 只有 filename,不含扩展名或 path
[ext] 带前缀 . 的扩展名(对 output.filename 不可用)

假设我们现在将输入输出设置如下:

JavaScript 复制代码
{
    entry: {
        index: './src/index.js'
    },
    
    output: {
        filename: [name].[contenthash].bundle.js,
        path: path.resolve(__dirname, 'dist')
    }
}

如下图,这是打包后的资源文件名:

如果我们修改文件内容,再次执行 webpack 进行打包,可以看到打包后的资源文件名也发生了改变。

如果文件内容不变,再次执行 webpack 打包,官网说也会生成新的资源文件名。但 webpack5 好像会去直接使用缓存的文件,如下图所示:

2.2 提取引导模板

上述SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中。webpack 还提供了一个优化功能,可以使用 optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk。将其设置为 single 以为所有 chunk 创建一个 runtime bundle。

由于像 lodashreact 这样的第三方库很少像本地源代码一样频繁修改,因此通常推荐将第三方库提取到单独的 vendor chunk 中。这一步将减少客户端对服务器的请求,同时保证自身代码与服务器一致。

可以添加如下配置:

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

然后执行webpack打包结果如下:

可以看到, index.[].bundle.js 不再包含 loadsh 等资源,体积明显变小。

本文到这里就结束啦,主要内容源于文档。

相关推荐
小小小小宇3 小时前
TS泛型笔记
前端
小小小小宇3 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping3 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇4 小时前
前端PerformanceObserver使用
前端
zhangxingchao5 小时前
Flutter中的页面跳转
前端
烛阴5 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝6 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇6 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军7 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加7 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript