「Webpack」从0到1学会 code splitting

掘金引流终版.gif

构建专栏系列目录入口javascript

焦传锴,微医前端技术部平台支撑组。不是吧阿 sir,又要兼容 IE?前端

1、前言

在默认的配置状况下,咱们知道,webpack 会把全部代码打包到一个 chunk 中,举个例子当你的一个单页面应用很大的时候,你可能就须要将每一个路由拆分到一个 chunk 中,这样才方便咱们实现按需加载。java

代码分离是 webpack 中最引人注目的特性之一。此特性可以把代码分离到不一样的 bundle 中,而后能够按需加载或并行加载这些文件。代码分离能够用于获取更小的 bundle,以及控制资源加载优先级,若是使用合理,会极大影响加载时间。node

2、关于代码分割

接下来咱们会分别分析不一样的代码分隔方式带来的打包差别,首先咱们的项目假设有这两个简单的文件👇react

index.jsjquery

import { mul } from './test'
import $ from 'jquery'

console.log($)
console.log(mul(2, 3))

复制代码

test.jswebpack

import $ from 'jquery'

console.log($)

function mul(a, b) {
    return a * b
}

export { mul }

复制代码

能够看到如今他们两者都依赖于 jquery 这个库,而且相互之间也会有依赖。 image.png 当咱们在默认配置的状况下进行打包,结果是这样的👇,会把全部内容打包进一个 main bundle 内(324kbimage.png image.png 那么咱们如何用最直接的方式从这个 bundle 中分离出其余模块呢?git

1. 多入口

webpack 配置中的 entry ,能够设置为多个,也就是说咱们能够分别将 index 和 test 文件分别做为入口:github

// entry: './src/index.js', 原来的单入口
/** 如今分别将它们做为入口 */
entry:{
  index:'./src/index.js',
  test:'./src/test.js'
},
output: {
  filename: '[name].[hash:8].js',
  path: path.resolve(__dirname, './dist'),
},
复制代码

这样让咱们看一下这样打包后的结果: image.png 确实打包出了两个文件!可是为何两个文件都有 320+kb 呢?不是说好拆分获取更小的 bundle ?这是由于因为两者都引入了 jquery 而 webpack 从两次入口进行打包分析的时候会每次都将依赖的模块分别打包进去👇 image.pngweb

没错,这种配置的方式确实会带来一些隐患以及不便:

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

那么有没有方式能够既能够将共同依赖的模块进行打包分离,又不用进行繁琐的手动配置入口的方式呢?那必然是有的。

2. SplitChunksPlugin

SplitChunks 是 webpack4 开始自带的开箱即用的一个插件,他能够将知足规则的 chunk 进行分离,也能够自定义配置。在 webpack4 中用它取代了以前用来解决重复依赖的 CommonsChunkPlugin

让咱们在咱们的 webpack 配置中加上一些配置:

entry: './src/index.js', // 这里咱们改回单入口
/** 加上以下设置 */
optimization: {
  splitChunks: {
    chunks: 'all',
  },
},
复制代码

打包后的结果如图: image.png 能够看到很明显除了根据入口打包出的 main bundle 以外,还多出了一个名为 vendors-node_modules_jquery_dist_jquery_js.xxxxx.js ,显然这样咱们将公用的 jquery 模块就提取出来了。

接下来咱们来探究一下 SplitChunksPlugin 。 首先看下配置的默认值:

splitChunks: {
    // 表示选择哪些 chunks 进行分割,可选值有:async,initial 和 all
    chunks: "async",
    // 表示新分离出的 chunk 必须大于等于 minSize,20000,约 20kb。
    minSize: 20000,
    // 经过确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块,仅在剩余单个 chunk 时生效
    minRemainingSize: 0,
    // 表示一个模块至少应被 minChunks 个 chunk 所包含才能分割。默认为 1。
    minChunks: 1,
    // 表示按需加载文件时,并行请求的最大数目。
    maxAsyncRequests: 30,
    // 表示加载入口文件时,并行请求的最大数目。
    maxInitialRequests: 30,
    // 强制执行拆分的体积阈值和其余限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略
    enforceSizeThreshold: 50000,
    // cacheGroups 下能够能够配置多个组,每一个组根据 test 设置条件,符合 test 条件的模块,就分配到该组。模块能够被多个组引用,但最终会根据 priority 来决定打包到哪一个组中。默认将全部来自 node_modules 目录的模块打包至 vendors 组,将两个以上的 chunk 所共享的模块打包至 default 组。
    cacheGroups: {
        defaultVendors: {
            test: /[\\/]node_modules[\\/]/,
            // 一个模块能够属于多个缓存组。优化将优先考虑具备更高 priority(优先级)的缓存组。
            priority: -10,
            // 若是当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用
            reuseExistingChunk: true,
        },
   		  default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}
复制代码

默认状况下,SplitChunks 只会对异步调用的模块进行分割(chunks: "async"),而且默认状况下处理的 chunk 至少要有 20kb ,太小的模块不会被包含进去。

补充一下,默认值会根据 mode 的配置不一样有所变化,具体参见源码👇:

const { splitChunks } = optimization;
if (splitChunks) {
  A(splitChunks, "defaultSizeTypes", () => ["javascript", "unknown"]);
  D(splitChunks, "hidePathInfo", production);
  D(splitChunks, "chunks", "async");
  D(splitChunks, "usedExports", optimization.usedExports === true);
  D(splitChunks, "minChunks", 1);
  F(splitChunks, "minSize", () => (production ? 20000 : 10000));
  F(splitChunks, "minRemainingSize", () => (development ? 0 : undefined));
  F(splitChunks, "enforceSizeThreshold", () => (production ? 50000 : 30000));
  F(splitChunks, "maxAsyncRequests", () => (production ? 30 : Infinity));
  F(splitChunks, "maxInitialRequests", () => (production ? 30 : Infinity));
  D(splitChunks, "automaticNameDelimiter", "-");
  const { cacheGroups } = splitChunks;
  F(cacheGroups, "default", () => ({
    idHint: "",
    reuseExistingChunk: true,
    minChunks: 2,
    priority: -20
  }));
  F(cacheGroups, "defaultVendors", () => ({
    idHint: "vendors",
    reuseExistingChunk: true,
    test: NODE_MODULES_REGEXP,
    priority: -10
  }));
}
复制代码

cacheGroups 缓存组是施行分割的重中之重,他可使用来自 splitChunks.*任何选项,可是 test、priority 和 reuseExistingChunk 只能在缓存组级别上进行配置。默认配置中已经给咱们提供了 Vendors 组和一个 defalut 组,**Vendors **组中使用 test: /[\\/]node_modules[\\/]/ 匹配了 node_modules 中的全部符合规则的模块。

Tip:当 webpack 处理文件路径时,它们始终包含 Unix 系统中的 / 和 Windows 系统中的 \。这就是为何在 {cacheGroup}.test 字段中使用 [\/] 来表示路径分隔符的缘由。{cacheGroup}.test 中的 / 或 \ 会在跨平台使用时产生问题。

综上的配置,咱们即可以理解为何咱们在打包中会产生出名为 vendors-node_modules_jquery_dist_jquery_js.db47cc72.js 的文件了。若是你想要对名称进行自定义的话,也可使用 splitChunks.name 属性(每一个 cacheGroup 中均可以使用),这个属性支持使用三种形式:

  1. boolean = false 设为 false 将保持 chunk 的相同名称,所以不会没必要要地更更名称。这是生产环境下构建的建议值。
  2. function (module, chunks, cacheGroupKey) => string 返回值要求是 string 类型,而且在 chunks 数组中每个 chunk 都有 chunk.namechunk.hash 属性,举个例子 👇
name(module, chunks, cacheGroupKey) {
  const moduleFileName = module
  .identifier()
  .split('/')
  .reduceRight((item) => item);
  const allChunksNames = chunks.map((item) => item.name).join('~');
  return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
},
复制代码
  1. string 指定字符串或始终返回相同字符串的函数会将全部常见模块和 vendor 合并为一个 chunk。这可能会致使更大的初始下载量并减慢页面加载速度

另外注意一下 splitChunks.maxAsyncRequestssplitChunks.maxInitialRequests 分别指的是按需加载时最大的并行请求数页面初始渲染时候须要的最大并行请求数

在咱们的项目较大时,若是须要对某个依赖单独拆包的话,能够进行这样的配置:

cacheGroups: {
  react: {
    name: 'react',
      test: /[\\/]node_modules[\\/](react)/,
      chunks: 'all',
      priority: -5,
  },
 },
复制代码

这样打包后就能够拆分指定的包: image.png

更多配置详见官网配置文档

3. 动态 import

使用 import()语法 来实现动态导入也是咱们很是推荐的一种代码分割的方式,咱们先来简单修改一下咱们的 index.js ,再来看一下使用后打包的效果:

// import { mul } from './test'
import $ from 'jquery'

import('./test').then(({ mul }) => {
    console.log(mul(2,3))
})

console.log($)
// console.log(mul(2, 3))
复制代码

能够看到,经过 import() 语法导入的模块在打包时会自动单独进行打包 image.png

值得注意的是,这种语法还有一种很方便的“动态引用”的方式,他能够加入一些适当的表达式,举个例子,假设咱们须要加载适当的主题:

const themeType = getUserTheme();
import(`./themes/${themeType}`).then((module) => {
  // do sth aboout theme
});
复制代码

这样咱们就能够“动态”加载咱们须要的异步模块,实现的原理主要在于两点:

  1. 至少须要包含模块相关的路径信息,打包能够限定于一个特定的目录或文件集。
  2. 根据路径信息 webpack 在打包时会把 ./themes  中的全部文件打包进新的 chunk 中,以便须要时使用到。

4. 魔术注释

在上述的 import() 语法中,咱们会发现打包自动生成的文件名并非咱们想要的,咱们如何才能本身控制打包的名称呢?这里就要引入咱们的魔术注释(Magic Comments):

import(/* webpackChunkName: "my-chunk-name" */'./test')
复制代码

经过这样打包出来的文件: image.png

魔术注释不只仅能够帮咱们修改 chunk 名这么简单,他还能够实现譬如预加载等功能,这里举个例子:

咱们经过但愿在点击按钮时才加载咱们须要的模块功能,代码能够这样:

// index.js
document.querySelector('#btn').onclick = function () {
  import('./test').then(({ mul }) => {
    console.log(mul(2, 3));
  });
};
复制代码
//test.js
function mul(a, b) {
  return a * b;
}
console.log('test 被加载了');
export { mul };
复制代码

03-03.gif 能够看到,在咱们点击按钮的同时确实加载了 test.js 的文件资源。可是若是这个模块是一个很大的模块,在点击时进行加载可能会形成长时间 loading 等用户体验不是很好的效果,这个时候咱们可使用咱们的 /* webpackPrefetch: true */ 方式进行预获取,来看下效果:

// index,js

document.querySelector('#btn').onclick = function () {
  import(/* webpackPrefetch: true */'./test').then(({ mul }) => {
    console.log(mul(2, 3));
  });
};
复制代码

03-04.gif 能够看到整个过程当中,在画面初始加载的时候,test.js 的资源就已经被预先加载了,而在咱们点击按钮时,会从 (prefetch cache) 中读取内容。这就是模块预获取的过程。另外咱们还有 /* webpackPreload: true */ 的方式进行预加载。

可是 prefetch 和 preload 听起来感受差很少,实际上他们的加载时机等是彻底不一样的:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具备中等优先级,并当即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中当即请求,用于当下时刻。prefetch chunk 会用于将来的某个时刻。

3、结尾

在最初有工程化打包思想时,咱们会考虑将多文件打包到一个文件内减小屡次的资源请求,随着项目的愈来愈复杂,作项目优化时,咱们发现项目加载越久用户体验就越很差,因而又能够经过代码分割的方式去减小页面初加载时的请求过大的资源体积。

本文中仅简单介绍了经常使用的 webpack 代码分割方式,可是在实际的项目中进行性能优化时,每每会有更加严苛的要求,但愿能够经过本文的介绍让你们快速了解上手代码分割的技巧与优点。

参考

如何使用 splitChunks 精细控制代码分割

Code Splitting - Webpack

相关文章
相关标签/搜索