2
头图

一、前言

提起微信小程序,相信所有人都不陌生,下面这个典型使用场景你一定经历过:

餐馆落座——微信扫桌角小程序码——使用微信小程序点餐🍔

微信小程序(下文简称:小程序)作为一种在微信平台内运行的应用程序,用户无需前往应用商店下载安装包即可使用,可以在微信内被便捷地获取和传播,2017年一经推出便迅速成为热门技术关键词,得物也随即发布了得物App小程序,欢迎扫码体验:

七年时间过去了,小程序的周边配套已经十分成熟,微信官方对小程序生态进行了很多迭代——微信开发者工具从难用到可用、小程序API经历了多轮简化和拓展、数据分析能力逐渐完善,同时开源社区也贡献了非常多的开发框架、实践记录等。

得物内部也形成了自己的小程序开发生态,本文主要介绍一些得物App小程序版本迭代中形成的开发指南、实践经验等,给小程序新手开发者一些实践经验,如果你已经是个小程序开发高手了,那么欢迎交流指正我们做得好的/不好的部分。

事实上,“小程序”这种技术架构并非微信首创,但无疑微信小程序是目前生态建设最完整、用户量最大的小程序生态。

二、小程序运行机制

先简单介绍一下小程序的运行机制和架构设计。

小程序的架构设计初衷是“快”,同时又需要对展示内容做严格的安全管控,所以采用了Webview结合Native的Hybrid技术,设计了双线程模型架构:

渲染层:小程序界面由Webview渲染,一部分较复杂组件用客户端原生渲染,以提供更好的性能。

逻辑层:一个JavaScript沙箱环境(利用原生客户端系统的JavaScript的解释引擎创建),只执行有关小程序业务逻辑的代码,不能访问任何浏览器相关接口,以避免恶意代码运行:在运行时访问DOM树获取敏感数据、跳转到其他在线网页等。

上述渲染层和逻辑层只是上层的抽象概念,在不同运行环境下有各自技术实现,如下表:

小程序本质上是Web和Native的结合——成熟的Web渲染技术负责界面渲染、Native提供客户端原生API补充Web的短板;由于Web基于单线程模型且开放灵活,因此微信隔离了渲染层与逻辑层,以此作为对内容安全管控的有效控制手段。

目前小程序在渲染层上已经有了基于"Skyline渲染线程"的新型架构,减少了双线程架构之间的通信开销和内存占用,理论上拥有更好的性能,需要单独开启,可自行查阅相关文档。

三、开发阶段

从小程序的运行机制可以得知:小程序在开发、发布、运行等每个环节都依赖于微信自身的生态而非传统Web,所以有很多特有的设计。先介绍一些基础概念和知识:

用户端运行的小程序资源文件需要先上传到微信侧,然后才能推送到用户端,微信限制了资源体积大小:

  • 整个小程序所有分包大小不超过20M(开通虚拟支付后的小游戏不超过30M);
  • 单个分包/主包大小不能超过2M。

主包:小程序默认启动页面/TabBar页面的构建产物,以及一些所有分包都需用到公共资源/JS脚本;

分包:为了提高初始化加载速度,开发者可以划分以页面为维度的分包,分包只包含一些子级页面的代码。

跨平台框架选型

如果你的项目确实只需要运行在微信小程序平台,选用微信小程序原生开发方式或许是一个更好的选择——能随时接入微信小程序的最新特性和API,缺点就是不够灵活,如果日后还想要在别的平台开展相同的业务,需要额外开发工作。

更普遍的情况是,大多数开发者都选用跨端框架去开发小程序,毕竟程序员都喜欢 Write once, run anywhere,所以支持小程序端的跨平台框架在小程序刚推出的前几年如雨后春笋般层出不穷。

当然,截止到2024这个时间节点,其中很多框架都已经停止维护(比如remax、mpvue、wepy...),目前比较推荐的跨平台框架有:uni-app、Taro,两者异同比较:

