小程序生态:原生与跨端
小程序是运行在「超级 App」宿主里的轻量应用:入口在微信 / 支付宝 / 抖音内,不需要安装,但代码与能力都受宿主平台约束。本页梳理小程序生态的两条技术路线——写原生小程序、或用 uni-app / Taro 做跨端编译,以及登录、发布与合规这些绕不开的工程问题。
小程序是什么:被托管的应用
小程序不是普通网页,也不是普通 App,它的特殊之处在于宿主托管:
| 维度 | 传统 H5 | 原生 App | 小程序 |
|---|---|---|---|
| 运行载体 | 浏览器 | 操作系统 | 宿主 App(微信 / 支付宝 / 抖音等)内的运行时 |
| 获取方式 | URL 打开 | 应用商店下载 | 扫码 / 搜索 / 聊天分享卡片 |
| 安装 | 无 | 需下载安装 | 无需安装(按需下载代码包) |
| 能力来源 | 浏览器 API | 系统 SDK | 宿主通过 JSAPI 桥暴露的能力(受限) |
| 发布 | 无审核(自有域) | 应用商店审核 | 平台审核 + 平台规范约束 |
| 代码分发 | 服务器 | 应用包 | 平台托管的分包代码(有体积上限) |
关键推论:
- 能力是平台给多少用多少:支付、登录、分享、订阅消息都走宿主封装的 API,且平台可以随时收紧(历史上头像昵称、手机号、隐私接口都改过规则);
- 生态价值大于技术价值:小程序通常不是技术选型,而是「要出现在微信 / 支付宝生态里获客」的业务决策,因此遵守平台规范是硬约束;
- 与原生 App 是互补关系:低频、获客、裂变场景适合小程序;高频、重体验、深度系统能力场景仍是 App 主战场。
选型坐标可对照技术全景与选型中的客户端版图;「是否做小程序」的判断应来自产品渠道而非技术偏好。
原生小程序运行模型:双线程 + 数据驱动
以微信为例(其余平台同构异名),小程序不是「一个 WebView 里的页面」:
- 逻辑层:运行在 JS 引擎(无
window/document),执行业务逻辑、管理页面数据; - 视图层:渲染 WXML/WXSS,负责显示;
- 两层之间用
setData把逻辑层数据序列化后传到视图层做 diff 更新,类似一个带桥的响应式系统。
这带来三条铁律:
- 页面没有 DOM:不能
getElementById、不能直接操作节点,一切更新靠setData或组件的属性绑定; setData有成本:数据量大、频率高会明显卡顿,甚至超出单次传输上限(约 1MB,超限直接失败);- 天生单向:视图层事件只能通过事件回调把值带回逻辑层。
工程骨架
project/
├── app.js # 全局逻辑:App() 注册、onLaunch 等
├── app.json # 全局配置:pages、window、tabBar、分包等
├── app.wxss # 全局样式
└── pages/
└── index/
├── index.js # Page() 注册:data / 生命周期 / 事件
├── index.json # 页面配置(页面级 window 与组件声明)
├── index.wxml # 结构:受控标签集
└── index.wxss # 样式:支持 rpx 等单位app.json 中 pages 数组的第一项是首页,每个页面必须登记:
{
"pages": ["pages/index/index", "pages/detail/detail"],
"window": {
"navigationBarTitleText": "示例",
"navigationBarBackgroundColor": "#ffffff"
},
"tabBar": {
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/detail/detail", "text": "详情" }
]
}
}页面生命周期
| 生命周期 | 时机 | 典型用途 |
|---|---|---|
onLoad(options) | 页面创建(带 query 参数) | 读取页面参数、初始化 |
onShow | 页面每次显示 | 刷新数据、埋点 |
onReady | 首次渲染完成 | 初始化 canvas / 组件 |
onHide | 页面隐藏 | 暂停耗时任务 |
onUnload | 页面销毁 | 释放资源 |
onPullDownRefresh / onReachBottom | 下拉刷新 / 触底 | 刷新、分页加载 |
组件(自定义组件)生命周期走 lifetimes(attached / ready / detached 等),逻辑不同:页面强调「进出一屏」,组件强调「挂载到视图树」。
WXML / WXSS 要点
- 标签受控:常用
view/text/image/button/input/scroll-view/swiper/rich-text等,不是 HTML 全集(没有div/span); - 尺寸单位
rpx:设计稿宽度对应 750rpx,自动按屏宽缩放,免去多机型换算; - 模板逻辑能力弱:想放个「计算后的值」要提前算好放进
data,或在 WXS(一种受控脚本语言)里写,不能在 WXML 里自由执行 JS; - 条件/列表渲染:
wx:if/wx:for(注意wx:key别漏,否则列表复用会出状态错乱)。
Page({
data: { list: [], loading: false },
onLoad() {
this.fetchList();
},
fetchList() {
this.setData({ loading: true });
// request 来自封装过的 wx.request(注意合法域名配置)
request({ url: '/api/list' }).then((res) => {
// 局部更新:路径式 setData,避免整页大对象传输
this.setData({ list: res.data, loading: false });
});
},
onPullDownRefresh() {
this.fetchList();
wx.stopPullDownRefresh();
}
});<view wx:if="{{list.length === 0 && !loading}}" class="empty">暂无数据</view>
<view wx:for="{{list}}" wx:key="id" class="row" bindtap="onTapItem" data-id="{{item.id}}">
<text>{{item.title}}</text>
</view>上面 WXML 里的双花括号是小程序模板语法本身,与本站 VitePress 无关,属正常示例。
分包、体积与性能
微信等平台对代码包有严格上限(历史上主包约 2MB、总包约 20MB,近年已逐步放宽,具体数值以各平台最新官方说明为准),超限无法上传。工程对策:
- 分包:
subpackages把低频页面(详情、活动、订单列表等)拆到独立分包,随用随下载,并支持「分包预下载」; - 主包瘦身:公共库外置、图片走 CDN 不塞包、大组件按需注册;
- 首屏优化:首屏只做必要请求并合并、关键数据预取(
wx.setStorage缓存上次结果先渲染再刷新)、骨架屏; setData纪律:用路径更新局部字段、避免在滚动/高频回调里整页setData、避免序列化不了的值(如循环引用、Date不经处理直接传)。
跨端框架:uni-app 与 Taro
「每个超级 App 写一套」成本太高,于是有了编译型跨端框架:用熟悉的 Vue / React 写业务,编译时转换输出成各目标端的小程序代码。
| 维度 | uni-app | Taro |
|---|---|---|
| 语法 | Vue(Vue 3 为主) | React(JSX + Hooks) |
| 目标端 | 微信 / 支付宝 / 抖音等 + H5 + App | 微信 / 支付宝 / 字节等 + H5(可选 RN) |
| 工程 | HBuilderX 或 Vite CLI,pages.json 声明页面 | CLI 脚手架,页面即路由(pages 配置) |
| 风格 | 类 Vue 生态(vuex / pinia、uni-ui) | 类 React 生态(zustand 等、Taro UI) |
| 多端差异处理 | 条件编译注释 | 端能力判断 + 平台配置文件 |
跨端框架的共同思路是「编译时转译 + 运行时抹平」:你写的是 Vue/React 组件,构建后产出目标端可运行的原生小程序代码。要接受的事实:
- 编译产物仍是原生小程序,性能下限由宿主决定,框架不改变「setData 驱动」的本质;
- 端差异永远抹不平:支付、登录、隐私授权等宿主能力在两端往往同名不同义,仍需条件编译或能力封装层逐端处理;
- 升级节奏跟着框架走:宿主平台更新 API 后,要等框架跟进才能用,框架版本的升级计划要纳入项目预算。
选型信号:
- 只做微信且团队就是小程序出身 → 原生写,少一层编译心智;
- 团队是 Vue / 前端出身、要同时覆盖多端甚至 H5 → uni-app;
- 团队是 React 栈、想在端上复用 JSX 心智与 npm 生态 → Taro。
若目标是 iOS + Android 双原生 App 而非超级 App 生态,则应选 Flutter / React Native / KMP(见跨端框架与Kotlin Multiplatform),不要用小程序跨端框架去碰 App。
登录、用户与合规红线
小程序登录闭环必须后端参与(典型流程,以微信为例):
- 前端
wx.login()拿到临时凭证code; - 前端把
code交给自己的后端; - 后端用
appid + secret + code调宿主接口(如auth.code2Session)换取openid/session_key,并生成自己的会话(自签 token 或 session 存储)返回前端; - 后续请求带自建会话,后端据此识别用户。
红线:
secret只能存在于后端,绝不能打进前端代码或传给前端;openid、session_key不下发前端当「登录态」,防止被冒用或数据泄露;- 用户头像昵称、手机号等敏感信息走平台合规组件 + 后端换取(手机号现已改为「快速验证组件返回 code → 后端换号码」),前端不再直接拿明文;
- 涉及用户信息的接口要申报平台「隐私保护指引」,隐私接口要调
wx.requirePrivacyAuthorize类能力; - 域名白名单:正式环境
wx.request等只能访问已配置并校验的 HTTPS 合法域名(需 ICP 备案)。
支付与开放能力(要点)
- 支付:后端用平台支付 API 下单拿到
prepay_id并做签名,前端wx.requestPayment拉起收银台;支付回调必须由后端接收并验签,不能只信前端通知(对应后端鉴权认证与回调验签的通用纪律); - 订阅消息:
wx.requestSubscribeMessage引导用户授权后,可发一次性/长期订阅消息做触达(如订单状态、活动提醒); - 分享:实现
onShareAppMessage/onShareTimeline才有转发入口;分享带参要能定位页面(path带 query),配合onLoad读参; - 内容合规:虚拟支付、金融、医疗等类目有额外资质与平台规范,审核前要逐条对照当前类目要求。
提审与发布
发布链路与 App 不同——没有「灰度大版本商店审核外」的自主权:
- 开发者工具上传代码包(可设「体验版」给内部/测试);
- 管理后台提交审核(填类目、提供测试账号、隐私说明等);
- 审核通过后发布;支持少量灰度(按比例 / 按白名单);
- 发布后发现问题:先功能开关/后端下线止血,再发新版本,别指望「下架 App」式操作;
- 留意基础库版本:用了新 API 要声明最低基础库版本,并在低版本设备上做降级或提示升级微信。
常见坑速查
| 坑 | 现象 | 对策 |
|---|---|---|
在逻辑层写 window / document | 直接报错或行为诡异 | 记住没有 DOM;操作 DOM 的场景该用 WebView 承载 |
WXML 写 div / span | 标签不生效、白屏 | 用受控标签 view / text 等 |
整页大对象高频 setData | 页面明显卡顿 / 传输超限 | 路径式更新、节流、只传变化字段 |
忘写 wx:key | 列表复用错乱、状态串 | 遍历绑定稳定唯一键 |
忘了 onShareAppMessage | 用户无法转发 | 需要分享就实现,且处理好分享参数回读 |
把 code/openid 当前端登录态 | 可被冒用、违反隐私红线 | 会话必须由自己后端签发 |
secret 进前端 | 凭证泄露事故 | 只存后端,环境变量管理 |
| 正式环境域名未加白/未备案 | 请求全部失败(开发工具正常、真机失败) | 提前配合法域名 + HTTPS + 备案 |
| 包超上限 | 上传失败 | 分包、CDN 化静态资源、按需引入 |
| 只在开发者工具调试 | 真机才暴露授权、渲染差异 | 高频用真机预览 + 体验版回归 |
| 用框架但不设升级预算 | 平台 API 更新后被迫返工 | 每年安排框架大版本升级 |
检查清单
- [ ] 明确做哪些宿主端,并逐条核对平台能力与规范差异;
- [ ] 决定原生 vs 跨端框架(uni-app / Taro),团队技术栈与端诉求匹配;
- [ ] 工程已按「分包 + 静态资源外置」规划,主包体积有预算;
- [ ]
setData纪律落地(路径更新、高频回调不整页传); - [ ] 登录闭环:
code → 后端 → 自建会话,secret与openid不下发前端; - [ ] 支付回调由后端接收验签,不信任前端通知;
- [ ] 隐私指引、敏感接口授权、域名白名单在生产前配好;
- [ ] 提审材料(类目、测试账号、隐私说明)齐全,有灰度与止血预案。
相关链接:客户端开发索引 · 技术全景与选型 · Flutter · React Native · 桌面应用 · 后端配合见鉴权认证。