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 等资源,体积明显变小。

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

相关推荐
wearegogog1235 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars5 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤5 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·5 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°6 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
qq_419854056 小时前
CSS动效
前端·javascript·css
烛阴6 小时前
3D字体TextGeometry
前端·webgl·three.js
桜吹雪7 小时前
markstream-vue实战踩坑笔记
前端
C_心欲无痕7 小时前
nginx - 实现域名跳转的几种方式
运维·前端·nginx
花哥码天下7 小时前
恢复网站console.log的脚本
前端·javascript·vue.js