小程序开发框架解析

课程目标

  1. ⼩程序原⽣开发对⽐,了解⾏业⼩程序开发⽀撑现状;
  2. 掌握多端⼩程序开发框架,配置开发环境;

知识要点

⾏业⼩程序对⽐

产品及定位

  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 // ⾮必须,⼩程序公共样式表
  2. 支付宝小程序
    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 // ⾮必须,⼩程序公共样式表
  3. ⼿淘⼩程序
    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 项⽬⼯程⽂件

    ## ⼿淘⼩程序:不基于轻框架完全同⽀付宝⼩程序
  4. 百度⼩程序
    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'
    }
    })

  2. ⽀付宝⼩程序
    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'
    }
    })
  3. 淘宝⼩程序
    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>

  4. 百度⼩程序
    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>
  2. ⽀付宝:指令以a:开头
    1
    2
    3
    <view a:if="{{view == 'WEBVIEW'}}"> WEBVIEW </view>
    <view a:elif="{{view == 'APP'}}"> APP </view>
    <view a:else> alipay </view>
  3. 淘宝:完全同⽀付宝
  4. 百度
    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。

多端⼩程序开发

Tips:只介绍React语⾔的跨端框架;

编译时

⽤户编写的业务代码解析成AST树,然后通过语法分析强⾏将⽤户写的类React代码转换为可运⾏的⼩ 程序代码,代表:京东的Taro1/2、去哪⼉的Nanachi,淘宝的Rax。
以下以Rax为例

概览

编译时链路主要分为五个模块:

  1. CLI:整个链路的⼊⼝,⽤户编写的所有业务代码都经由 CLI 读取、处理和输出;
  2. loader:webpack loader,⽤于处理各种类型的⽂件,包括 app、page、component、script 以及静态资源等;
  3. compiler:⽤于进⾏ AST 转换并⽣成对应的⼩程序代码;
  4. runtime:为⽣成的 js 代码提供了运⾏时的垫⽚⽀持;
  5. universal:多端统⼀的 universal 组件以及 API 的基础服务⽀持;
CLI

从命令⾏读取各种必要参数,然后传⼊ webpack 执⾏。利⽤ webpack 的依赖分析能⼒,遍历到所有有效代码并交由对应的 loader 进⾏处理。

具体⽤途:

  1. CLI 依赖 webpack 对项⽬进⾏依赖分析,然后调⽤ loader对对应类型的⽂件进⾏处理
  2. CLI 对外提供 watch 和 build 两个指令
    1. watch:监听代码变动并实时编译;
    2. build:剔除部分调试⽤的代码(如 source map)并压缩代码,完成编译打包;

Q:在⼩程序原⽣开发框架中,⼊⼝⽂件 app.js 并没有声明依赖,⽽ pages 是在 app.json 中注册的, Rax⼊⼝⽂件是什么样的,它⼜是如何声明依赖的?
A:为了保持多端统⼀,Rax 采⽤同⼀套⼯程⽬录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├──	README.md	#	项⽬说明
├── build.json # 项⽬构建配置
├── package.json
└── src # 源码⽬录
├── app.js # 应⽤⼊⼝⽂件
├── app.json # 应⽤配置,包括路由配置,⼩程序
├── components # 应⽤的公共组件
│ └── Logo # 组件
│ ├── index.css # Logo 组件的样式⽂件
│ └── index.jsx # Logo 组件 JSX 源码
├── document # ⻚⾯的 HTML 模板
│ └── index.jsx
└── pages # ⻚⾯
└── Home # home ⻚ ⾯
└── index.jsx

app.json 内容

1
2
3
4
5
6
7
8
9
10
11
{
"routes": [
{
"path": "/",
"source": "pages/Home/index"
}
],
"window": {
"defaultTitle": "Rax App 1.0"
}
}

CLI 读取其中的 routes 并将所有引⽤到的 pages ⽂件以及 app.js 作为 entry,以 pages ⽂件为⼊⼝, 所有依赖⽂件将依次被遍历并交由对应 loader 进⾏处理。loader 处理完毕后最终的编译代码将⽣成到⽬的⽬录。

loader

