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 相关文章推荐
javascript getElementsByName()的用法说明
Jul 31 Javascript
Jquery replace 字符替换实现代码
Dec 02 Javascript
jquery插件制作 提示框插件实现代码
Aug 17 Javascript
Jquery颜色选择器ColorPicker实现代码
Nov 14 Javascript
jquery实现excel导出的方法
Apr 04 Javascript
使用简洁的jQuery方法实现隔行换色功能
Jan 02 Javascript
JS+HTML5实现的前端购物车功能插件实例【附demo源码下载】
Oct 17 Javascript
详解jQuery选择器
Dec 21 Javascript
Vue.js 2.0窥探之Virtual DOM到底是什么?
Feb 10 Javascript
详解vue 模版组件的三种用法
Jul 21 Javascript
微信小程序之几种常见的弹框提示信息实现详解
Jul 11 Javascript
vue 封装 Adminlte3组件的实现
Mar 18 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
解析php类的注册与自动加载
2013/07/05 PHP
ThinkPHP模板判断输出Defined标签用法详解
2014/06/30 PHP
yii分页组件用法实例分析
2015/12/28 PHP
javascript 面向对象思想 附源码
2009/07/07 Javascript
jquery 操作单选框,复选框,下拉列表实现代码
2009/10/27 Javascript
js substr、substring和slice使用说明小记
2011/09/15 Javascript
基于jquery实现状态限定编辑的代码
2012/02/11 Javascript
jQuery+ajax实现鼠标单击修改内容的思路
2014/06/29 Javascript
setinterval()与clearInterval()JS函数的调用方法
2015/01/21 Javascript
判断js的Array和Object的实现方法
2016/08/29 Javascript
javascript 动态脚本添加的简单方法
2016/10/11 Javascript
JS常用加密编码与算法实例总结
2016/12/22 Javascript
JS使用插件cryptojs进行加密解密数据实例
2017/05/11 Javascript
javascript 跨域问题以及解决办法
2017/07/17 Javascript
在React 组件中使用Echarts的示例代码
2017/11/08 Javascript
Vue项目中如何引入icon图标
2018/03/28 Javascript
Vue EventBus自定义组件事件传递
2018/06/25 Javascript
NodeJs 模仿SIP话机注册的方法
2019/06/21 NodeJs
JQuery样式与属性设置方法分析
2019/12/07 jQuery
Python中eval带来的潜在风险代码分析
2017/12/11 Python
Python pyinotify日志监控系统处理日志的方法
2018/03/08 Python
python整小时 整天时间戳获取算法示例
2019/02/20 Python
浅谈python的深浅拷贝以及fromkeys的用法
2019/03/08 Python
详解python中的线程与线程池
2019/05/10 Python
python pyecharts 实现一个文件绘制多张图
2020/05/13 Python
Python无损压缩图片的示例代码
2020/08/06 Python
python实现定时发送邮件
2020/12/23 Python
床上用品全球在线购物:BeddingInn
2016/12/18 全球购物
电子商务专业毕业生工作推荐信
2013/11/17 职场文书
读书活动总结
2014/04/28 职场文书
房屋出租委托书格式
2014/09/23 职场文书
2015年语文教师工作总结
2015/05/25 职场文书
2015年大学迎新工作总结
2015/07/16 职场文书
2015年汽车销售员工作总结
2015/07/24 职场文书
《妈妈别哭,有我在》读后感3篇
2020/01/13 职场文书
JavaScript前端面试组合函数
2022/06/21 Javascript