一句话总结:喜欢写Vue的话用uni-app,喜欢写React用Taro,这两者在小程序平台上的表现来说都没有绝对的优势和劣势,社区活跃度都很高,时至今日都在持续更新维护中。

目前在得物内部,uni-app和Taro都有使用。

包体积优化

主包/分包配置

从前文中可以得知,微信限制了单个分包/主包大小不能超过2M,因此需要规划合理的目录,以uni-app的开发目录做示例:

├── ...
├── bin // 构建命令
├── loaders
├── src
│   ├── assets // 静态资源
│   ├── components // 通用组件
│   ├── pages // 主包目录
│   ├── product // product分包目录
│   ├── order // order分包目录
│   ├── ...
│   ├── sdk // 外部SDK
│   ├── store // 全局状态
│   ├── style // 样式文件
│   ├── utils // 通用方法
│   └── wxcomponents // 小程序原生组件
├── ...

小程序根目录下的app.json文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多tab等。(选用不同的跨平台框架会有不同的配置方式和语法,但是殊途同归,编译到小程序平台的配置文件都会遵从小程序规范,下文将用小程序原生语法做说明。)

需要额外注意的是,考虑到小程序有主包和分包的设计,将关联性较强的页面放在同一目录里可以更合理地配置分包,示例如下

// app.json
{
  // 主包页面
  "pages": [
    "pages/index/index",
    "pages/xxxxx/index",
  ],
  // 分包配置页面
  "subPackages": [
    {
      "root": "product",
      "pages": [
        "xxxxx/index",
      ]
    },
  ],
  "window": {
    // 全局相关配置
  },
  "tabBar": {
    // tabBar相关配置
  }
}

即便是合理地配置了分包且做好了目录规划,也依然可能超过2M的体积限制,这个时候我们的优化手段跟Web项目基本相同,这里不再赘述,主要手段:静态资源从cdn加载、代码压缩、依赖优化分析等。

分仓开发

小程序的构建发布机制决定了所有页面都要一起打包、上传,在开发阶段也是如此。

我们曾经真实经历的开发流程:

  1. 打开IDE,打开项目代码;
  2. 运行微信平台构建命令:npm run mp-weixin;
  3. 运行字节平台构建命令:npm run mp-bytedance;
  4. 运行QQ平台构建命令:npm run mp-qq;
  5. 运行Web构建命令:npm run h5;
  6. 打开微信开发者工具、字节开发者工具、QQ开发者工具、Chrome浏览器,在不同工具中预览页面。

上古时期的噩梦

我们的小程序业务代码已经非常庞大,面对这样复杂的多线程开发流程,开发阶段的编译构建给研发电脑极大的内存压力,卡顿属实家常便饭。

为了能更高效地进行开发工作,我们利用Webpack的虚拟模块能力,结合私有npm包管理,将业务代码收敛进了各业务域的仓库内,做到了:

将一个完整的小程序项目,分割成多个能独立进行开发、部署的子应用(基于独立git仓库),同时又可以将所有子应用作为一个完整小程序应用打包发布。

时至今日,分仓开发在得物成为了“过去式”——由于后续的一波研发电脑大升级(唯快不破M2 Pro+力大砖飞32GB RAM),同时得物在QQ平台&字节平台的小程序也停止了迭代。只运行Web构建和小程序构建,并不会卡顿影响开发效率,所以目前已经不再需要分仓开发模式。

限于本文主题和篇幅,这里我们不再过多赘述分仓开发相关实现,有兴趣的话记得关注我们,后续可以整理一波技术文档以供方案参考~

提效工具

在Web中切换API环境有天然的域名隔离,可以直接访问不同域名下部署的站点。而在小程序中,像原生App一样,页面路径在用户端是无感且不能修改的,而且小程序没有安装包的概念,同一时间只存在一个体验版。所以为了更方便去调试和测试,可以做一些提效小工具。