Rax转⼩程序的loader统称为:jsx2mp-loader

  1. app-loader
    1. 处理 rax 源码中的 app.js
    2. 处理 app.json 中 的 window 属性并作⽀付宝/微信两端的配置抹平
  2. page-loader
    1. 处理定义在 app.json 中 routes 属性内的 page 类型组件
    2. 根据 jsx-compiler 中解析到的该组件所引⽤组件的信息,写⼊ json ⽂件中的usingComponents ,并将这些组件加⼊ webpack 依赖分析链并交由 component-loader 处理
    3. 处理⽤户定义在 app.json 中 routes 数组内每⼀个⻚⾯的配置(即 window 配置项)并输出⾄对应⻚⾯的 json ⽂件中
  3. component-loader
    1. 处理 component 类型组件并交由 jsx-compiler 处理然后产出编译后代码,并写⼊⾄指定⽬标⽂件夹位置
    2. 根据 jsx-compiler 中解析到的该组件所引⽤组件的信息,写⼊ json ⽂件的usingComponents 属性中,并将这些组件加⼊ webpack 依赖分析链并交由 component- loader 处理
  4. file loader
    1. 处理图⽚等静态⽂件资源,将其拷⻉⾄指定⽬标⽂件夹
  5. script loader:负责依赖路径处理
    1. npm包:搜集代码中使⽤到的 npm 依赖,获取 npm 包的真实地址 => 路径处理 => babel 编译 => 输出代码⾄⽬标⽂件夹
    2. 来⾃ npm 包的第三⽅原⽣⼩程序库:⽤户使⽤绝对路径去使⽤第三⽅原⽣⼩程序库时, script-loader 需要读取 js ⽂件同⽬录下同名的 json ⽂件中的 usingComponents 字段并将其加⼊ webpack 的依赖分析链
compiler

编译:是⼀种利⽤编译程序从源语⾔编写的源程序产⽣⽬标程序的过程或者动作,完整的流程是从⾼级 语⾔转换成计算机可以理解的⼆进制语⾔的过程:Rax -> ⼩程序DSL

编译在rax主要是 jsx-compiler:

  1. 词法分析(tokenizing)
  2. 语法分析(parsing)
  3. 代码⽣成(generate)

Example:

  1. input
    1
    2
    3
    4
    5
    6
    import { Component } from 'rax';
    export default class extends Component {
    render() {
    return (<view>hello world</view>);
    }
    }
    jsx compiler执⾏
  • type // app, page, component.
  • outputPath
  • sourcePath
  • resourcePath
    1
    2
    3
    const compile = require('jsx-compiler');
    const { baseOptions } = compile;
    const output = compile(code, { ...baseOptions, type: 'component' });
  1. output
    • ast:Babel 7格式的AST
    • imported:引⼊的模块和本地定义变量
    • exported:导出变量
    • template:miniapp识别模板
    • code:转义后的代码
    • map:source map
    • config:⼩程序配置
    • style:样式
    • usingComponents
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      {
      ast: ASTNodeTree,
      imported: {
      rax: [
      {
      local: "Component",
      default: false,
      importFrom: "Component",
      name: "rax",
      external: true
      }
      ]
      },
      exported: ["default"],
      code:
      'import { createComponent as __create_component__, Component as __component__ } from "jsx2mp-runtime";\n\nconst __def__ = class extends __component__ {\n render() {\n return {};\n }\n\n};\n\nComponent(__create_component__(__def__, {\n events: []\n}));',
      map: null,
      config: {
      component: true
      },
      style: "",
      usingComponents: {},
      template: "<view>hello world</view>"
      }
runtime

提供垫⽚,⼩程序 Page/Component与原⽣Rax⽀持还是有区别,使⽤ jsx2mp-runtime 来作了⼆者的桥接;

https://github.com/raxjs/miniapp/tree/master/packages/jsx2mp-runtime/src

universal

支持生态

运行时

代表:有蚂蚁的Remax,京东的Taro 3,淘宝的Rax。

小程序运⾏时的起点:kbone

  1. ⼩程序的技术底层依托于web技术,由于多线程架构的限制,对于有多端需求的项⽬来说,加⼀个功能或者改⼀个样式都可能需要改动两套代码(DOM、BOM API ⽆法打平);

⽬的:

  1. 为了更好的复⽤组件,尽可能完整的⽀持 Web 端的特性;
  2. 在⼩程序端的渲染结果要尽可能接近 Web 端 h5 ⻚⾯;

