背景

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的静态资源的输出路径,在该方法内部制定了如下资源的的输出规则:

  1. 主包引用的资源输出至主包
  2. 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
  3. 分包引用且无其他包引用的资源输出至当前分包
  4. 分包引用且其他分包也引用过的资源,重复输出至当前分包

这样,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内部是通过内置splitChunkscacheGroups配置项来主动实现对小程序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进行分割,可选值为asyncallinitial

    • async:对于异步加载的chunks进行分割
    • initial:对非异步加载的初始chunks进行分割
    • all:对所有chunks进行分割
  • minSize: 分割后的chunk要满足的最小大小,否则不会分割
  • minChunks: 表示一个模块至少应被minChunks个chunk所包含才能分割
  • maxAsyncRequests: 表示按需加载异步chunk时,并行请求的最大数目;这个数目包括当前请求的异步chunk以及其所依赖chunk的请求
  • maxInitialRequests: 表示加载入口chunk时,并行请求的最大数目
  • automaticNameDelimiter: 表示拆分出的chunk的名称连接符,默认为。如chunkvendors.js
  • name: 设置chunk的文件名,默认为true,表示splitChunks基于chunk和cacheGroups的key自动命名。
  • cacheGroups: 通过它可以配置多个组,实现精细化分割代码;
    • 该对象配置属性继承splitChunks中除cacheGroups外所有属性,可以在该对象重新配置这些属性值覆盖splitChunks中的值
    • 该对象还有一些特有属性如testpriorityreuseExistingChunk

针对cacheGroups配置补充一点:

cacheGroups配置的每个组可以根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。

从Mpx资源构建优化看splitChunks代码分割的更多相关文章

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

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

  2. iOS开发UI篇—从代码的逐步优化看MVC

    iOS开发UI篇—从代码的逐步优化看MVC 一.要求 要求完成下面一个小的应用程序. 二.一步步对代码进行优化 注意:在开发过程中,优化的过程是一步一步进行的.(如果一个人要吃五个包子才能吃饱,那么他 ...

  3. webpack优化之玩转代码分割和公共代码提取

    前言 开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码).当项目业务越来越复杂,打包出来 ...

  4. Vuejs项目的Webpack2构建优化

    最近在做的项目因为相对较大(打包有100多个chunk),在build构建的时候速度一直上不去,甚是烦恼.由于用的是vue-cli的webpack2模板,一开始并没有想着要对其进行优化,一直觉得是we ...

  5. Intel64及IA-32架构优化指南第8章多核与超线程技术——8.9 其它共享资源的优化

    8.9 其它共享资源的优化 在多线程应用中的资源优化依赖于处理器拓扑层级内相关联的Cache拓扑以及执行资源.在第7章中讨论了处理器拓扑以及标识处理器拓扑的一种软件算法. 在带有共享总线的平台中,总线 ...

  6. NEO4J亿级数据全文索引构建优化

    NEO4J亿级数据全文索引构建优化 一.数据量规模(亿级) 二.构建索引的方式 三.构建索引发生的异常 四.全文索引代码优化 1.Java.lang.OutOfMemoryError 2.访问数据库时 ...

  7. CssStats – 分析和优化网站 CSS 代码的利器

    CssStats 是一个在线的 CSS 代码分析工具,你只需要输入网址或者直接 CSS 地址即可进行 CSS 代码的全方位分析,是前端开发人员和网页设计师分析网站 CSS 代码的利器,可以统计出 CS ...

  8. 前端资源构建-Grunt环境搭建

    前端资源构建-Grunt 随着前端开发的复杂度越来越高,前端页面动辄上几十个js,十几个html页面.用户打开一个页面需要加载一堆的css,js,html文件,对于有大量用户的web应用来说,既消耗服 ...

  9. GCC扩展(转--对看kernel代码有帮助

    http://my.oschina.net/senmole/blog?catalog=153878 Linux Kernel的代码,上次就发现一个结构体的定义形式看不懂,后来才知道它用的不是标准的AN ...

随机推荐

  1. toFixed()与银行家舍入

    toFixed()与银行家舍入 一直在用toFixed()方法做浮点数的舍入取值,如果只是客户端展示数据是没有多大问题的,但是如果涉及到和后端互交,数据的精度可能会导致接口对接失败,当然了,涉及安全性 ...

  2. OpenHarmony 3.1 Release版本发布

    OpenHarmony 3.1 Release 版本概述 当前版本在OpenHarmony 3.1 Beta的基础上,更新支持以下能力: 标准系统基础能力增强 本地基础音视频播放能力.视频硬编解码.相 ...

  3. Java语言学习day01--6月28日

    Java语言学习day01一:Java概述 1.Java语言发展史 任职于太阳微系统的 詹姆斯·高斯林 等人于1990年代初开发Java语言的雏形,最初被命名为 Oak ,目标设置在 家用电器等小型系 ...

  4. Java语言学习day17--7月23日

    1.面向对象思想2.类与对象的关系3.局部变量和成员变量的关系4.封装思想5.private,this关键字6.随机点名器 ###01面向对象和面向过程的思想 * A: 面向过程与面向对象都是我们编程 ...

  5. SpringCloud入门简述

    1.微服务简述 ​ 微服务,是一个小型的服务,也是一种设计理念,将一个大型繁杂的系统拆分为多个小型的服务,进行独立部署,这些服务在独立进程中运行,通过特定的协议进行通信 ​ 优点: 轻量化:一个服务不 ...

  6. 联邦平均算法(Federated Averaging Algorithm,FedAvg)

    设一共有\(K\)个客户机, 中心服务器初始化模型参数,执行若干轮(round),每轮选取至少1个至多\(K\)个客户机参与训练,接下来每个被选中的客户机同时在自己的本地根据服务器下发的本轮(\(t\ ...

  7. C#常见控件与SQL Sever数据库交互

    C#常见控件与SQL Sever数据库交互 下拉框(ComboBox)与数据库绑定 首先,我们采用DataSet作为临时的数据库,这样会比较好 那么,我们先创建两个成员(对象) string sqlc ...

  8. 如何对用户的绑定的身份证真实性进行实名认证(java)

    现在随着对用户实名制的要求,因此用户提交的身份证信息经查需要检查是否为真实信息,我们需要对用户提交的身份证信息进行核验,具体操作步骤如下: 第一步 到认证平台注册账号:云亿互通--实名认证服务 (yu ...

  9. 2021 CSP-J复赛 我的备战与游记

    目录 备战 2021.10.18 2021.10.19 2021.10.20 2021.10.21 2021.10.22 比赛当日 早上 线下见面 正文 比赛后 赛后总结与讲解 简单总结 Candy ...

  10. apache tomcat 目录session应用信息漏洞

    Tomcat 是一款开源的 Web 应用服务器软件.Tomcat 属于轻量级应用服务器,在中小型系统和并发访问用户不多的场合下被普遍使用,是开发和调试 JSP 程序的首选. 漏洞描述 apache T ...