webpack优化之代码分割与公共代码提取详解


Posted in Javascript onNovember 22, 2019

前言

开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码)。当项目业务越来越复杂,打包出来的代码会非常冗余,文件体积会非常庞大。大体积文件会增加编译时间,影响开发效率;如果直接上线,还会拉长请求和加载时长,影响网站体验。作为一个追求极致体验的攻城狮,是不能忍的。所以在多页应用中优化打包尤为必要。那么如何优化webpack打包呢?

一、概念

在一切开始前,有必要先理清一下这三个概念:

  • module: 模块,在webpack眼里,任何可以被导入导出的文件都是一个模块。
  • chunk: chunk是webpack拆分出来的:
    • 每个入口文件都是一个chunk
    • 通过 import、require 引入的代码也是
    • 通过 splitChunks 拆分出来的代码也是
  • bundle: webpack打包出来的文件,也可以理解为就是对chunk编译压缩打包等处理后的产出。

二、问题分析

首先,简单分析下,我们刚才提到的打包问题:

  • 核心问题就是:多页应用打包后代码冗余,文件体积大。
  • 究其原因就是:相同模块在不同入口之间没有得到复用,bundle之间比较独立。

弄明白了问题的原因,那么大致的解决思路也就出来了:

  • 我们在打包的时候,应该把不同入口之间,共同引用的模块,抽离出来,放到一个公共模块中。这样不管这个模块被多少个入口引用,都只会在最终打包结果中出现一次。——解决代码冗余。
  • 另外,当我们把这些共同引用的模块都堆在一个模块中,这个文件可能异常巨大,也是不利于网络请求和页面加载的。所以我们需要把这个公共模块再按照一定规则进一步拆分成几个模块文件。——减小文件体积。
  • 至于如何拆分,方式因人而异,因项目而异。我个人的拆分原则是:
    • 业务代码和第三方库分离打包,实现代码分割;
    • 业务代码中的公共业务模块提取打包到一个模块;
    • 第三方库最好也不要全部打包到一个文件中,因为第三方库加起来通常会很大,我会把一些特别大的库分别独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。

optimization.splitChunks

webpack提供了一个非常好的内置插件帮我们实现这一需求:CommonsChunkPlugin。不过在 webpack4 中CommonsChunkPlugin被删除,取而代之的是optimization.splitChunks, 所幸的是optimization.splitChunks更强大!

三、 实现

通过一个多页应用的小demo,我们一步一步来实现上述思路的配置。

demo目录结构:

|--public/
|   |--a.html
|   |--index.html
|--src/
|   |--a.js
|   |--b.js
|   |--c.js
|   |--index.js
|--package.json
|--webpack.config.js

代码逻辑很简单,index模块中引用了 a 和 b 2个模块,a 模块中引用了 c 模块和 jquery库,b 模块中也引用了 c 模块和 jquery 库,c 是一个独立的模块没有其他依赖。

index.js代码如下:

//index.js
import a from './a.js';
import b from './b.js';
function fn() {
 console.log('index-------');
}
fn();

a.js代码如下:

//a.js
require('./c.js');
const $ = require('jquery')
function fn() {
 console.log('a-------');
}
module.exports = fn();

b.js代码如下:

//b.js
require('./c.js');
const $ = require('jquery')
function fn() {
 console.log('b-------');
}
module.exports = fn();

c.js代码如下:

//c.js
function fn() {
 console.log('c-------');
}
module.exports = fn();

1. 基本配置

webpack先不做优化,只做基本配置,看看效果。项目配置了2个入口,搭配html-webpack-plugin实现多页打包:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
 entry: {
 index: './src/index.js',
 a: './src/a.js'
 },
 output: {
 path: path.resolve(__dirname, 'dist'),
 filename: '[name].js'
 },
 plugins: [
 new HtmlWebpackPlugin({
  template: './public/index.html',
  filename: 'index.html'
 }),
 new HtmlWebpackPlugin({
  template: './public/a.html',
  filename: 'a.html'
 })
 ]
}

在开发模式下运行webpack:

webpack优化之代码分割与公共代码提取详解

可以看到,打包出两个html和两个体积很大的(300多K)的文件a.js,index.js。

进入dist目录检查js文件:

  • a.js里包含c模块代码和jquery代码
  • index.js里包含a模块、b模块、c模块和jquery代码