⽅案:
Web组件转⼩程序

  1. 限制⼤部分 Web 端特性,兼容性差,需要将 Web 端框架(⽐如 vue、react 等)给完整引进来 ;
  2. Web框架(vue、react)底层依赖DOM、BOM,需要提供适配

适配器:

  1. 在 appService 端运⾏的轻型 DOM 树;
  2. 提供基础的 DOM/BOM API;
  3. appService 端和 webView 端的交互通过适配器来进⾏;

Tips:

  1. DOM树本身是没有固定模式可循的,它的层级、dom 节点数量都是不固定的,没有办法⽤固定的wxml 将其描述出来;
  2. ⼩程序⾃定义组件⽀持使⽤⾃⼰作为其⼦节点,可以⽤递归引⽤的⽅式来构造任意层级、任意节点数量的⾃定义组件树,可以将若⼲个 DOM 节点映射成⼀个⼩程序的⾃定义组件,每⼀个⾃定义组件相当于描述出了 DOM 树的⼀部分,然后将这些⾃定义组件拼接起来就可以描述出⼀棵完整的DOM 树。
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
// 举例来说,假设⼀个⼩程序⾃定义组件 element 的 WXML 模板如下所示:
<view
wx:if="{{r.tagName === 'view'}}"
id="{{r.nodeId}}"
>
<block
wx:for=“{{r.children}}”
wx:key="nodeId"
>
<element data="{{r: item}}" />
</block>
</view>
<text wx:elif="{{r.tagName === 'text'}}">
{{r.content}}
</text>
// element 在模板中递归引⽤了⾃身,并通过条件判断终⽌递归。
// 那么,当逻辑层通过 setData 传递了以下⼀份数据过来时:

{
"nodeId": "1",
"tagName": "view",
"children": [
{
"nodeId": "2",
"tagName": "text",
"content": "我是?"
},
{
"nodeId": "3",
"tagName": "text",
"content": "rax"
}
]
}
// 最终呈现出来的视图便成了:

<view>
<text>我是</text>
<text>rax</text>
</view>

通过这种⽅式,我们巧妙地实现了在 WXML 模板固定的情况下,根据传⼊的 setData 数据来动态渲染视图的能⼒。⽽这,也正是运⾏时⽅案能够诞⽣的基础

kbone 在 worker 线程适配了⼀套 JS DOM API,前端框架(react、vue)或 js 调⽤ JS DOM API操作DOM,适配的JS DOM API 则接管 DOM 操作,在内存中维护了⼀棵 DOM tree,所有上层最终调⽤的DOM 操作都会更新到这棵 DOM tree 中,每次操作(有节流)后会把 dom tree 同步到 render 线程中,通过 wxml ⾃定义组件进⾏ render。

Rax类似Kbone:

  1. 采⽤driver,⼩程序的driver 只需复⽤ web 端的 driver-dom,因为底层的 document 和 window 变量都已经模拟好;
  2. 为开发者提供更贴近 web 的开发体验。这套⽅案意味着开发者除了使⽤ JSX 之外,也是⽀持直接使⽤ BOM/DOM API 创建视图,driver的API操作是可以引⽤的;
  • driver-miniapp
  • Rax事件系统:miniapp-render

    miniapp-render is a DOM simulator designed for MiniApp which can provides DOM-related API for developers.
    You can think of it as a lightweight jsDom running on appService.

Rax ⼩程序运⾏时中,模拟 DOM/BOM API 的库为 miniapp-render,其⽀持的 API 如下:

Rax事件系统通过 EventTarget 基类实现了⼀套完整的事件派发机制。逻辑层 DOM 节点均继承⾃EventTarget,通过唯⼀的 nodeId 来收集⾃身绑定事件。视图层模板上的每个内置组件都会绑定nodeId,并监听所有可触发的事件

  • ⽐如⼀个简单的 view 标签,会将 bindtap/bindtouchstart/bindtouchend 等事件都进⾏绑定。在事件触发时,通过 event.currentTarget.dataset.nodeId 获取到⽬标节点 id,再触发该节点上⽤户绑定的对应函数。

  • ⼯程设计
    Rax ⼩程序运⾏时 follow 了 Rax Web 的设计,Web 端 Webpack 打包出的 JS Bundle 可以在⼩程序运 ⾏时中复⽤。我们通过插件将 miniapp-render 模拟出的 window 和 document 变量注⼊该 bundle,再⽣成⼀个固定的⼩程序项⽬⻣架,在 app.js 中加载 JS Bundle 即可。

