基于uniapp的小程序多版本方案与vendor.js体积异常排查解决

1,789 阅读6分钟

需求背景

后端服务架构重构,业务接口都需要走新的域名及上下文,由于涉及库表拆分,无法保证新接口跟旧接口一致性,也就无法通过接口转发的方式实现版本切换(接口转发方案确实也会带来新的代码维护性问题),需要前端针对小程序支持业务新旧两套版本环境。

方案设计

当时提出了两套实现方案:

1. 原有旧代码上做兼容

基于以下考虑舍弃了该方案:

  • 由于主包长期处于2M边缘,在旧代码上做兼容,后期面临主包体积问题;
  • 一份代码内,涉及版本判断的特殊逻辑场景比较多,比如请求相关的处理(域名、上下文、参数、返回值、vue模板字段的前端兼容-针对后端返回字段不一致)等;
  • 当前并行开发的版本,对于后续代码合并有可预见的痛苦;
  • 后端完成新旧服务流量切换后,前端这些特殊逻辑都需要手动删除,容易留下迭代垃圾;
  • 旧版本小程序总体积double后 < 20M(小程序上限),方案2具备可行性。
2. 两个版本页面独立维护

页面独立维护意味着所有的页面都需要有一个对应的新版本,比如之前的主包page,则需要配置一个分包 pageNew 作为新版本的首页容器;分包 subPagesA -> subPagesANew (用统一的New后缀区分新版本页面,方便后续对于页面跳转的路径替换)

处理两个跳转入口
  • 底部tab栏的reLaunch,需要在主包页面onLoad下判断新版本后,跳转到分包pageNew内(所以需要自定义tabber);

  • utils的跳转方法内部基于版本标识为新版本用户加上New后缀(各个页面都使用该方法跳转,这样就保证不需要在copy后的新版本代码内去各个地方改跳转路径)。

image.png

处理请求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,炸裂

image.png

image.png

明明只针对主包更新了一些特殊逻辑代码,为什么主包溢出这么多?

分析vendor文件
  • vendor

vendor是代码构建过程中,将公共代码(多处使用的代码)合并提取为一个bundle模块的文件统称,公共代码比如第三方库、框架等等,合并提取后避免对相同模块重复加载和声明,有效节约资源和提高后续模块响应效率。

uniapp编译构建后的vendor包括:

  • vue-core 代码,大概300多kb
  • 主包所有页面的公共模块代码
  • 跨分包下的公共模块代码

之前的vendor文件只有600多kb,为什么新版本加入后成倍数增加?于是决定对两个vendor.js进行diff,果然发现了问题

1. 大量的base64图片

历史代码中很多基于require引用的形式使用图片资源,这种方式会在构建时将图片转成本地base64形式,由于多版本模式下,新旧两个分包对同一个图片require,会被引用标记,从而提取到了vendor中;

解决办法就是将require引用的图片,改为绝对路径或相对路径引用(这里在新分包内的路径记得同样需要New后缀处理)。

image.png

2. ucharts三方库提取到vendor

从vendor.js体积上来看,总体积远大于double的量,那么多余的体积是怎么导致的呢?尤其是处理完图片之后,vendor的体积仍然不对劲。

继续分析vendor文件发现,多了一个三方库 u-charts:

原来是以前只在分包内用了该库,所以该三方库会被构建到分包的js文件中,而多分包下,与图片的require处理一样,被提取到了vendor.

image.png

这个问题看似无解,其实还是有解:

  • 创建一个新分包,将两个版本下,使用到该三方库的页面统一都放到新分包内(会对旧业务造成侵入式变更,尤其要考虑如果你的页面涉及分享,从产品体验性上需要处理用户从分享旧外链的跳转);

  • 新版本的镜像分包内的涉及到该三方库的新页面,单独配置到旧分包中,在生成跳转新路径的方法中,针对这些涉及三方库的页面做特殊处理。

由于涉及页面只有8个,所以最终还是选择了改动相对较快的方案2。

最终优化效果

image.png

经过上传压缩,主包控制在2M以内,只剩8kb,可惜的是这8KB是留给开发的,不是留给产品的···

image.png

扩展

由于时间有限,其实后续的优化方向有两个:

  1. 本地图片资源线上化处理;

  2. 对于自定义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')
}