小程序优化-接入分包实践

一、业务背景

1.1 前情提要

小程序作为当今社会一个流量的主要承载形式,具有方便快捷,随用随走的特性,因此收到不少企业作为自己产品的一个业务拓展承载渠道。但是随着业务不断迭代,小程序主包体积大小极容易就收到微信的 2M 的限制(这里省略针对静态资源和字体图标等资源的优化处理),我们也毫不例外的遇到了这个构建上传包体积超限制的瓶颈问题。

查询了下微信小程序官方文档,给出了这种情况下的唯一解 -- 分包。那木得办法了,也就只能老老实实的对当前小程序进行改造升级,接入分包的技术,能够保证代码能上传审核发布,不阻塞业务的迭代发展。

1.2 分包概念

分包的定义

借用腾讯的微信小程序文档上面的原话:

某些情况下,开发者需要将小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。

在构建小程序分包项目时,构建会输出一个或多个分包。每个使用分包小程序必定含有一个主包。所谓的主包,即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本;而分包则是根据开发者的配置进行划分。

在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示。

简单来讲,就是将代码拆分除了主包以外的各个额外的代码包里面,当进入分包页面时候,能够自动将该分包代码下载下来(类似于懒加载的形式)。

分包的配置

app.json 配置

 
json
复制代码
{
  "pages":[
    "pages/index",
    "pages/logs"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/cat",
        "pages/dog"
      ]
    }, {
      "root": "packageB",
      "name": "pack2",
      "pages": [
        "pages/apple",
        "pages/banana"
      ]
    }
  ]
}

subpackages 中,每个分包的配置有以下几项:

字段类型说明
root String 分包根目录
name String 分包别名
pages Array 分包页面路径,相对于分包根目录

主要是在 app.json 文件当中配置 subpackages 分包的相关各自分包及包内页面的设置,看起来配置的难度也不大。





二、优化实践

2.1 拆分调研

拆分页面

主要诉求:为了解决微信小程序对主包代码上传体积的 2MB 的限制。

基本宗旨:在进行技术优化同时保证系统的稳定运行与平稳发布。

拆分分包页面的划分:为了将影响面尽可能降低,从访问入口链路深浅,访问量,业务承载重要性等几个方面对当前页面进行综合性划分。

分包目录配置:

包的初步概念划分:

  • 主包:主页底部 tab 关联的页面;
  • common 包:多个小程序通用的包;
  • aaa / bbb / ccc ··· 包:各个独立业务的分包

2.2 实践落地

页面资源引用:

将所有引用资源全部改成绝对路径:

组件、图片、js、样式这类资源的引入进行调整,需要将相对路径改成绝对路径,这里推荐使用打包构建工具提供的路径别名功能来简化绝对路径的写法。

路由跳转的兼容:

因为分包后不仅仅页面文件的路径会发生变化,页面的路由也会随之发生变化,因此为了让各端和用户能够无感前端小程序的改造,因此小程序端内需要做好改造前后的路由跳转兼容问题。

小程序页面跳转的各个形式梳理

  • 小程序内页面之间跳转
  • 小程序 webview 页跳转到小程序原生页面
  • 分享的小程序卡片进入小程序
  • 扫码进入小程序
  • 从服务号/公号众通过消息提醒进入小程序

根据跳转类型进行一个归类后及对应的兼容处理方案

1、小程序页面内跳转

针对原生的各个跳转方法进行拦截封装,兼容分包后的真实页面路径的跳转。

 
javascript
复制代码
// 小程序没有提供获取分包信息的接口,分包信息写死
const subPackages = [
  'pages/common/',
  'pages/aaa/',
  'pages/bbb/',
  'pages/ccc/',
  // ···
]

let routeMap = null, navigateRewrote = false;

function getRouteRedirectMap() {
  if (routeMap) return routeMap

  // 获取配置的所有页面
  const pages = __wxConfig.pages

  routeMap = pages.reduce((res, path) => {
    const packageName = subPackages.find((item) => {
      return path.startsWith(item)
    })

    if (packageName) {
      // 建立新旧连接映射 eg. /pages/test/index => /pages/main/test/index
      res[`/${path.replace(packageName, 'pages/')}`] = `/${path}`
    }

    return res
  }, {})

  return routeMap
}