框架选择

API 设计与性能

  • API设计
    1. ⼩程序端总会存在⽆法抹平以及需要单独处理的地⽅;
    2. 每个端独⽴的属性不应该⼊侵基础框架本身,保证基础框架的纯净有利于做更多的扩展;
      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
      // Taro
      import Taro, { Component } from '@tarojs/taro'
      import { View, Text } from '@tarojs/components'

      export default class Index extends Component {
      config = {
      navigationBarTitleText: '⾸⻚'
      }

      componentWillMount () { }
      componentDidMount () { }
      componentWillUnmount () { }
      componentDidShow () { }
      componentDidHide () { }
      render () {
      return (
      <View>
      <Text>1</Text>
      </View>
      )
      }
      }

      // Rax
      import { createElement, Component } from 'rax';
      import View from 'rax-view';
      import Text from 'rax-text';
      import { isMiniApp } from 'universal-api';
      import { registerNativeListeners, addNativeEventListener, removeNativeEventListener } from 'rax-app';

      function handlePageShow() {}
      class Index extends Component {
      componentWillMount () { }
      componentDidMount () {
      if (isMiniApp) {
      addNativeEventListener('onShow', handlePageShow);
      }
      }
      componentWillUnmount () {
      if (isMiniApp) {
      removeNativeEventListener('onShow', handlePageShow);
      }
      }

      render () {
      return (
      <View>
      <Text>1</Text>
      </View>
      )
      }
      }

      if (isMiniApp) {
      registerNativeListeners(Index, ['onShow']);
      }
      export default Index;
  1. Rax 没有 componentDidShow componentDidHide 的概念,新增了和 W3C 标准类似的addNativeEventLisenter removeEventListener 等 API;
  2. 组件实例上没有⼀个叫做 config 的静态属性⽤来设置⻚⾯的 title 等配置;

react 本身是没有这些⽣命周期和配置的:Rax此处优势更明显;

  • 性能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // ⼩程序本身需要预置⽣命周期,⽽不能动态注册:
    Page({
    onShow() {}
    });

    // 不⽣效
    const config = {}
    Page(config);
    setTimeout(() => {
    config.onShow = () => {};
    }, 1000);

  • Rax:引⼊了 registerNativeListeners ,需要先注册,才能监听;

多端组件协议设计

  1. Taro:将组件统⼀在项⽬中进⾏编译产出为⼩程序代码不同;
  2. Rax:⽀持在Rax ⼩程序项⽬和原⽣⼩程序项⽬中都能正常使⽤ Rax ⼩程序组件;
  • ⽀持渐进式接⼊或迁移⾄Rax:
    • Rax ⼩程序组件⼯程的构建产物符合⼩程序语法,可以直接在原⽣⼩程序项⽬中使⽤;
    • 如果想渐进式地使⽤ Rax 来开发⼩程序,可以以组件或者⻚⾯为单位迁移到 Rax ;
  • 多端统⼀的组件使⽤体验
1
2
3
4
// Wrong
import CustomComponent from 'custom-component/miniapp/index'
// Correct,⽀持miniapp、web、weex等保持⼀致
import CustomComponent from 'custom-component'

基于 webpack 构建

  1. 基于插件体系,可定制扩展:Rax ⼯程以 build-script 为基础,通过插件体系⽀持各个场景;基于webpack-chain 提供了灵活的 webpack 配置能⼒,⽤户可以通过组合各种插件实现⼯程需求;
  2. 命令简洁,体验统⼀:Rax ⼩程序的编译时⽅案通过 webpack loader 来处理⾃身逻辑。以
    app/page/component 等⽂件⻆⾊分类的 webpack loader 会调⽤ jsx-compiler 进⾏代码的 AST 分析及处理,再将处理完的代码交由 loader ⽣成对应的⼩程序⽂件;运⾏时⽅案直接复⽤ Web 端
    的编译配置,再通过额外的 webpack 插件⽣成具体的⼩程序代码。

补充知识点

主流小程序框架对比