看,同样的代码c和jquery被打包了2遍。

2. 初步添加splitChunks优化配置

首先解决相同代码打包2次的问题,我们需要让webpack把c和jquery提取出来打包为公共模块。

在webpack配置文件添加splitChunks:

//webpack.config.js

optimization: {
 splitChunks: {
 cacheGroups: {
  default: {
  name: 'common',
  chunks: 'initial'
  }
 }
 }
}

- cacheGroups

  • cacheGroups是splitChunks配置的核心,对代码的拆分规则全在cacheGroups缓存组里配置。
  • 缓存组的每一个属性都是一个配置规则,我这里给他的default属性进行了配置,属性名可以不叫default可以自己定。
  • 属性的值是一个对象,里面放的我们对一个代码拆分规则的描述。

- name

  • name:提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是index~a.js这样的。

- chunks

  • chunks:指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:all, async, initial,all 代表所有模块,async代表只管异步加载的, initial代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选。

再次打包:

webpack优化之代码分割与公共代码提取详解

可以看到a.js,index.js从300多K减少到6点几K。同时增加了一个common.js文件,并且两个打包入口都自动添加了common.js这个公共模块:

webpack优化之代码分割与公共代码提取详解

进入dist目录,依次查看这3个js文件:

  • a.js里不包含任何模块的代码了,只有webpack生成的默认代码。
  • index.js里同样不包含任何模块的代码了,只有webpack生成的默认代码。
  • common.js里有a,b,c,index,jquery代码。

发现,提是提取了,但是似乎跟我们预料的不太一样,所有的模块都跑到common.js里去了。

这是因为我们没有告诉webpack(splitChunks)什么样的代码为公共代码,splitChunks默认任何模块都会被提取。

- minChunks

splitChunks是自带默认配置的,而缓存组默认会继承这些配置,其中有个minChunks属性:

  • 它控制的是每个模块什么时候被抽离出去:当模块被不同entry引用的次数大于等于这个配置值时,才会被抽离出去。
  • 它的默认值是1。也就是任何模块都会被抽离出去(入口模块其实也会被webpack引入一次)。

我们上面没有配置minChunks,只配置了name和chunk两个属性,所以minChunks的默认值 1 生效。也难怪所有的模块都被抽离到common.js中了。

优化一下,在缓存组里配置minChunks覆盖默认值:

//webpack.config.js

optimization: {
 splitChunks: {
  cacheGroups: {
   default: {
    name: 'common',
    chunks: 'initial',
    minChunks: 2 //模块被引用2次以上的才抽离
   }
  }
 }
}

然后运行webpack

webpack优化之代码分割与公共代码提取详解

可以看到有2个文件的大小发生了变化:common.js由314K减小到311K,index.js由6.22K增大到7.56K。

进入dist目录查看:

a.js里依然不包含任何模块的代码(正常,因为a作为模块被index引入了一次,又作为入口被webpack引入了一次,所以a是有2次引用的)。

  • index.js里出现了b和index模块的代码了。
  • common.js里只剩a,c,和jquery模块的代码。
  • 现在我们把共同引用的模块a, c, jquery,从a和index这两个入口模块里抽取到common.js里了。有点符合我们的预期了。

3. 配置多个拆分规则

3.1 实现代码分离,拆分第三方库

接下来,我希望公共模块common.js中,业务代码和第三方模块jquery能够剥离开来。

我们需要再添加一个拆分规则。

//webpack.config.js

optimization: {
 splitChunks: {
  minSize: 30, //提取出的chunk的最小大小
  cacheGroups: {
   default: {
    name: 'common',
    chunks: 'initial',
    minChunks: 2, //模块被引用2次以上的才抽离
    priority: -20
   },
   vendors: { //拆分第三方库(通过npm|yarn安装的库)
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'initial',
    priority: -10
   }
  }
 }
}

我给cacheGroups添加了一个vendors属性(属性名可以自己取,只要不跟缓存组下其他定义过的属性同名就行,否则后面的拆分规则会把前面的配置覆盖掉)。

- minSize