export function getRealPath(path) {
  const result = getRouteRedirectMap()[path]

  return result ? result : path
}

const generateFn = (navigate) => {
  return (options) => {
    const fullPath = options.url

    const { url: path, query } = parseUrl(fullPath)

    const newOptions = {
      ...options,
      url: stringifyUrl({
        url: getRealPath(path),
        query,
      }),
      fail(...args) {
        options.fail(...args)
      },
    }

    return navigate(newOptions)
  }
}

export default {
  navigateTo(options) {
    wx.navigateTo(options)
  },
  redirectTo(options) {
    wx.redirectTo(options)
  },
  reLaunch(options) {
    wx.reLaunch(options)
  },
  switchTab(options) {
    wx.switchTab(options)
  },

  rewriteNavigate() {
    if (navigateRewrote) {
      return
    }

    [
      'navigateTo',
      'redirectTo',
      'reLaunch',
    ].forEach((fnt) => {
      this[fnt] = generateFn(wx[fnt])
    })

    navigateRewrote = true
  },
}

2、小程序 webview 页跳转到小程序原生页面

webview 的 h5 页面要跳转到小程序原生页面需要使用微信自己的 weixin-js-sdk 的 navigateTo API,在此之上需要进行路由映射的封装。

 
php
复制代码
import { parseUrl, stringifyUrl } from 'common'

const ROUTE_MAP = {
  '/pages/subject/index': '/pages/common/subject/index',
  '/pages/webview/index': '/pages/common/webview/index',
  '/pages/test1/index': '/pages/aaa/test1/index',
  '/pages/test2/index': '/pages/bbb/test2/index',
  // ··· ···
}

/**
 * 将旧路径转换成分包后的路径
 * @param {String} fullPath 页面路径
 * @return {String}
 */
function getRealPath(fullPath) {
  const { url: path, query } = parseUrl(fullPath)

  return stringifyUrl({
    url: ROUTE_MAP[path] || path,
    query,
  })
}

/**
 * 微信内网页跳转
 * @param {String} options 页面路径
 */
function navigateTo(options) {
  options.url = stringifyUrl({
    url: getRealPath(options.url, this.$route.query.weapp_type),
    query: options.query,
  })
  delete options.query

  this.$wx.miniProgram.navigateTo(options)
}

export default {
  navigateTo,
  getRealPath,
}

3、分享的小程序卡片进入小程序

点击他人分享的小程序卡片进入小程序,如果要跳转的页面不存在会触发 onPageNotFound 方法,做个页面重定向就可以跳转到分包后的页面。

Ps:对于小程序内页面跳转失败是不会触发 onPageNotFound 的,因此小程序需要对内部的跳转方法进行重新封装支持(上文已提及封装的分包跳转方法)。

4、扫旧的小程序码进入小程序

  1. 如何兼容以前已经上传到 OSS 的小程序码图片的链接,使之扫码后能正常进入页面
    1. 参考上文的 onPageNotFound的封装处理解决;
  1. 如何避免分包后导致小程序码生成失败
    1. 保留一份分包前的页面,这个页面只做一件事,就是在重定向到分包后的页面
    2. 保留原有的页面及路由,但是页面内容变得仅声明周期钩子函数进行一个重定向;

5、从服务号/公号众通过消息提醒进入小程序

因为点击跳转的路由链接也是中间层下发固定了,因此处理与小程序码的处理是类似的,通过对旧路由页面内进行一个重定向映射处理即可,也就是上文中提到的保留原页面和路由进行分包新页面的重定向路由跳转。

思考总结:

总体分析来看,其实很多跳转的形式都可以通过在小程序内保留原有页面路由,然后在原有页面内通过重定向到新的分包的页面这种方法来兼容处理,因此重心也是在这方面的处理上。

但是这块会存在一个问题,就是会加重页面切换时候的白屏时长的问题。


