前言

开发多页应用的时候,如果不对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目录结构:

  1. |--public/
  2. | |--a.html
  3. | |--index.html
  4. |--src/
  5. | |--a.js
  6. | |--b.js
  7. | |--c.js
  8. | |--index.js
  9. |--package.json
  10. |--webpack.config.js

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

index.js代码如下:

  1. //index.js
  2. import a from './a.js';
  3. import b from './b.js';
  4. function fn() {
  5. console.log('index-------');
  6. }
  7. fn();

a.js代码如下:

  1. //a.js
  2. require('./c.js');
  3. const $ = require('jquery')
  4. function fn() {
  5. console.log('a-------');
  6. }
  7. module.exports = fn();

b.js代码如下:

  1. //b.js
  2. require('./c.js');
  3. const $ = require('jquery')
  4. function fn() {
  5. console.log('b-------');
  6. }
  7. module.exports = fn();

c.js代码如下:

  1. //c.js
  2. function fn() {
  3. console.log('c-------');
  4. }
  5. module.exports = fn();

1. 基本配置

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

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin');
  3. module.exports = {
  4. entry: {
  5. index: './src/index.js',
  6. a: './src/a.js'
  7. },
  8. output: {
  9. path: path.resolve(__dirname, 'dist'),
  10. filename: '[name].js'
  11. },
  12. plugins: [
  13. new HtmlWebpackPlugin({
  14. template: './public/index.html',
  15. filename: 'index.html'
  16. }),
  17. new HtmlWebpackPlugin({
  18. template: './public/a.html',
  19. filename: 'a.html'
  20. })
  21. ]
  22. }

在开发模式下运行webpack:

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

进入dist目录检查js文件:

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

看,同样的代码cjquery被打包了2遍。

2. 初步添加splitChunks优化配置

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

在webpack配置文件添加splitChunks:

  1. //webpack.config.js
  2. optimization: {
  3. splitChunks: {
  4. cacheGroups: {
  5. default: {
  6. name: 'common',
  7. chunks: 'initial'
  8. }
  9. }
  10. }
  11. }

- cacheGroups

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

- name

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

- chunks

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

再次打包:

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

进入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,只配置了namechunk两个属性,所以minChunks的默认值 1 生效。也难怪所有的模块都被抽离到common.js中了。

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

  1. //webpack.config.js
  2. optimization: {
  3. splitChunks: {
  4. cacheGroups: {
  5. default: {
  6. name: 'common',
  7. chunks: 'initial',
  8. minChunks: 2 //模块被引用2次以上的才抽离
  9. }
  10. }
  11. }
  12. }

然后运行webpack

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

进入dist目录查看:

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

现在我们把共同引用的模块a, c, jquery,从aindex这两个入口模块里抽取到common.js里了。有点符合我们的预期了。

3. 配置多个拆分规则

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

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

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

  1. //webpack.config.js
  2. optimization: {
  3. splitChunks: {
  4. minSize: 30, //提取出的chunk的最小大小
  5. cacheGroups: {
  6. default: {
  7. name: 'common',
  8. chunks: 'initial',
  9. minChunks: 2, //模块被引用2次以上的才抽离
  10. priority: -20
  11. },
  12. vendors: { //拆分第三方库(通过npm|yarn安装的库)
  13. test: /[\\/]node_modules[\\/]/,
  14. name: 'vendor',
  15. chunks: 'initial',
  16. priority: -10
  17. }
  18. }
  19. }
  20. }

我给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:

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

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

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

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

3.2 拆分指定文件

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

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

可以这么配置:

  1. //webpack.config.js
  2. optimization: {
  3. splitChunks: {
  4. minSize: 30, //提取出的chunk的最小大小
  5. cacheGroups: {
  6. default: {
  7. name: 'common',
  8. chunks: 'initial',
  9. minChunks: 2, //模块被引用2次以上的才抽离
  10. priority: -20
  11. },
  12. vendors: { //拆分第三方库(通过npm|yarn安装的库)
  13. test: /[\\/]node_modules[\\/]/,
  14. name: 'vendor',
  15. chunks: 'initial',
  16. priority: -10
  17. },
  18. locallib: { //拆分指定文件
  19. test: /(src\/locallib\.js)$/,
  20. name: 'locallib',
  21. chunks: 'initial',
  22. priority: -9
  23. }
  24. }
  25. }
  26. }

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

运行webpack打包后:

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

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

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

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

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

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

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

小结

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

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

欢迎交流~

本文的完整webpack配置和demo源码可以在这里获取:

https://github.com/yc111/webpack-optimize-demo

--

欢迎转载,转载请注明出处:

https://champyin.com/2019/11/15/webpack优化之玩转代码分割和公共代码提取/

本文同步发表于:

webpack优化之玩转代码分割和公共代码提取 | 掘金

