小程序开发框架解析

小程序开发有一个”行业特产”的痛点:同一个业务要在微信、支付宝、百度等多个平台上线,每个平台有自己的 DSL、自己的 API、自己的组件体系。如果每个平台写一套代码,维护成本直接乘以 N。

跨端框架试图解决这个问题——用一套 DSL(通常是 React 或 Vue 语法)写代码,编译或运行时转换为各平台的小程序代码。这篇文章从行业小程序的差异对比开始,然后拆解编译时(Rax compile)和运行时(Rax runtime + kbone)两种方案的原理。

行业小程序对比

产品及定位

  1. 微信:在微信内被便捷地获取和传播的连接⽤户与服务的⽅式,同时具有出⾊的使⽤体验;
  2. ⽀付宝:运⾏在⽀付宝客户端,可以被便捷地获取和传播,为终端⽤户提供更优的⽤户体验;
  3. 淘宝:服务移动开发者的平台,帮助开发者构建⾃⼰的业务阵地,并提供良好的⽤户体验;
  4. 百度:依托以百度App为代表的全域流量,通过百度AI开放式赋能,精准连接⽤户,业界⾸家开放⽣态,让开发者重回业务理解和创意赛道;

总结:

  1. ⼩程序的⽬标是万事万物皆可⼩程序;
  2. 超级APP的崛起为⼩程序提供⽣存⼟壤 -> 圈地运营;
  3. 云计算发展,Saas标准化服务输出,降低了品牌建站的成本-> FaaS;

官⽅⽂档

  1. 微信:微信公众平台-⼩程序—⽂档
  2. ⽀付宝:蚂蚁⾦服开发平台-⼩程序—⽂档
  3. 淘宝:淘宝开发者平台—⽂档
  4. 百度:智能⼩程序 — ⽂档

IDE

  1. 微信IDE
  2. ⽀付宝IDE
  3. 淘宝IDE
  4. 百度IDE

⽂件结构

  1. 微信⼩程序
1
2
3
4
5
6
7
8
9
10
## 微信⼩程序
- pages/ ⻚⾯⽬录
- page1/
- index.wxml // 必须,⻚⾯结构
- index.js // 必须,⻚⾯逻辑
- index.wxss // ⾮必须,⻚⾯样式表
- index.json // ⾮必须,⻚⾯配置
- app.js // 必须,⼩程序逻辑
- app.json // 必须,⼩程序公共配置
- app.wxss // ⾮必须,⼩程序公共样式表
  1. 支付宝小程序
1
2
3
4
5
6
7
8
9
10
## ⽀付宝⼩程序
- pages/
- page1/
- index.axml // 必须,⻚⾯结构
- index.js // 必须,⻚⾯逻辑
- index.acss // ⾮必须,⻚⾯样式表
- index.json // ⾮必须,⻚⾯配置
- app.json // 必须,⼩程序公共设置
- app.js // 必须,⼩程序逻辑
- app.acss // ⾮必须,⼩程序公共样式表
  1. ⼿淘⼩程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## ⼿淘⼩程序:基于轻框架
- src/
- components/ 组件⽬录,可以没有
- component1.html 组件⽂件
- component2.html
- pages/ ⻚⾯⽬录
- page1/
- index.html ⻚⾯⼊⼝
- page2/
- index.html
- index/
- index.html
- manifest.json 描述项⽬基本信息,包括⻚⾯、tabBar等
- app.js 程序级应⽤⼊⼝
- package.json 项⽬⼯程⽂件

## ⼿淘⼩程序:不基于轻框架完全同⽀付宝⼩程序
  1. 百度⼩程序
1
2
3
4
5
6
7
8
9
10
## 百度⼩程序
- pages/
- page1/
- index.swan // 模 板 ⽂ 件
- index.js // ⻚ ⾯ 逻 辑
- index.css // ⻚⾯样式
- app.js
- app.json // 配置⽂件
- app.css
- project.swan.json

⽣命周期

  1. 微信⼩程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

