从Mpx资源构建优化看splitChunks代码分割
背景
MPX是滴滴出品的一款增强型小程序跨端框架,其核心是对原生小程序功能的增强。具体的使用不是本文讨论的范畴,想了解更多可以去官网了解更多。
回到正题,使用MPX开发小程序有一段时间了,该框架对不同包之间的共享资源有一套自己的构建输出策略,其官网有这样一段描述说明:
总结关键的两点:
纯js资源
:主包引用则输出主包,或者分包之间共享也输出到主包非js资源,包括wxml、样式、图片等
:主包引用则输出主包,分包之间共享则输出到各自分包
很好奇MPX内部是怎么做到上面这种效果的,尤其是js资源,于是就拜读了@mpxjs/webpack-plugin@2.6.61
揭开其实现的细节。
mpx怎么实现的
首先简单介绍下MPX
是怎么整合小程序离散化的文件结构,它基于webpack打包构建的,用户在Webpack配置中只需要配置一个入口文件app.mpx,它会基于依赖分析
和动态添加entry
的方式来整合小程序的离散化文件,,loader会解析json配置文件中的pages域
和usingComponents域
中声明的路径,通过动态添加entry的方式将这些文件添加到Webpack的构建系统当中,并递归执行这个过程,直到整个项目中所有用到的.mpx
文件都加入进来。
重点来了,MPX在输出前,其借助了webpack的SplitChunksPlugin
的能力将复用的模块抽取到一个外部的bundle中,确保最终生成的包中不包含重复模块。
js资源模块的输出
@mpxjs/webpack-plugin
插件是MPX基于webapck构建的核心,其会在webpack所有模块构建完成的finishMoudles
钩子中来实现构建输出策略,主要是配置SplitChunks
的cacheGroup,后续webpack代码优化阶段会根据SplitChunks
的配置来输出代码。
apply(compiler) {
...
// 拿到webpack默认配置对象splitChunks
let splitChunksOptions = compiler.options.optimization.splitChunks
// 删除splitChunks配置后,webpack内部就不会实例化SplitChunkPlugin
delete compiler.options.optimization.splitChunks
// SplitChunkPlugin的实例化由mpx来接管,这样可以拿到其实例可以后续对其options进行修正
let splitChunksPlugin = new SplitChunksPlugin(splitChunksOptions)
splitChunksPlugin.apply(compiler)
...
compilation.hooks.finishModules.tap('MpxWebpackPlugin', (modules) => {
// 自动跟进分包配置修改splitChunksPlugin配置
if (splitChunksPlugin) {
let needInit = false
Object.keys(mpx.componentsMap).forEach((packageName) => {
if (!splitChunksOptions.cacheGroups.hasOwnProperty(packageName)) {
needInit = true
splitChunksOptions.cacheGroups[packageName] = getPackageCacheGroup(packageName)
}
})
if (needInit) {
splitChunksPlugin.options = SplitChunksPlugin.normalizeOptions(splitChunksOptions)
}
}
})
可以看出在所有模块构建完成时,针对不同的packageName来生成其对应的cacheGroups
,主要体现在getPackageCacheGroup方法的实现。然后拿到SplitChunksPlugin
实例的句柄,对其options进行重写规格化。
function isChunkInPackage (chunkName, packageName) {
return (new RegExp(`^${packageName}\\/`)).test(chunkName)
}
function getPackageCacheGroup (packageName) {
if (packageName === 'main') {
return {
name: 'bundle',
minChunks: 2,
chunks: 'all'
}
} else {
return {
test: (module, chunks) => {
return chunks.every((chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
},
name: `${packageName}/bundle`,
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
}
}
}
getPackageCacheGroup会为小程序的每个包生成一个代码分割组,也就是生成每个包对应的cacheGroups
。
例如一个小程序项目有主包和A、B两个分包,其生成的cacheGroups
内容如下:
{
default: {
automaticNamePrefix: '',
reuseExistingChunk: true,
minChunks: 2,
priority: -20
},
vendors: {
automaticNamePrefix: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10
},
main: { name: 'bundle', minChunks: 2, chunks: 'all' },
A: {
test: [Function: test],
name: 'A/bundle',
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
},
B: {
test: [Function: test],
name: 'B/bundle',
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
}
分包代码分割输出bundle的优先级是最高的(priority: 100
),所以会优先处理分包中的打包;否则会执行main
中的代码打包规则,它会处理所有包之间的共享模块的打包以及主包中被复用的模块。
下面来看分包和主包的打包规则。
1、针对分包中的模块:
{
test: (module, chunks) => {
// 依赖当前模块的所有chunks是否都是当前分包下的chunk
return chunks.every((chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
},
name: `${packageName}/bundle`,
minChunks: 2,
minSize: 1000,
priority: 100,
chunks: 'all'
}
分包中的模块被抽离到当前分包下的bundle文件中,需满足:
- 该模块没有被其他包引用,包括主包和其他分包(
test函数逻辑
) - 至少被该分包下的2个
chunk
引用(minChunks:2
) - 抽离后的bundle大小最少满足 约1kb(
minSize: 1000
)
2、针对主包中的模块:
{
name: 'bundle',
minChunks: 2,
chunks: 'all'
}
会抽取到主包的bundle文件的条件:
- 该模块至少被2个chunk引用(
minChunks:2
),这个chunk不区分主分包中的chunk
3、针对分包间共享的模块:
分包间共享的模块,不满足SplitChunks
为每个分包设置的分包独享规则,即该模块只在当前分包引用,没有在其他包中被引用过。
test: (module, chunks) => {
return chunks.every((chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
}
所以,最终会走到main
的cacheGroup的打包规则中,也就是主包的打包规则中。
这样,MPX通过配置SplitChunksOptions.cacheGroups
来实现将主包中的js模块和分包共享的js模块都输出到主包,分包单独引用的模块输出到当前分包下。
组件和静态资源
对于组件和静态资源,MPX在webpack构建的thisCompilation
钩子函数中会在compilation
上挂载一个有关打包的__mpx__
对象,包含静态资源、组件资源、页面资源等属性,也包含静态的非js资源的输出处理等:
compiler.hooks.thisCompilation.tap('MpxWebpackPlugin', (compilation, { normalModuleFactory }) => {
...
if (!compilation.__mpx__) {
mpx = compilation.__mpx__ = {
...
componentsMap: {
main: {}
},
// 静态资源(图片,字体,独立样式)等,依照所属包进行记录,冗余存储,同上
staticResourcesMap: {
main: {}
},
...
// 组件和静态资源的输出规则如下:
// 1. 主包引用的资源输出至主包
// 2. 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
// 3. 分包引用且无其他包引用的资源输出至当前分包
// 4. 分包引用且其他分包也引用过的资源,重复输出至当前分包
getPackageInfo: ({ resource, outputPath, resourceType = 'components', warn }) => {
let packageRoot = ''
let packageName = 'main'
const { resourcePath } = parseRequest(resource)
const currentPackageRoot = mpx.currentPackageRoot
const currentPackageName = currentPackageRoot || 'main'
const resourceMap = mpx[`${resourceType}Map`]
const isIndependent = mpx.independentSubpackagesMap[currentPackageRoot]
// 主包中有引用一律使用主包中资源,不再额外输出
if (!resourceMap.main[resourcePath] || isIndependent) {
packageRoot = currentPackageRoot
packageName = currentPackageName
...
}
resourceMap[packageName] = resourceMap[packageName] || {}
const currentResourceMap = resourceMap[packageName]
let alreadyOutputed = false
if (outputPath) {
outputPath = toPosix(path.join(packageRoot, outputPath))
// 如果之前已经进行过输出,则不需要重复进行
if (currentResourceMap[resourcePath] === outputPath) {
alreadyOutputed = true
} else {
currentResourceMap[resourcePath] = outputPath
}
} else {
currentResourceMap[resourcePath] = true
}
return {
packageName,
packageRoot,
outputPath,
alreadyOutputed
}
},
...
}
}
}
这样webpack构建编译非js资源时会调用compilation.__mpx__.getPackageInfo
方法返回非js的静态资源的输出路径,在该方法内部制定了如下资源的的输出规则:
- 主包引用的资源输出至主包
- 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
- 分包引用且无其他包引用的资源输出至当前分包
- 分包引用且其他分包也引用过的资源,重复输出至当前分包
这样,mpx在处理项目中的静态资源时,会调用该方法获得静态资源的输出路径。
下面以一个简单例子来说明,
例如对项目中的图片会调用@mpxjs/webpack-plugin
提供的url-loader
进行处理,与webpack的url-loader
类似,对于图片大小小于指定limit的进行base64
处理,否则使用file-loader
来输出图片(此时需要调用getPackageInfo方法获取图片的输出路径),相关代码:
let outputPath
if (options.publicPath) { // 优先loader配置的publicPath
outputPath = url
if (options.outputPathCDN) {
if (typeof options.outputPathCDN === 'function') {
outputPath = options.outputPathCDN(outputPath, this.resourcePath, context)
} else {
outputPath = toPosix(path.join(options.outputPathCDN, outputPath))
}
}
} else {
// 否则,调用getPackageInfo获取输出路径
url = outputPath = mpx.getPackageInfo({
resource: this.resource,
outputPath: url,
resourceType: 'staticResources',
warn: (err) => {
this.emitWarning(err)
}
}).outputPath
}
...
this.emitFile(outputPath, content);
...
最终,图片资源会调用compilation.__mpx__.getPackageInfo
方法来获取图片资源的输出路径进行产出。
同样对于css资源
、wxml资源
以及json资源
,mpx内部是通过创建子编译器来抽取的,这里就不做深入介绍。
splitChunks的用法
webpack的splitChunks
插件是用来进行代码拆分的,通过上面的分析可以看出MPX内部是通过内置splitChunks
的cacheGroups
配置项来主动实现对小程序js模块实现分割优化的。webpack常见的代码分割方式有三种:
- 多入口分割:webpack的
entry
配置项配置的手动入口,也包括可以使用compilation.addEntry
程序添加的入口 - 动态导入:通过模块的内联函数来分离代码,如通过
import('./a')
- 防止重复:使用
splitChunks
来去重和分离chunk
前两种在我们日常的开发中比较常见,第三种是通过webpack的optimization.splitChunks
配置项来配置的。
通常情况下,webpack配置项optimization.splitChunks
会有默认配置来实现代码分割,上面我们说到MPX在为不同包生成cacheGroups
时,细心的同学会发现我们最终生成的包多了两个配置组:
{
default: {
automaticNamePrefix: '',
reuseExistingChunk: true,
minChunks: 2,
priority: -20
},
vendors: {
automaticNamePrefix: 'vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10
},
...
}
这是webpack为optimization.splitChunks.cacheGroups
配置的默认组,除此之外optimization.splitChunks
还有一些其他默认配置项,如下代码所示:
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
}
}
}
上面默认配置的实现的效果是:满足下面4个条件的模块代码会抽离成新的chunk
- 来自
node_modules
中的模块,或者至少被2个chunk复用的模块代码 - 分离出的chunk必须大于等于3000byte,约30kb
- 按需异步加载chunk时,并行请求的最大数不超过5个
- 页面初始加载时,并行请求的最大数不超过3个
下面来介绍下这些配置项的作用:
chunks
:表示webpack将对哪些chunk进行分割,可选值为async、all、initial- async:对于异步加载的chunks进行分割
- initial:对非异步加载的初始chunks进行分割
- all:对所有chunks进行分割
minSize
: 分割后的chunk要满足的最小大小,否则不会分割minChunks
: 表示一个模块至少应被minChunks个chunk所包含才能分割maxAsyncRequests
: 表示按需加载异步chunk时,并行请求的最大数目;这个数目包括当前请求的异步chunk以及其所依赖chunk的请求maxInitialRequests
: 表示加载入口chunk时,并行请求的最大数目automaticNameDelimiter
: 表示拆分出的chunk的名称连接符,默认为。如chunkvendors.jsname
: 设置chunk的文件名,默认为true,表示splitChunks基于chunk和cacheGroups的key自动命名。cacheGroups
: 通过它可以配置多个组,实现精细化分割代码;- 该对象配置属性继承
splitChunks
中除cacheGroups
外所有属性,可以在该对象重新配置这些属性值覆盖splitChunks
中的值 - 该对象还有一些特有属性如
test
、priority
和reuseExistingChunk
等
- 该对象配置属性继承
针对cacheGroups
配置补充一点:
cacheGroups
配置的每个组可以根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority
来决定打包到哪个组中。
从Mpx资源构建优化看splitChunks代码分割的更多相关文章
- Webpack之optimization.splitChunks代码分割插件的配置
SplitChunkPlugin插件配置参数详解 对引入的库代码(例如:lodash.jQuery等)进行代码的分割进行优化 若配置时只写chunks:"all",其余则为默认配置 ...
- iOS开发UI篇—从代码的逐步优化看MVC
iOS开发UI篇—从代码的逐步优化看MVC 一.要求 要求完成下面一个小的应用程序. 二.一步步对代码进行优化 注意:在开发过程中,优化的过程是一步一步进行的.(如果一个人要吃五个包子才能吃饱,那么他 ...
- webpack优化之玩转代码分割和公共代码提取
前言 开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码).当项目业务越来越复杂,打包出来 ...
- Vuejs项目的Webpack2构建优化
最近在做的项目因为相对较大(打包有100多个chunk),在build构建的时候速度一直上不去,甚是烦恼.由于用的是vue-cli的webpack2模板,一开始并没有想着要对其进行优化,一直觉得是we ...
- Intel64及IA-32架构优化指南第8章多核与超线程技术——8.9 其它共享资源的优化
8.9 其它共享资源的优化 在多线程应用中的资源优化依赖于处理器拓扑层级内相关联的Cache拓扑以及执行资源.在第7章中讨论了处理器拓扑以及标识处理器拓扑的一种软件算法. 在带有共享总线的平台中,总线 ...
- NEO4J亿级数据全文索引构建优化
NEO4J亿级数据全文索引构建优化 一.数据量规模(亿级) 二.构建索引的方式 三.构建索引发生的异常 四.全文索引代码优化 1.Java.lang.OutOfMemoryError 2.访问数据库时 ...
- CssStats – 分析和优化网站 CSS 代码的利器
CssStats 是一个在线的 CSS 代码分析工具,你只需要输入网址或者直接 CSS 地址即可进行 CSS 代码的全方位分析,是前端开发人员和网页设计师分析网站 CSS 代码的利器,可以统计出 CS ...
- 前端资源构建-Grunt环境搭建
前端资源构建-Grunt 随着前端开发的复杂度越来越高,前端页面动辄上几十个js,十几个html页面.用户打开一个页面需要加载一堆的css,js,html文件,对于有大量用户的web应用来说,既消耗服 ...
- GCC扩展(转--对看kernel代码有帮助
http://my.oschina.net/senmole/blog?catalog=153878 Linux Kernel的代码,上次就发现一个结构体的定义形式看不懂,后来才知道它用的不是标准的AN ...
随机推荐
- css 第二排文字居中
text-align: center; 超过长度 自动换行居中
- canvas 隐藏 踩坑
当我在把canvas绘制完成时,要把canvas隐藏起来.试了display 和 opacity 都不行. 然后我用了 position: absolute; left:1000px; top:0; ...
- cookie,sessionStorage,localStorage
本文转 sessionStorage 和 localStorage 是HTML5 Web Storage API 提供的,可以方便的在web请求之间保存数据.有了本地数据,就可以避免数据在浏览器和服务 ...
- 使用 shell 脚本自动获取发版指标数据
问题背景 大一点的公司都会建立一套规章流程来避免低级错误,例如合入代码前必需经过同行评审:上线前必需提测且通过 QA 验证:全量前必需经过 1%.5%.10%.20%.50% 的灰度过程.尤其是最后一 ...
- XCTF练习题---CRYPTO---safer-than-rot13
XCTF练习题---CRYPTO---safer-than-rot13 flag:no_this_is_not_crypto_my_dear 解题步骤: 1.观察题目,下载附件 2.打开后发现是个文件 ...
- Java 18 新增@snipppet标签,注释中写样例代码更舒适了!
在这次的Java 18中,新增了一个@snipppet标签,主要用于JavaDoc中需要放示例代码的场景.其实在Java 18之前,已经有一个@code标签,可以用于在JavaDoc中编写小段的代码内 ...
- ansible的介绍与安装
ansible是在远程主机上批量执行命令或者脚本的一个工具 epel源:一个第三方的yum源(阿里巴巴开源镜像站:https://opsx.alibaba.com/mirror?lang=zh-CN) ...
- Spring Boot下Spring Batch入门实例
一.About Spring Batch是什么能干什么,网上一搜就有,但是就是没有入门实例,能找到的例子也都是2.0的,看文档都是英文无从下手~~~,使用当前最新的版本整合网络上找到的例子. 关于基础 ...
- vue2 sass主题一键修改功能
1.先创建一个 _theme.scss文件 作为各类主题颜色的整理 $themes: ( light: ( //字体 font_color1: #414141, font_color2: white, ...
- 使用 Vite 插件开发构建 Tampermonkey 用户脚本
起因 一直以来,我都是直接在浏览器 Tampermonkey 扩展页面直接新建用户脚本来开发的: 对于一些简单的脚本,这没有什么问题,即改即看.但当代码多了以后问题就来了,自带编辑器开发体验确实不太舒 ...