webpack优化之玩转代码分割和公共代码提取的更多相关文章

  1. CSS重置代码和常用公共代码

    发的发生的发生法士大夫撒打发士大夫

  2. webpack的代码分割/离

    两种方法: 1.webpack的methods----require.ensure 2.ES 2015的Loader spec //require.ensure语法 require.ensure [] ...

  3. React配合Webpack实现代码分割与异步加载

    这是Webpack+React系列配置过程记录的第四篇.其他内容请参考: 第一篇:使用webpack.babel.react.antdesign配置单页面应用开发环境 第二篇:使用react-rout ...

  4. 基于webpack的前端工程化开发解决方案探索(二):代码分割与图片加载

    今天我们继续来进行webpack工程化开发的探索! 首先来验证上一篇文章   基于webpack的前端工程化开发解决方案探索(一):动态生成HTML  中的遗留问题:webpack将如何处理按需加载的 ...

  5. Webpack之optimization.splitChunks代码分割插件的配置

    SplitChunkPlugin插件配置参数详解 对引入的库代码(例如:lodash.jQuery等)进行代码的分割进行优化 若配置时只写chunks:"all",其余则为默认配置 ...

  6. 从Mpx资源构建优化看splitChunks代码分割

    背景 MPX是滴滴出品的一款增强型小程序跨端框架,其核心是对原生小程序功能的增强.具体的使用不是本文讨论的范畴,想了解更多可以去官网了解更多. 回到正题,使用MPX开发小程序有一段时间了,该框架对不同 ...

  7. webpack练手项目之easySlide(二):代码分割(转)

    在上一篇 webpack练手项目之easySlide(一):初探webpack  中我们一起为大家介绍了webpack的基本用法,使用webpack对前端代码进行模块化打包. 但是乍一看webpack ...

  8. webpack 代码分割一点事

    webpack 俨然已经成为前端最主流的构建工具,其功能多种多样,我们今天就来分析下关于代码分割这部分的一点事,并在最后讲述如何实现在webpack编译出的代码里手动添加一个异步chunk. 什么是c ...

  9. webpack:代码分割与按需加载

    代码分割就是我们根据实际业务需求将代码进行分割,然后在合适的时候在将其加载进入文档中. 代码中总有些东西我们希望拆分开来,比如: 使用概率较低的模块,希望后期使用的时候异步加载 框架代码,希望能利用浏 ...

随机推荐

  1. [BZOJ29957] 楼房重建 - 线段树

    2957: 楼房重建 Time Limit: 10 Sec  Memory Limit: 256 MBSubmit: 3294  Solved: 1554[Submit][Status][Discus ...

  2. 【Java基础】谈谈集合.CopyOnWriteArrayList

    目录 实现原理 遍历时不用加锁的原因 CopyOnWriteArrayLis的缺点 使用场景 总结 参考 本篇博客介绍CopyOnWriteArrayList类,读完本博客你将会了解: 什么是COW机 ...

  3. Ubuntu8.04::扩容(LVM)磁盘

    .扩容 sudo lvextend -l +%FREE /dev/mapper/ubuntu--vg-ubuntu--lv .重新计算磁盘大小 sudo resize2fs /dev/mapper/u ...

  4. webpack4.0入门总结

    1. 安装webpack: // 初始化.安装webpack以及webpack-clinpm init npm install --save-dev webpack webpack-cli 2.创建配 ...

  5. 实现基于netty的web框架,了解一下

    上一篇写了,基于netty实现的rpc的微框架,其中详细介绍netty的原理及组件,这篇就不过多介绍 这篇实现基于netty的web框架,你说netty强不强,文中有不对的地方,欢迎大牛指正 先普及几 ...

  6. The usage of Markdown---引用

    目录 1. 序言 2. 引用与嵌套引用 3. 列表中的引用 更新时间:209.09.14 1. 序言   在本篇,我们来仔细谈一下Markdown的引用. 2. 引用与嵌套引用   在Markdown ...

  7. (day27)subprocess模块+粘包问题+struct模块+ UDP协议+socketserver

    目录 昨日回顾 软件开发架构 C/S架构 B/S架构 网络编程 互联网协议 socket套接字 今日内容 一.subprocess模块 二.粘包问题 三.struct模块 四.UDP 五.QQ聊天室 ...

  8. 变量 + 数据类型(数字 + 字符串)(day03整理)

    目录 一.上节课回顾 四 编程语言分类 (一) 机器语言 (二)汇编语言 (三) 高级语言 (四) 网络瓶颈效应 五.执行python程序两种方式 (一) 交互式(jupytre) (二) 命令行式( ...

  9. django-模型之(ORM)对象关系映射(一)

    所谓对象关系映射,就是将数据库的一些名字与python中的一些名字相对应,表名-->类名,字段-->属性,操作(增删改查)-->方法.这样,我们就可以通过对Python代码的编辑来对 ...

  10. Mui 长按保存图片

    必须先要 引入 mui.js,然后参考具体代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8 ...