API环境切换功能

在TabBar页面可以开发一个环境切换功能,例如:

  <view v-if="!IS_PRODUCTION" class="changeServiceEnv">
    <view class="changeTitle">环境切换</view>
    <radio-group @change="radioChange">
      <radio
        v-for="item in ENV_Array"
        :key="item"
        :value="item"
        :checked="item === SERVICE_ENV"
        class="radio-info"
      >
        {{ item }}
      </radio>
    </radio-group>
  </view>
radioChange(e) {
  let { value } = e.detail
  this.$store.commit('SET_SERVICE_ENV', value)
  uni.showToast({ title: `当前环境是${this.SERVICE_ENV}` })
}

这样就可以做到在单一体验版中自由切换API环境,验证不同环境下的数据展现和业务逻辑了。

DEV面板

在测试场景下,尝试访问路径较深的页面会比较耗时繁琐,和很多原生App的测试包做法一样,可以开发一个全局的DEV面板。

但是受限于小程序的架构设计:小程序每跳转到一个页面,实际上都是打开一个新的Webview,在这种设计架构下,虽然可以注册全局组件,但是如果想要在每个页面都渲染出全局组件,还是需要在每个页面都写一遍全局组件的代码,这样并不cool,我们探索出来的方案:

在非生产环境的Webpack配置中使用注入全局组件的loader:

  • 分析小程序页面配置,计算并得到路由表,这一步具体怎么实现跟选用的框架相关;
  • loader的上下文环境this.resourcePath指向真实的文件路径,将它和上一步拿到路由表进行匹配,如果未命中,则不做任何操作;
  • 分析命中路由表的文件,从中找到模版代码,紧随第一个标签,在其后方插入全局组件代码:

    • 这一步可以用正则,也可以用AST;
    • 对于我们的项目来说,在.vue文件的<template/>标签中查找第一个标签用正则很方便,所以我们直接正则替换。

关键代码:

// 全局loader主要代码
function modifyTemplateString(inputString, customTag) {
  // 查找 <template> 标签内容
  let templateStart
  if (inputString.indexOf('<template>') > -1) {
    templateStart = inputString.indexOf('<template>') + '<template>'.length
  }
  const templateEnd = inputString.indexOf('</template>')


  const templateContent = inputString.substring(templateStart, templateEnd).trim()
  let modifiedTemplateContent = ''
  // 在 <template> 第一个元素开头插入自定义标签
  modifiedTemplateContent = templateContent.replace(/(^\s*<\w+(?:-\w+)*\b(?:\s+(?:"[^"]*"|'[^']*'|[^>"])+)?\s*>)/, `$1\n${customTag}`)
  // 替换原始字符串中的 <template> 和 <script> 内容
  return inputString
    .replace(templateContent, () => modifiedTemplateContent)
}


module.exports = function loader(source) {
  const relPath = path.relative(path.join(__dirname, '../', './src'), this.resourcePath)
  // <devPanel>是已经在全局中注册过的全局组件
  return modifyTemplateString(source, '<devPanel></devPanel>')
}

这样就能自动在所有页面渲染出全局DEV按钮组件了,可以更加方便地进行各种快捷操作,实现效果如下

自动化发布

小程序开发完成之后,微信开发者工具提供了上传入口,构建产物上传之后,需要到小程序后台再设置为体验版,如下图所示:

每个开发者都这样操作会造成后台系统上有很多的开发者版本,而且不同系统环境下的构建产物可能不一样,这样的工作流低效且不稳定,所以通过专门的机器执行构建,并将构建产物上传到微信服务器是更可靠的选择。

目前我们的发布工作流如下

  1. 开发人员将代码合入指定分支后,会触发发布平台的持续部署任务,打包机拉取代码后执行命令:
// 打包机执行的命令
#!/usr/bin/env node
const shell = require('shelljs')
const signale = require('signale')
const { Signale } = signale
const ci = require('miniprogram-ci')