minSize设置的是生成文件的最小大小,单位是字节。如果一个模块符合之前所说的拆分规则,但是如果提取出来最后生成文件大小比minSize要小,那它仍然不会被提取出来。这个属性可以在每个缓存组属性中设置,也可以在splitChunks属性中设置,这样在每个缓存组都会继承这个配置。这里由于我的demo中文件非常小,为了演示效果,我把minSize设置为30字节,好让公共模块可以被提取出来,正常项目中不用设这么小。

- priority

priority属性的值为数字,可以为负数。作用是当缓存组中设置有多个拆分规则,而某个模块同时符合好几个规则的时候,则需要通过优先级属性priority来决定使用哪个拆分规则。优先级高者执行。我这里给业务代码组设置的优先级为-20,给第三方库组设置的优先级为-10,这样当一个第三方库被引用超过2次的时候,就不会打包到业务模块里了。

- test

test属性用于进一步控制缓存组选择的模块,与chunks属性的作用有一点像,但是维度不一样。test的值可以是一个正则表达式,也可以是一个函数。它可以匹配模块的绝对资源路径或chunk名称,匹配chunk名称时,将选择chunk中的所有模块。我这里用了一个正则/[\\/]node_modules[\\/]/来匹配第三方模块的绝对路径,因为通过npm或者yarn安装的模块,都会存放在node_modules目录下。

运行一下webpack:

webpack优化之代码分割与公共代码提取详解

可以看到新产生了一个叫vendor.js的文件(name属性的值),同时common.js文件体积由原来的311k减少到了861bytes!

进入dist目录,检查js文件:

  • a.js里不包含任何模块代码。
  • common.js只包含a和c模块的代码。
  • index.js只包含b和index模块的代码。
  • vendor.js只包含jquery模块的代码。

现在,我们在上一步的基础上,成功从common.js里把第三方库jquery抽离出来放到了vendor.js里。

3.2 拆分指定文件

如果我们还想把项目中的某一些文件单独拎出来打包(比如工程本地开发的组件库),可以继续添加拆分规则。比如我的src下有个locallib.js文件要单独打包,假设a.js中引入了它。

//a.js
require('./c.js');
require('./locallib.js'); //引入自己本地的库
const $ = require('jquery')
function fn() {
 console.log('a-------');
}
module.exports = fn();

可以这么配置:

//webpack.config.js

optimization: {
 splitChunks: {
  minSize: 30, //提取出的chunk的最小大小
  cacheGroups: {
   default: {
    name: 'common',
    chunks: 'initial',
    minChunks: 2, //模块被引用2次以上的才抽离
    priority: -20
   },
   vendors: { //拆分第三方库(通过npm|yarn安装的库)
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'initial',
    priority: -10
   },
   locallib: { //拆分指定文件
    test: /(src\/locallib\.js)$/,
    name: 'locallib',
    chunks: 'initial',
    priority: -9
   }
  }
 }
}

我在缓存组下又新增了一个拆分规则,通过test正则指定我就要单独打包src/locallib.js文件,并且把优先级设置为-9,这样当它被多次引用时,不会进入其他拆分规则组,因为另外两个规则的优先级都比它要低。

运行webpack打包后:

webpack优化之代码分割与公共代码提取详解

可以看到新产生了一个locallib.js文件。进入dist目录查看:

  • a.js里不包含任何模块代码。
  • common.js只包含a和c模块的代码。
  • index.js只包含b和index模块的代码。
  • vendor.js只包含jquery模块的代码。
  • locallib.js里只包含locallib模块的代码。

现在我们又在上一步的基础上独立打包了一个指定的模块locallib.js。

至此,我们就成功实现了抽离公共模块、业务代码和第三方代码剥离、独立打包指定模块。

对比一下,优化前,打包出来js一共有633KB:

webpack优化之代码分割与公共代码提取详解

优化后,打包出来js一共不到330KB:

webpack优化之代码分割与公共代码提取详解

优化打包后的文件分类清晰,体积比优化前缩小了几乎50%,有点小完美是不是!击掌!这还只是我举的一个简单例子,在实际多页应用中,优化力度说不定还不止这么多。

小结

webpack很强大,以上只是冰山一角,但是只要掌握了上述optimization.splitChunks的核心配置,我们就可以几乎随心所欲地按照自己的想法来拆分优化代码控制打包文件了,是不是很酷?玩转代码拆分,你也可以!

