webpack分离第三方库及公用文件
我们开发的项目中,具体代码文件主要包含三种类型,有 第三方库、工具函数、业务代码。在这篇文章,你会看到使用 webpack 处理这些文件的一些方法。
为了便于说明,首先,安装需要用到的第三方库,这里,以 jQuery
为例:
npm i -S jquery |
一、直接引入
我们最常用的引入方式,就是用 AMD 或者 ES6 模块导入的形式在具体的业务模块中直接引入:
// header.js |
如果webpack配置文件没有做其他相关设置,那么在这种情况下,jQuery 源码会和业务代码最终会打包到一个文件中。
倘若多个业务模块都引用了 jQuery,则在打包时,webpack很机智,不会对 jQuery 源码进行多次打包。即最终打包的文件,只包含一份 jQuery 源码。
二、webpack.ProvidePlugin
如果想要使用 jQuery,但又不想在业务代码中反复使用 import $ from 'jquery'
来引入 jQuery,你可以使用 ProvidePlugin
。它属于webpack的内置API,我们可以在 webpack 配置文件进行如下设置:
// webpack.config.js |
这样,我们甚至不用通过 require('jquery')
或者 import $ from 'jquery'
这种形式引入 jQuery,就能在开发模块中使用 jQuery 的 API 了,如:
// header.js |
在使用 webpack-dev-server
插件启动本地服务开发的环境下,应用上面的设置,会把 jQuery 源码和其他模块一起打包成一个文件,假设打包后的文件为 main.js
,当打开 main.js
,你会发现,jQuery 的源码处于 main.js
的顶部,有关 jQuery 操作的代码,则是在后面的一个独立模块。
当打包发布生产环境时,jQuery
源码也会和其他模块一起打包成一个文件,也是位于文件的顶部,不同的是,有关 jQuery 操作的代码,是和 jQuery 源码处于同一模块。
由于在开发环境和生产环境,jQuery 都是以模块的形式呈现,因此,你在浏览器的控制台,无法使用 jQuery 的API。即只能是在开发模块中,使用在 ProvidePlugin
中定义好的 $
或者 jQuery
。
如果要使用 jQuery
的第三方插件,也是非常方便的,比如绘制圆环的插件,安装npm包后,直接引入:
// header.js |
三、expose-loader
通过前面的方式,我们知道,打包后 jQuery 源码以模块的形式呈现。因此,对于一些依赖它的文件,或者希望在Chrome开发者工具中,调试 jQuery API等情况并不友好。
针对这个问题,我们放弃使用 webpack.ProvidePlugin
方案(移除配置中的相关代码)。而是采用另外一种方案 - expose-loader
。它的功能主要是将第三方库(本文指的是 jQuery)暴露给全局变量环境中,这样一来,无论是调试页面,还是依赖它的其他js文件,都能很方便的使用到它的API。
首先,我们安装它:
npm i expose-loader -D |
然后,我们对 webpack 配置文件进行如下更新(前提是你已安装 jQuery):
// webpack.config.js |
最后,需要在业务代码中引入第三方库,才能将它暴露到全局变量中:
// header.js |
倘若你希望对这个第三方库设置多个全局变量,则可以继续新增:
// webpack.config.js |
上面的代码,除了原来的 $
,现在还增加了 jQuery
作为 jQuery 的全局变量。如此一来,在 Chrome 开发者工具控制台中,无论是使用变量 $
还是使用 jQuery
,它们都能正常访问 jQuery 的 API。
如果项目中希望对其他第三方库(比如 lodash、react 等)暴露全局变量,那你只需要在安装好这些第三方库后,再在打包配置文件中,做相关新增类似配置即可。
可以看到,虽然在采用了 webpack.ProvidePlugin
方案后,模块无需在业务代码中引入第三方库,就能使用相关 API,但它无法全局,对依赖包不友好。而 expose-loader
则需要在业务代码中引入第三方库 ,它解决 webpack.ProvidePlugin
存在的问题。
四、externals
说到这里,其实对于上面的两种方案,我们都忽略一个重要的问题。
按理来说,对于第三方库而言,我们几乎不会修改它的源码。因此,并非每次修改完业务代码,都需要每次都将第三方库也重新打包一遍。
而上述的 webpack.ProvidePlugin
和 expose-loader
方案,都存在第三方库重新打包或者说需要打包的问题,这就直接导致了打包效率慢。同时,这也导致了第三方库和业务代码打包在了一起,所以,打包后的文件通常都比较大。
而对于这两个问题,我们可以通过配置 externals
选项来解决。
首先,在webpack配置文件进行相关设置:
// webpack.config.js |
然后,针对第三方库,我们一般用相对路径或者类似 CDN 这种绝对路径的形式,以 <script>
标签在页面里直接引入。这里我们拿 CDN 上的 jQuery 做演示:
<!-- index.html --> |
最后,无需在业务代码中引入第三方库,就能直接使用 jQuery 的 API:
// header.js |
再次打包,你会发现打包效率变高了,因为第三方库不参与打包。你会发现打包后的文件变小了,因为第三方库与业务代码分离了。你会发现 jQuery 被暴露为全局变量,因为第三方库引用的是 CDN,而非webpack模块。另外,即使你没在业务代码中引入第三方库,你也在这个业务模块使用 jQuery 的 API。
但如果你不在 index.html
页面中 jQuery 文件(即移除上面的 script
连接),而是采用在业务代码中引入它(即 import $ from 'jquery'
),则会直接导致报错。毕竟这也违背了 externals
分离第三方库的初衷。
可以说,externals
方案满足了我们大多数功能!
五、将第三方库从业务文件分离 - Entry + CommonsChunkPlugin
前面说到,由于第三方库几乎不怎么会变动,所以,我们通常希望这样处理它:
- 打包时,不对第三方库进行打包(加快打包速度)
- 将第三方库与业务代码分离,让第三方库充分利用浏览器缓存
5.1 最初打包
为方便说明,我们首先安装了两个工具库,分别是 jQuery 和 lodash:
npm i -S jquery lodash |
假设有公共函数模块:
// utils.js |
再假设有以下两个业务js文件:
// app1.js |
// app2.js |
让我们调整下配置文件:
// webpack.config.js |
然后,打包后生成了 app1.min.js
和 app2.min.js
两个文件,页面也正常引用了这两个js文件,功能也都正常。
但是,当打开 app1.min.js
和 app2.min.js
,你会发现,这两个文件都包含了 jQuery、lodash 以及 utils.js 的源码,也就是说,webpack对于不同业务模块引用的公用文件(工具库)会重复打包,这显然是不对的。我们希望的理想情况是,所有的公用文件,只打包一次,在页面里也只引入一次。
那我们就要使用到 Entry + CommonsChunkPlugin
。
5.2 公用资源的分离
再次调整配置文件,在入口文件中新增第三方库的配置:
// webpack.config.js |
再次打包,生成了 vendor.js
、app1.min.js
、app2.min.js
这三个文件。此时,再打开 app1.min.js
、app2.min.js
,会发现里面只包含我们自己写的业务代码,而 jQuery、lodash 以及 utils.js 的源码则被打进了 vendor.js
这个文件中。
使用这种方式打包,除了可以分离公用资源、避免重复打包以外。对第三方库(这里指的是 jQuery、lodash),在业务代码中还可以无需引入模块,便能使用第三方库的API。比如,app1.js
可以调整为:
// app1.js 无需在此文件中引入 jquery 和 lodash |
5.3 第三方库、工具函数模块,业务代码的分离
到这里为止,我们基本达到了公用资源与业务代码分离的需求。但是,由于公用资源又包含 第三方库 和 工具函数模块。可以肯定的是,第三方库我们几乎不改,但 工具函数模块 则可能会不定时的修改。因此,最理想的情况是,把 第三方模块 和 工具函数模块 也分离开来。
这个也好办,只需要利用 chunks
再指定引用了 工具函数模块 的js文件即可。再次调整配置文件:
// webpack.config.js |
再次打包,则生成了 vendor.js
、common.js
、app1.min.js
、app2.min.js
这四个文件。它们的内容分别如下:
vendor.js
: 第三方库代码(这里指 jQuery + lodash)common.js
: 工具函数模块app1.min.js
: 业务代码1app2.min.js
: 业务代码2
这样,便达到我们最终的目的。打开这四个文件,你会发现,后面三个都是以 webpackJsonp([0], ...)
、webpackJsonp([1], ...)
… 这样开头的webpack运行模块。
并且,由于我们是将第三方库单独打包在一个文件里,因此,你可以在该文件后面或者浏览器控制台使用第三方库的API。
不过注意,在 webpack4.x 中,CommonsChunkPlugin
这种形式已经被废弃了。
当你尝试在 webpack4.x 中使用 CommonsChunkPlugin
,在命令面板会出现报错,并提示我们用 optimization.splitChunks
:

六、webpack4.x 模块分离 - optimization + splitChunks
在 webpack4.x 中,我们使用 optimization.splitChunks
来分离公用的代码块。
这里说的分离,当然只是针对一些第三方库(一般来自 node_modules),以及我们自己定义的工具库(或公用方法)。不然,还分离啥呢?
不知如何下手?首先,我们来看官网给的一份默认配置:
optimization: { |
接着,我们再来看下它们的含义:
- chunks: 该属性值的数据类型可以是 字符串 或者 函数。如果是字符串,那它的值可能为 initial | async | all 三者之一。默认值的数据类型为 字符串,默认值为 async,但推荐用 all。它表示将哪种类型的模块分离成新文件。字符串参数值的作用分别如下:
- initial:表示对异步引入的模块不处理
- async:表示只处理异步模块
- all:无论同步还是异步,都会处理
- minSize: 该属性值的数据类型为数字。它表示将引用模块分离成新代码文件的最小体积,默认为
30000
,单位为字节,即 30K(指min+gzip之前的体积)。这里的 30K 应该是最佳实践,因为如果引用模块小于 30K 就分离成一个新代码文件,那页面打开时,势必会多增加一个请求。 - maxSize: 该属性值的数据类型为数字。它表示?
- minChunks: 该属性值的数据类型为数字。它表示将引用模块如不同文件引用了多少次,才能分离生成新代码文件。默认值为 1
- maxAsyncRequests: 该属性值的数据类型为数字,默认值为
5
。它表示按需加载最大的并行请求数,针对异步。 - maxInitialRequests: 该属性值的数据类型为数字,默认值为
3
。它表示单个入口文件最大的并行请求数,针对同步。 - automaticNameDelimiter: 该属性值的数据类型为字符串,默认值为
~
。它表示分离后生成新代码文件名称的链接符,比如说 app1.js 和 app2.js 都引用了utils.js
这个工具库,那么,最后打包后分离生成的公用文件名可能是xx~app1~app2.js
这样的,即以~
符号连接。 - name: 该属性值的数据类型可以是 布尔值 或者 函数(返回值为字符串),其中布尔值得为
true
,此时,分离文件后生成的文件名将基于cacheGroups
和automaticNameDelimiter
。如果设置为false
,则不会进行模块分离。 - cacheGroups: 该属性值的数据类型为对象,它的值可以继承
splitChunks.*
中的内容。如果cacheGroups
存在与splitChunks.*
同名的属性,则cacheGroups
的属性值则直接覆盖splitChunks.*
中设置的值。 - test: 该属性值的数据类型可以为 字符串 或 正则表达式,它规定了哪些文件目录的模块可以被分离生成新文件。
- priority: 该属性值的数据类型可以为数字,默认值为
0
。它表示打包分离文件的优先级。 - reuseExistingChunk: 该属性值的数据类型可以为布尔值。它表示针对已经分离的模块,不再重新分离。
那么问题来了,我们该如何理解这些属性配置?并且利用它们实现更加高效的打包?
其实没有什么,要验证这些配置参数,只不过是反复打包验证的过程。
6.1 默认配置
在开始验证之前,我们准备三个入口文件(app1、app2、app3)以及它们各自引用的模块(jQuery、Lodash、React均来自 node_modules,utils 则是本地定义的工具函数文件),其中分离打包采用默认的配置:
// webpack.config.js |
打包后,得到:

可以看到,由于采用默认分离打包配置,即 chunks: 'async'
。而我们代码中没用使用到异步加载模块,此时打包,并没有分离出单独的模块文件。所以输出的文件是下面这三个。它们的内容分别为:
- app1.min.js:jQuery、Lodash、utils 的源码和 app1.js 的业务代码
- app2.min.js:jQuery、Lodash、utils 的源码和 app2.js 的业务代码
- app3.min.js:React、React-dom 的源码和 app3.js 的业务代码
6.2 分离第三方库
要将第三方库分离出来,我们需要调整配置文件,设置 chunks: 'all'
,即表示让所有加载类型的模块在某些条件下都能打包:
// webpack.config.js |
打包后,得到:

由于 jQuery、Lodash、React、React-dom 未压缩之前的源码都超过了30K,所以它们被分离成独立的文件。由于它们都来自 node_modules
,所以这些分离文件的名称前缀是我们的配置 vendors
, 接着使用 automaticNameDelimiter
的值(符号 ‘~’)来连接共同引入它们的业务文件名,如 vendors~app1~app2.min.js
和 vendors~app3.min.js
。
它们的内容分别为:
- vendors~app1~app2.min.js:jQuery、Lodash的源码
- app1.min.js:utils 的源码和 app1.js 的业务代码
- app2.min.js:utils 的源码和 app2.js 的业务代码
- vendors~app3.min.js:React、React-dom 的源码
- app3.min.js:app3.js 的业务代码
6.3 分离工具函数
在上面的打包中,我们发现,工具函数模块(utils)的源码被分别打包到了 app1.min.js 和 app2.min.js 这两个文件中,这显然是不对。之所以出现这种情况,是因为我们设置了 minSize: 30000
,即分离成独立文件的最小体积为 30K,而这里的 工具函数(utils.js)只有几KB,所以,没被分离成单独的文件。
我们在 cacheGroups
中重新设置 minSize
的值,这样,就能覆盖 splitChunks.*
里面 minSize
的默认值:
// webpack.config.js |
打包后,得到:

这样就分离出了 工具函数模块。它们的内容分别为:
- vendors~app1~app2.min.js:jQuery、Lodash的源码
- default~app1~app2.min.js:utils 的源码
- app1.min.js:app1.js 的业务代码
- app2.min.js:app2.js 的业务代码
- vendors~app3.min.js:React、React-dom 的源码
- app3.min.js:app3.js 的业务代码
6.4 第三方库合并打包并重命名
有的时候,我们希望将所有来自 node_modules
的第三方库都打包到同一个文件中。显然,上面的打包配置并没有满足这个条件。并且,我们还希望可以对打包后的文件名进行重命名。
要完成,只需要在 cacheGroups
设置 name
属性即可。
// webpack.config.js |
打包后,得到:

这样不仅合并了所有来自 node_modules
的第三方库,还自定义了打包后的文件名称。它们的内容分别为:
- lib.min.js:jQuery、Lodash、React、React-dom 的源码 (由于包含的库都比较大,所有直接黄色了!!)
- utils.min.js:utils 的源码
- app1.min.js:app1.js 的业务代码
- app2.min.js:app2.js 的业务代码
- app3.min.js:app3.js 的业务代码
如果你嫌打包后的业务文件大,还可以结合 optimization.runtimeChunk
。它可以提取 entry chunk 中的 runtime函数部分,生成一个单独的文件:
// webpack.config.js |

可以看到,打包后的业务文件(app1、app2、app3)体积都变小了。
另外, cacheGroups
中还有一个关于优先级 priority
的属性,由于一个模块可以被分配到多个缓存组中,优化策略会将模块分配至跟高优先级别的缓存组,这对于将这个模块分离到更大体积的js文件,减少请求数特别有用。
最后,需要说明的是,test
、priorty
和 reuseExistingChunk
只能用于配置缓存组。