//index.js
Page({

// {Object}:⻚⾯的初始数据
data: {
text: "This is page data."
},

// {Function}:⽣命周期回调—监听⻚⾯加载
onLoad: function(options) {
// Do some initialize when page load.
},

// {Function}:⽣命周期回调—监听⻚⾯初次渲染完成
onReady: function() {
// Do something when page ready.
},

// {Function}:⽣命周期回调—监听⻚⾯显示
onShow: function() {
// Do something when page show.
},

// {Function}:⽣命周期回调—监听⻚⾯隐藏
onHide: function() {
// Do something when page hide.
},

// {Function}:⽣命周期回调—监听⻚⾯卸载
onUnload: function() {
// Do something when page close.
},

// {Function}:监听⽤户下拉动作
onPullDownRefresh: function() {
// Do something when pull down.
},

// {Function}:⻚⾯上拉触底事件的处理函数
onReachBottom: function() {
// Do something when page reach bottom.
},

// {Function}:⽤户点击右上⻆转发
onShareAppMessage: function () {
// return custom share data when user share
},

// {Function}:⻚⾯滚动触发事件的处理函数
onPageScroll: function() {
// Do something when page scroll
},

// {Function}:当前是 tab ⻚时,点击 tab 时触发
onTabItemTap(item) {
console.log(item.index)
console.log(item.pagePath)
console.log(item.text)
},
// Event handler 其他:开发者可以添加任意的函数或数据到 Object 参数中,在⻚⾯的函数中⽤ this 可以访问
viewTap: function() {
this.setData({
text: 'Set some data for updating view.'
}, function() {
// this is setData callback
})
},
customData: {
hi: 'MINA'
}
})

  1. ⽀付宝⼩程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

//index.js
Page({
data: {
title: "Alipay"
},
onLoad(query) {
// ⻚⾯加载
},
onReady() {
// ⻚⾯加载完成
},
onShow() {
// ⻚⾯显示
},
onHide() {
// ⻚⾯隐藏
},
onUnload() {
// ⻚⾯被关闭
},
// {Function}:点击标题触发
onTitleClick() {
// 标题被点击
},
onPullDownRefresh() {
// ⻚⾯被下拉
},
onReachBottom() {
// ⻚⾯被拉到底部
},
onShareAppMessage() {
// 返回⾃定义分享信息
},
viewTap() {
// 事件处理
this.setData({
text: 'Set data for updat.'
})
},

// 其他:开发者可以添加任意的函数或属性到 object 参数中,在⻚⾯的函数中可以⽤ this 来访问
go() {
// 带参数的跳转,从 page/index 的 onLoad 函数的 query 中读取 xx
my.navigateTo({url:'/page/index?xx=1'})
},
customData: {
hi: 'alipay'
}
})
  1. 淘宝⼩程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 不基于框架:跟⽀付宝⼩程序语法⼏乎完全⼀样
