前身 - CommonsChunkPlugin
webpack4之前,大家一直都是用CommonsChunkPlugin来做代码切割。那用他具体有什么坑呢?
-
我用起来感觉最诡异的点就是每次切割之后,新生成的chunk会和被提取的chunk生成一种父子关系(生成的公共chunk为entry chunk,之前有写过一篇文章介绍),即被提取的chunk会依赖于生成的chunk。下一次再做提取的时候,除非手动指定,只会扫描所有的entry chunk.
-
感觉CommonsChunkPlugin的表现也不是那么稳定,之前写了一个把node_modules里面的module全打包到vendor chunk里,明明minChunks属性设置成了Infinity,线上还是出了锅,同事在node_modules里面装了一个包莫名其妙地被打到了业务代码里,害我出了一次事故。
SplitChunksPlugin
升级了webpack4之后,production模式下,SplitChunksPlugin插件是默认被启用的,默认配置如下:
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
复制代码
这个配置是啥意思呢?老规矩,一个个参数的来看:
- chunks: 可填
async
,initial
,all
. 顾名思义,async针对异步加载的chunk做切割,initial针对初始chunk,all针对所有chunk。 - minSize: 我们切割完要生成的新chunk要>30kb,否则不生成新chunk。
- minChunks: 共享该module的最小chunk数
- maxAsyncRequests:最多有5个异步加载请求该module
- maxInitialRequests: 初始化的时候最多有3个请求该module
- automaticNameDelimiter:名字中间的间隔符
- name:chunk的名字,如果设成true,会根据被提取的chunk自动生成。
- cacheGroups: 这个就是重点了,我们要切割成的每一个新chunk就是一个cache group。
- test:和CommonsChunkPlugin里的minChunks非常像,用来决定提取哪些module,可以接受字符串,正则表达式,或者函数,函数的一个参数为module,第二个参数为引用这个module的chunk(数组).
- priority:优先级高的chunk为被优先选择(说出来感觉好蠢),优先级一样的话,size大的优先被选择
- reuseExistingChunk: 当module未变时,是否可以使用之前的chunk.
看到这里八成是一脸懵逼,这特么是在说啥?别急,具体来看几个例子就清楚了:
const entry = {
index: "./src/index.jsx",
index2: './src/index2.jsx',
}
复制代码
有index和index2两个entry(都引用了react和react-dom):
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
复制代码
import React from 'react';
import ReactDOM from 'react-dom';
console.log(React, ReactDOM);
复制代码
然后App.jsx长这样:
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader'
class App extends PureComponent {
render() {
return (
<div>App...</div>
)
}
}
export default hot(module)(App);
复制代码
根据默认配置,是不会有任何切割的,因为我们只对async代码做切割。先把chunks改成initial,build得到如下结果:
如图,所有node_modules里面的引用都被打到了vendors~index~index2~.....里面. 现在我们再加一个group:
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
'react-vendor': {
test: (module, chunks) => /react/.test(module.context),
priority: 1,
},
复制代码
build得到如下结果:
因为react-vendor的优先级高,所以react相关module都被打到了react-vendor~index~index2-.....里面。可以看到index和index2 chunk里都包含了fbjs 和lib包,这是因为这两个包太小了,放到一起生成的chunk size<我们设置的30kb。我们把minSize设置成3kb,再次build:
可以看到fbjs和lib等小module被打到了vendors里面(react相关的module之所以被打到react-vendor 而不是vendor就是因为react-vendor的优先级高于vendor。 如果把react-vendor的优先级改成-11,则所有所有的module都会被打到vendor,不会生成react-vendor)。
接着我们再来看一下minchunks,在index2里我们引用moment
import React from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
console.log(React, ReactDOM, moment);
复制代码
把vendor group的minchunks 设成2,覆盖掉splitChunks里面上面设置的1,build:
如图,moment因为只被index2引用了一次,所以不会被提取出来.
除了这种简单的提取,通过test我们可以拿到module的信息(目前我只用过他的路径)和提取chunk的信息,然后自己进行组合来自定义生成新chunk。
vendors: {
test: (module, chunks) => {
let chunkName = '';
chunks.forEach(chunk => {
chunkName += chunk.name + ',';
})
console.log(module.context, chunkName, chunks.length);
return /node_modules/.test(module.context)
},
},
复制代码
另外,如果需要做持久缓存的话,虽然runtime可以通过设置runtimeChunk: true
来解决,但是moduleID,ChunkID的问题还是需要HashedModuleIdsPlugin等插件来解决(webpack实现持久缓存)。
参考文献
webpack 4: Code Splitting, chunk graph and the splitChunks optimization