如果觉得这些依然不能满足你的需求,还想更精(bian)细(tai)地定制打包规则,可以到webpack官网查看optimization.splitChunks的更多配置。

欢迎交流~

本文的完整webpack配置和demo源码可以在这里获取:https://github.com/yc111/webpack-optimize-demo

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
基于jQuery实现的美观星级评论打分组件代码
Oct 30 Javascript
基于jquery实现省市联动效果
Nov 23 Javascript
实现JavaScript的组成----BOM和DOM详解
May 18 Javascript
jQuery继承extend用法详解
Oct 10 Javascript
jQuery ajax实现省市县三级联动
Mar 07 Javascript
JavaScript实现body内任意节点的自定义属性功能示例
Sep 18 Javascript
Angular CLI在Angular项目中如何使用scss详解
Apr 10 Javascript
使用Angular Cli如何创建Angular私有库详解
Jan 30 Javascript
vue指令做滚动加载和监听等
May 26 Javascript
vue cli4下环境变量和模式示例详解
Apr 09 Javascript
React实现轮播效果
Aug 25 Javascript
小程序实现密码输入框
Nov 16 Javascript
小程序实现录音上传功能
Nov 22 #Javascript
vue使用recorder.js实现录音功能
Nov 22 #Javascript
微信小程序开发摇一摇功能
Nov 22 #Javascript
js实现录音上传功能
Nov 22 #Javascript
解决vue自定义全局消息框组件问题
Nov 22 #Javascript
JavaScript实现省市联动效果
Nov 22 #Javascript
Vue混入mixins滚动触底的方法
Nov 22 #Javascript
You might like
asp和php下textarea提交大量数据发生丢失的解决方法
2008/01/20 PHP
Thinkphp整合阿里云OSS图片上传实例代码
2019/04/28 PHP
javascript Zifa FormValid 0.1表单验证 代码打包下载
2007/06/08 Javascript
不错的JS中变量相关的细节分析
2007/08/13 Javascript
jquery Tab效果和动态加载的简单实例
2013/12/11 Javascript
js实现通用的微信分享组件示例
2014/03/10 Javascript
实用框架(iframe)操作代码
2014/10/23 Javascript
jQuery EasyUI学习教程之datagrid点击列表头排序
2016/07/09 Javascript
Mvc提交表单的四种方法全程详解
2016/08/10 Javascript
微信小程序模板之分页滑动栏
2017/02/10 Javascript
微信小程序基于本地缓存实现点赞功能的方法
2017/12/18 Javascript
JS处理一些简单计算题
2018/02/24 Javascript
vue2中使用less简易教程
2018/03/27 Javascript
解决VUE 在IE下出现ReferenceError: Promise未定义的问题
2020/11/07 Javascript
JavaScript仿京东轮播图效果
2021/02/25 Javascript
使用python搭建Django应用程序步骤及版本冲突问题解决
2013/11/19 Python
用Python实现一个简单的能够发送带附件的邮件程序的教程
2015/04/08 Python
Python跨文件全局变量的实现方法示例
2017/12/10 Python
Python 编码规范(Google Python Style Guide)
2018/05/05 Python
Python3中bytes类型转换为str类型
2018/09/27 Python
使用pandas实现csv/excel sheet互相转换的方法
2018/12/10 Python
用Pelican搭建一个极简静态博客系统过程解析
2019/08/22 Python
Python爬虫实现vip电影下载的示例代码
2020/04/20 Python
Python-openpyxl表格读取写入的案例详解
2020/11/02 Python
25道Java面试题集合
2013/05/21 面试题
大学生四年生活自我鉴定
2013/11/21 职场文书
公司年会晚宴演讲稿
2014/01/06 职场文书
交通安全演讲稿
2014/01/07 职场文书
员工合理化建议书
2014/05/19 职场文书
收款授权委托书
2014/10/02 职场文书
个人股份转让协议书范本
2014/10/26 职场文书
诚实守信主题班会
2015/08/13 职场文书
Python利用folium实现地图可视化
2021/05/23 Python
详解Java线程池是如何重复利用空闲线程的
2021/06/26 Java/Android
Java elasticsearch安装以及部署教程
2021/06/28 Java/Android
Python内置类型集合set和frozenset的使用详解
2022/04/26 Python