//index.js
Page({
data: {
title: "Alipay"
},
onLoad(query) {
// ⻚⾯加载
},
onReady() {
// ⻚⾯加载完成
},
onShow() {
// ⻚⾯显示
},
onHide() {
// ⻚⾯隐藏
},
onUnload() {
// ⻚⾯被关闭
},

viewTap() {
// 事件处理
this.setData({
text: 'Set data for updat.'
})
},
go() {
// 带参数的跳转,从 page/index 的 onLoad 函数的 query 中读取 xx
my.navigateTo('/page/index?xx=1')
},
customData: {
hi: 'alipay'
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 基于框架,类Rax框架,html、js、css在同⼀⽂件中,通过sfc2mp转为⽀付宝⼩程序
<template>
<view class="demo-page">
<text class="title">欢迎来到{{title}}</text>
<button :class="btn" @click="goto">开启未来</button>
</view>
</template>
<style>
.demo-page {
flex-direction: column;
justify-content: center;
align-items: center;
}

.title {
font-size: 40px;
text-align: center;
}

.btn {
width: 550px;
height: 86px;
margin-top: 75px;
border-radius: 43px;
background-color: #09ba07;
font-size: 30px;
color: #ffffff;
}

.clickedBtn {
width: 550px;
height: 86px;
margin-top: 75px;
border-radius: 43px;
background-color: #09ba07;
font-size: 30px;
color: red;
}
</style>
<script>
import page from '@core/page';
export default {
data: {
title: '示例⻚⾯',
btn: 'btn',
},
methods: {
goto () {
this.btn = 'clickedBtn';
my.navigateTo({
url: 'test?id=1'
});
}
},
beforeCreate() {
page.on('show', () => {});
page.on('hide', () => {});
},
created (){},
updated () {},
destroyed () {},
}
</script>

  1. 百度⼩程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Page({
data: {
name: 'swan'
},
onLoad: function () {

},
onReady: function() {
// Do something when page ready.
},
onShow: function() {
// Do something when page show.
},
onHide: function() {
// Do something when page hide.
},
onUnload: function() {
// Do something when page close.
},
onPullDownRefresh: function() {
// Do something when pull down.
},
onReachBottom: function() {
// Do something when page reach bottom.
},
onShareAppMessage: function () {
// return custom share data when user share.
},
// {Function}:错误监听函数
onError: function (e) {
// return error info : e
},

// 其他:开发者可以添加任意的函数或数据到 object 参数中,在⻚⾯的函数中⽤ this 可以访问
any: function () {
// return custom data
}
});

视图层

数据绑定
  1. 微信:指令以wx:开头
1
<view wx:if="{{condition}}"> </view>
  1. ⽀付宝:指令以a:开头
1
2
3
<view a:if="{{view == 'WEBVIEW'}}"> WEBVIEW </view>
<view a:elif="{{view == 'APP'}}"> APP </view>
<view a:else> alipay </view>
  1. 淘宝:完全同⽀付宝
  2. 百度
1
2
3
4
5
6
7
8
9
10
11
Page({
data: {
person: {name: 'Lebron James', pos: 'SF', age: 33},
teams: ['Cleveland Cavaliers', 'Miami Heat', 'Los Angeles Lakers'],
tag: 'basketball',
flag: true
}
});

<view s-if="flag"></view> // 1.`s-` 开头 2.`flag`没有{{}}
<template is="team-card" data="{{ {teams} }}" /> // {{ {items} }}
样式⽀持

全部⽀持rpx逻辑单位

组件

⽬前以微信⼩程序⽀持最为完善

  1. 微信⼩程序
    • picker-view:滚动选择器
    • functional-page-navigator:跳转⾄插件功能⻚
    • live-push:实时⾳视频录制
    • ad:banner⼴告
    • official-account:公众号关注组件
  2. ⽀付宝⼩程序
    • 缺少movable-area
    • 缺少cover-view
    • 缺少rich-text
    • 缺少audio
    • 缺少video
    • 缺少camera
    • 缺少live-player
    • 缺少live-pusher
    • 缺少ad
    • 缺少open-data
  3. 淘宝
    • 缺少movable-area
    • 缺少cover-view
    • 缺少rich-text
    • 缺少camera
    • 缺少live-player
    • 缺少live-pusher
    • 缺少canvas
    • 缺少web-view
    • 缺少ad
    • 缺少open-data
    • 缺少offical-account
  4. 百度
    • animation-view:动画组件
    • 缺少live-pusher

总结

⼩程序底层⽅案都是⼀致的,只不过在⽀持程度等有所不同。

以⽀付宝⼩程序为例:

  1. ⼩程序分别运⾏在 worker(JSEngine) 以及 render 渲染层中, render 可以有多个, worker 只有⼀个,⽅便 app 数据在⻚⾯间的共享和交互;(渲染层 & 逻辑层)
  2. worker 运⾏⼩程序的逻辑处理代码,包括事件处理,api 调⽤以及框架的⽣命周期管理;(逻辑层功能)
  3. render 运⾏⼩程序的渲染代码,主要包括模版/样式和框架的跨终端的 js 组件或 native 组件,获取逻辑层的数据进⾏渲染;(渲染层功能)
  4. worker 和所有的 render 都建⽴连接,将需要渲染的数据传递给对应的 render 进⾏渲染,worker 也会将 api 调⽤转给 native SDK 处理;(Hybrid通信)
  5. render 则将组件的触发事件送到对应的 worker 处理,同时接受 worker 的 setData 调⽤ React 重新渲染。 render 可以看作⼀个⽆状态的渲染终端,状态统统保留在 app 级别的 worker ⾥⾯;(渲染层&逻辑层交互)

小程序跨端框架介绍

原⽣⼩程序开发

原⽣⼩程序适⽤于:需求明确只在指定⼩程序⼀端进⾏,保证最⼤程度的避免多端框架兼容带来的莫名 bug。

多端小程序开发主要有两条技术路线——编译时和运行时。这里只讨论 React 语法的跨端框架。

编译时方案

把开发者写的类 React 代码解析成 AST,通过语法分析转换为可运行的小程序代码。代表:Taro 1/2、Nanachi、Rax(compile mode)。

以 Rax 编译时为例,链路分为五个模块:

  1. CLI:入口,读取用户代码,传入 webpack 执行依赖分析,提供 watchbuild 两个指令
  2. Loader(jsx2mp-loader):按文件角色分发——app-loader 处理 app.js 和配置抹平、page-loader 处理页面组件、component-loader 处理组件、script-loader 处理依赖路径
  3. Compiler(jsx-compiler):AST 转换核心——词法分析 → 语法分析 → 代码生成。输入类 React 代码,输出小程序可执行代码 + 模板
  4. Runtime(jsx2mp-runtime):运行时垫片,桥接小程序 Page/Component 和 Rax API
  5. Universal:多端统一的组件和 API 基础服务
CLI 如何解决小程序入口问题

原生小程序中入口文件 app.js 并没有声明依赖——pages 是在 app.json 中注册的。Rax 采用统一的工程目录,app.json 中通过 routes 声明路由和页面:

1
2
3
4
5
6
7
├── src
│ ├── app.js # 应用入口
│ ├── app.json # 路由配置
│ ├── components # 公共组件
│ └── pages # 页面
│ └── Home
│ └── index.jsx

CLI 读取 routes 中引用的所有 pages 文件和 app.js 作为 entry,webpack 依次遍历依赖,交由对应 loader 处理。

Loader 体系
  • app-loader:处理 app.js,抹平支付宝/微信两端的 window 配置差异
  • page-loader:处理页面组件,解析引用的组件写入 usingComponents,并将这些组件加入依赖分析链
  • component-loader:处理组件,同样解析子组件依赖并写入 usingComponents
  • script-loader:处理 npm 包路径,搜集依赖并进行 babel 编译;对于第三方原生小程序 npm 包,读取同目录 json 中的 usingComponents 加入依赖链
  • file-loader:处理图片等静态资源
Compiler:jsx-compiler

编译流程:词法分析(tokenizing)→ 语法分析(parsing)→ 代码生成(generate)。

输入一段类 React 代码,输出包含 astcodetemplateconfigstyleusingComponents 等完整的小程序产物。

运行时方案

代表:Remax、Taro 3、Rax(runtime mode)。

起点的关键技术是 kbone——在小程序的逻辑层(appService)适配一套轻量 DOM/BOM API,在内存中维护一棵 DOM tree,每当你通过前端框架操作 DOM 时,实际修改的是这棵内存中的树。每次操作后(有节流)把 DOM tree 同步到渲染层(webView),通过 WXML 自定义组件递归渲染。

关键技巧:小程序自定义组件支持递归引用自身。利用这个特性,可以将任意层级、任意节点数量的 DOM 树映射为一棵自定义组件树。

Rax 的运行时方案类似 kbone:有一个 driver-miniapp 作为小程序 Driver,miniapp-render 库模拟 DOM/BOM API,事件系统基于 EventTarget 基类实现完整的事件派发机制——逻辑层 DOM 节点继承 EventTarget,通过唯一 nodeId 收集绑定事件,视图层模板上的每个组件监听所有可触发事件,通过 event.currentTarget.dataset.nodeId 定位目标节点。

框架选择:Rax vs Taro

API 设计

每个平台总有一些无法抹平的差异。好的框架设计应该让平台特有属性不侵入基础框架本身。

对比 Taro 和 Rax 的页面组件写法:

Taro:内置了 componentDidShowcomponentDidHide 等小程序特有生命周期,以及 config 静态属性设置页面标题。

Rax:没有这些非 React 标准的 API,改用 addNativeEventListener / removeNativeEventListener 等更接近 W3C 标准的接口。页面配置不在组件实例上,而在 app.json 中。

从”保持 React 纯净性”的角度,Rax 的设计更克制——它不往组件上挂不属于 React 的属性和生命周期。

多端组件协议

  • Taro:组件统一在项目中编译产出为小程序代码
  • Rax:构建产物符合小程序语法,可以直接在原声小程序项目中使用,支持渐进式迁移

构建体系

Rax 基于 build-scripts 和 webpack-chain 提供灵活的插件体系。编译时方案通过 webpack loader 调用 jsx-compiler 做 AST 转换,运行时方案直接复用 Web 端的 webpack 配置。

主流框架对比


选择编译时还是运行时,本质上是一个”性能 vs 灵活性”的权衡。编译时性能更好但语法受限,运行时几乎无语法限制但有性能开销。Rax 选择了”双引擎”——默认运行时,局部高性能场景(如长列表)使用编译时组件,试图在两个极端之间找到平衡。