async function main() {
  const interactive = new Signale({ interactive: true })
  interactive.pending(task)
  // 执行构建命令
  const result = shell.exec(`npm run build`)
  if (result.code !== 0) {
    // 如果构建失败了 退出
    interactive.error(task)
    process.exit(1)
  } else {
    const project = new ci.Project({
      type: 'miniProgram',
      projectPath: '/dist/dev/mp-weixin',
      ignores: ['node_modules/**/*'],
    })
    // 将构建产物上传至微信服务器
    ci.upload({
      project,
    }).then(res => {
      console.log('上传成功')
      // 通过IM通知相关测试/研发验证体验版小程序
    }).catch(err => {
      console.log('上传失败')
      interactive.error(task)
      process.exit(1)
    })
  }
}
main()
  1. 打包并上传成功后,通过IM通知相关测试/研发验证体验版小程序:

miniprogram-ci是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。

使用前需要使用小程序管理员身份访问"微信公众平台-开发-开发设置"后下载代码上传密钥,并配置IP白名单,才能进行上传、预览操作。

miniprogram-ci从1.0.28开始支持第三方平台开发的上传和预览,调用方式与普通开发模式无异。

采用自动化发布的优势

  • 节省手动操作的时间和精力;
  • 集成到CI/CD流程:可以集成到持续集成/持续部署(CI/CD)流程中,实现自动化构建和部署;
  • 可编程性:可以通过脚本编程控制上传代码的过程,实现定制化的部署流程。

四、发布后的数据分析

想要对小程序产生的数据进行精确化分析,除了自建监控和埋点,还可以从微信官方提供的“We分析”数据分析平台入手,我们从研发比较关心的两点做介绍:稳定性监控、JS错误分析。

稳定性监控

  • “性能数据”面板,可以看到当前小程序性能综合表现,也可以看到更详细的“白屏分析”、“网络请求分析”等:

  • “实时日志”面板,可以根据关键词查找小程序运行中产生的日志,以此排查业务逻辑等:

JS错误分析

“JS分析”面板,这里可以通过版本、时间等多个维度筛选微信记录的JS错误,通过错误堆栈信息可以定位到代码中的bug进行修复。

同样可以配置微信告警群,群里会定时push错误量的趋势,能够让开发者及时观测到新发布版本的代码bug。

五、总结思考

在小程序发布之初,很多开发者都不看好小程序。从技术视角来看,小程序确实带着非常多的debuff:闭源、充满限制、原生IDE丑、bug多。然而,如今看来,不论开发者是否接受这种与Web精神相悖的技术理念,也不妨碍它已经成为一种新的技术标准——目前市场上很多App都已建设了自己的小程序生态,如支付宝小程序、飞书小程序等,它们无一例外都参考了微信的技术架构和API设计。

尽管小程序在技术上存在诸多限制和缺陷,但其所带来的便利和商业机会无疑是巨大的。未来,随着技术的不断进步和生态的逐渐完善,小程序有望成为移动应用开发的重要范式之一,为用户和开发者创造更多价值。

同时,小程序的成功也提醒我们开发者,要不断关注和学习新技术,不断调整自己的思维和方法,以适应技术和市场的变化。

引用:

https://developers.weixin.qq.com/miniprogram/introduction/
https://segmentfault.com/a/1190000019131399
https://juejin.cn/post/7087041847700226062
https://developers.weixin.qq.com/ebook?action=get_post_info&d...
https://developers.weixin.qq.com/miniprogram/dev/framework/ru...
https://uniapp.dcloud.net.cn/
https://docs.taro.zone/docs/
https://www.npmjs.com/package/miniprogram-ci

*文/ Zyqy 航飞

本文属得物技术原创,更多精彩文章请看:得物技术

未经得物技术许可严禁转载,否则依法追究法律责任!


得物技术
846 声望1.5k 粉丝