2.3 实践中的打包构建优化

  1. 配置需要自动生成的路由及原路由 wxml 和 js 文件的分包页面
 
java
复制代码
// weapp-back-pages.js 需要自动生成对应 wxml 与 js 文件的页面配置

module.exports = {
	'pages/xxx/index': 'pages/common/xxx/index',
  'pages/yyy1/index': 'pages/aaa/yyy1/index',
  'pages/yyy2/index': 'pages/aaa/yyy2/index',
  // ··· ···
}
 
javascript
复制代码
// 原页面 wxml 与 js 重定向模板文件

// backpage.js - routePath 是 webpack plugin 注入 “转跳目标路径” 的内容写进去
Page({
  onLoad: function () {
    var options = this.options, queries = ''

    for (var key in options) {
      var value = options[key]
      queries += `${key}=${value}&`
    }

    queries = queries.substring(0, queries.length - 1)

    wx.redirectTo({ url: `${routePath}?${queries}` })
  },
})
  1. 利用 webpack plugin 通过上述配置自动生成对应的 wxml 和 js 页面文件和自动化修改 app.json 内的配置
  • 触发时机:生成资源到 output 目录之前(compiler.hooks.emit
  • 触发操作:通过修改每一次 webpack 编译运行的 compilation 对象的 assets 来达到修改生成的最终资源文件
    • webpack-sources 的 ConcatSource 合并输出的资源文件
 
javascript
复制代码
const ConcatSource = require('webpack-sources').ConcatSource
const { readFile } = require('fs').promises
const UglifyJS = require('uglify-es')
const minimist = require('minimist')

const BACK_PAGES_MAP = require(`./weapp-back-pages.js`)

const readFiles = (paths) => {
  return Promise.all(paths.map(async(path) => {
    const content = await readFile(require.resolve(path), 'utf-8')

    return UglifyJS.minify(content, {
      mangle: {
        safari10: true,
      },
    }).code
  }))
}

module.exports = class PageBackupPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('PageBackupPlugin', async(compilation) => {
      const { assets } = compilation

      const [template] = await readFiles(['./page-template/backpage.js'])
      const config = JSON.parse(assets['./app.json'].source())
      
      Object.entries(BACK_PAGES_MAP)
        .forEach(([backPath, realPath]) => {
          // 增加 app.json pages 配置转跳承载页
          config.pages.push(`${backPath}`)

          // 添加备份页面入口
          assets[`${backPath}.js`] = new ConcatSource(template.replace('routePath', `'/${realPath}'`))
          assets[`${backPath}.wxml`] = new ConcatSource('<view />')
        })

      // 重写 app.json
      assets['./app.json'] = new ConcatSource(JSON.stringify(config))
    })
  }
}

2.4 优化成效

这里回归到业务背景上面,这里进行分包优化主要还是建立在原本无分包架构下业务迭代快速导致主包超体积大小限制无法上传问题;性能问题反而是其次问题,因此这里主要还是关注技术优化后主包、分包的体积变化。

打包构建体积变化:

初步接入分包,拆分的页面可能不多,也是为了初期改造的影响面和稳定性的保证,因此这里节约的主体的包体积并不算多,拆分了 5 个页面和 8 个业务组件至各自的分包当中,其小程序的主包体积大约减少了 120 kb 左右的容量,能够正常的上传代码了,也算是取得比较不错的成绩,后续稳定了再进行大规模的进行业务上面的拆分和迁移对应的业务旧页面至分包当中。

访问性能提升:

因为这期作为接入分包的初步尝试,仅拆分了一部分的页面,主包的体积其实减少不了多少,因此其实访问的速度性能这方面并没有能够提升多少,根据腾讯提供的小程序性能分析上分析:

  • 启动性能:小程序启动总耗时大约提升了 5%
  • 运行性能页面切换耗时接近无变化(曲线变化率不高),这块还是比较遗憾的,但是还是保留到下一次的针对整体项目的技术性能优化上能有所突破。
posted @ 2023-05-15 09:19  这个夏天要冰凉  阅读(102)  评论(0编辑  收藏  举报