需求背景
后端服务架构重构,业务接口都需要走新的域名及上下文,由于涉及库表拆分,无法保证新接口跟旧接口一致性,也就无法通过接口转发的方式实现版本切换(接口转发方案确实也会带来新的代码维护性问题),需要前端针对小程序支持业务新旧两套版本环境。
方案设计
当时提出了两套实现方案:
1. 原有旧代码上做兼容
基于以下考虑舍弃了该方案:
- 由于主包长期处于2M边缘,在旧代码上做兼容,后期面临主包体积问题;
- 一份代码内,涉及版本判断的特殊逻辑场景比较多,比如请求相关的处理(域名、上下文、参数、返回值、vue模板字段的前端兼容-针对后端返回字段不一致)等;
- 当前并行开发的版本,对于后续代码合并有可预见的痛苦;
- 后端完成新旧服务流量切换后,前端这些特殊逻辑都需要手动删除,容易留下迭代垃圾;
- 旧版本小程序总体积double后 < 20M(小程序上限),方案2具备可行性。
2. 两个版本页面独立维护
页面独立维护意味着所有的页面都需要有一个对应的新版本,比如之前的主包page,则需要配置一个分包 pageNew 作为新版本的首页容器;分包 subPagesA -> subPagesANew (用统一的New后缀区分新版本页面,方便后续对于页面跳转的路径替换)
处理两个跳转入口
-
底部tab栏的reLaunch,需要在主包页面onLoad下判断新版本后,跳转到分包pageNew内(所以需要自定义tabber);
-
utils的跳转方法内部基于版本标识为新版本用户加上New后缀(各个页面都使用该方法跳转,这样就保证不需要在copy后的新版本代码内去各个地方改跳转路径)。
处理请求API
// 针对旧API常量
const path = 'https://use.service.com' + ‘cost’
const getOrderList = () => request(path + '/order/list', params)
// 多版本兼容改造后
const path = () => {
if(isNewVersion) {
return 'https://use.service_new.com' + ‘costNew’
}
return 'https://use.service.com' + ‘cost’
}
const getOrderList = () => request(path() + '/order/list', params)
// 以及可能的:
const getOrderList = () => {
const url = isNewVersion ? '/orderNew/list' : '/order/list'
return request(path() + url, params)
}
以上用示例代码的形式解释对api改造的思路,核心就是尽量保证对于旧代码的改动最小,将多版本兼容性的处理统一抽象到公共逻辑中去,减少对业务代码的污染。
理想很完美,但是当方案落地后,主包达到3094.2kb,炸裂
明明只针对主包更新了一些特殊逻辑代码,为什么主包溢出这么多?
分析vendor文件
- vendor
vendor是代码构建过程中,将公共代码(多处使用的代码)合并提取为一个bundle模块的文件统称,公共代码比如第三方库、框架等等,合并提取后避免对相同模块重复加载和声明,有效节约资源和提高后续模块响应效率。
uniapp编译构建后的vendor包括:
- vue-core 代码,大概300多kb
- 主包所有页面的公共模块代码
- 跨分包下的公共模块代码
之前的vendor文件只有600多kb,为什么新版本加入后成倍数增加?于是决定对两个vendor.js进行diff,果然发现了问题
1. 大量的base64图片
历史代码中很多基于require引用的形式使用图片资源,这种方式会在构建时将图片转成本地base64形式,由于多版本模式下,新旧两个分包对同一个图片require,会被引用标记,从而提取到了vendor中;
解决办法就是将require引用的图片,改为绝对路径或相对路径引用(这里在新分包内的路径记得同样需要New后缀处理)。
2. ucharts三方库提取到vendor
从vendor.js体积上来看,总体积远大于double的量,那么多余的体积是怎么导致的呢?尤其是处理完图片之后,vendor的体积仍然不对劲。
继续分析vendor文件发现,多了一个三方库 u-charts:
原来是以前只在分包内用了该库,所以该三方库会被构建到分包的js文件中,而多分包下,与图片的require处理一样,被提取到了vendor.
这个问题看似无解,其实还是有解:
-
创建一个新分包,将两个版本下,使用到该三方库的页面统一都放到新分包内(会对旧业务造成侵入式变更,尤其要考虑如果你的页面涉及分享,从产品体验性上需要处理用户从分享旧外链的跳转);
-
新版本的镜像分包内的涉及到该三方库的新页面,单独配置到旧分包中,在生成跳转新路径的方法中,针对这些涉及三方库的页面做特殊处理。
由于涉及页面只有8个,所以最终还是选择了改动相对较快的方案2。
最终优化效果
经过上传压缩,主包控制在2M以内,只剩8kb,可惜的是这8KB是留给开发的,不是留给产品的···
扩展
由于时间有限,其实后续的优化方向有两个:
-
本地图片资源线上化处理;
-
对于自定义tabber改造彻底,目前主包内的4个tabbar栏,只有一个用的是分包改造,还有2个页面可切换到分包内,这样主包就只留一个首页即可;
附上部分代码实现
/** 首页reLanuch处理 */
const routeHash = {
'pages/home/index': 'pagesNew/home/index',
// ···
}
export const reLaunchToNewPage = (options) => {
const pageList = getCurrentPages()
const pageKey = pageList[0].route
let baseUrl = routeHash[pageKey]
if (!baseUrl) {
throw '未配置当前路由映射' + pageKey
}
if (!baseUrl.startsWith('/')) {
baseUrl = '/' + baseUrl
}
if (options && Object.keys(options).length) {
const query = []
for (let key in options) {
query.push(key + '=' + options[key])
}
const queryStr = query.join('&')
baseUrl += `?${queryStr}`
}
wx.reLaunch({ url: baseUrl })
}
/** 三方库路由跳转方法处理 */
const specRoutePath = [
'pages/home/index', // 新版本跳转 'pages/homeNew/index'
'pages/useCharts/index', // 新版本跳转 'pages/useChartsNew/index'
]
// 这里的key对应的是路径文件夹
const pathKeyList = ['home', 'useCharts']
export const generateNewPath = (path) => {
const isSpecPath = specRoutePath.find(item => path.includes(item))
if (isSpecPath) {
// 分包不变,路径变更,针对三方库集成场景
const pathKey = pathKeyList.find(rt => isSpecPath.includes(rt))
return path.replace(pathKey, pathKey + 'New')
}
const pathList = path.split('/')
const root = pathList[0] || pathList[1]
// 镜像分包新路径
return path.replace(root, root + 'New')
}