小程序 runtime 整理(实在凑不够万字长文)

1,842 阅读6分钟

因为最近在重新梳理小程序的 runtime 标准,所以单独写一篇文章整理小程序的重要功能

最终目的是构建一棵树,将这所有的信息放到树上,最终得到一个微信的子集

一、json 配置:

  1. app.json
{
  "pages": ["pages/index/index", "pages/logs/index"], // 默认匹配第一个
  "window": {
    "navigationBarBackgroundColor": "#ffffff", // 顶部导航栏背景色
    "navigationBarTextStyle": "black", // 顶部导航栏文字颜色
    "navigationBarTitleText": "微信接口功能演示", // 顶部导航栏文字内容
  },
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index", // 页面路径
        "text": "首页", // 下面的小字
        "iconPath": "img/news.svg", // 默认图
        "selectedIconPath": "img/news-sel.svg" // 选中后的图片
      },
      {
        "pagePath": "pages/logs/logs",
        "text": "日志"
      }
    ]
  },
  "usingComponents": { // 公共组件
    "component-tag-name": "path/to/the/custom/component" 
  },
  "useExtendedLib": { // 减少预编译
    "kbone": true,
    "weui": true
  },
  "debug": true, // 开启 debug 模式,有用的日志
  "version": "v3", // 可以借助这个字段做版本控制
}

以上,是我认为 app.json 中比较重要的内容,我们除了支持这些字段,还要一些小的细节要处理

1. tabbar 的 icon 需要支持 svg 格式,png 位图会失真
2. useExtendedLib 可以做成 module federation,用来减少预编译
3. 增加一个 version 字段,做版本控制
  1. [page].json
{  
  "navigationBarBackgroundColor": "#ffffff",
  "navigationBarTextStyle": "black",
  "navigationBarTitleText": "微信接口功能演示",
  "enablePullDownRefresh" : "true", // 启动下拉刷新并触发 onPullDownRefresh 回调
  "onReachBottomDistance": 50, // 触发 onReachBottom 回调的距离
  "usingComponents": { // 页面组件
    "component-tag-name": "path/to/the/custom/component" 
  },
  "disableScroll": "true" // 禁用 IOS 自带的回弹和 onPageScroll 方法
}

page 的字段就简单地多了,除了几个 window 的字段会覆盖 app.json,只有 useComponents 字段比较重要

有一些需要注意的:

1. 上面列举的字段,app.json 也支持统一配置
2. 下拉刷新,触底,scroll 三个配置都是针对页面的,需要和 scroll-view 区分开

二、App 接口

  1. App()
App({
  onLaunch (options) {
    // app 启动时
  },
  onShow (options) {
    // app 激活时
  },
  onHide () {
    // app 失活时
  },
  onError (msg) {
    // 错误收集
  },
  globalData: {}, // globaldata
})
  1. getApp()
const app = getApp()
console.log(app.globalData)

小程序的 App 实例上没有太多东西,但是一些工具组件啥的,还是要挂上来的

三、Page 接口

  1. Page()
Page({
  data: {
    count: 0
  },
  onLoad: function(options) {
    // 页面 load
  },
  onShow: function() {
    // 页面 show
  },
  onReady: function() {
    // 页面 ready
  },
  onHide: function() {
    // 页面 hide
  },
  onUnload: function() {
    // 页面 unload
  },
  
  onPullDownRefresh: function() {
    // 下拉刷新回调
  },
  onReachBottom: function() {
    // 触底回调
  },
  onShareAppMessage: function () {
    // 基本没啥用的分享
  },
  onPageScroll: function() {
    // scrollTo
  },
  onResize: function() {
    // pc 上的 resize
  },
  onTabItemTap(item) {
    // tab 的 tap 回调
    console.log(item.index)
    console.log(item.pagePath)
    console.log(item.text)
  },
  // 普通事件
  viewTap: function() {
    this.setData({
      text: 'Set some data for updating view.'
    })
    // 序列化为 json-patch
    this.setData({
      "a.b.c[0]": 'Set some data for updating view.'
    })
  }
})

Page 的事件比较多,但主要分为三类:

1. 生命周期 -> 待会总体讲
2. 下拉,触底,scroll 的回调
3. data,setData,普通事件

其中最容易被忽视的是 this.setData 的 json-patch 格式,这个是之后自定义组件的 observer 的基础

  1. getCurrentPages()
const stack = getCurrentPages()

页面栈

这是一个栈,通过 getCurrentPages 拿到的栈,第一个元素是首页,最后一个元素是当前页

API栈行为
load首页入栈
wx.navigetTo入栈
wx.redirectTo栈顶的替换
wx.navigateBack循环出栈
wx.switchTab清空栈,然后目标页面入栈
reload清空栈,然后当前页面入栈

以上的栈行为和生命周期相关,但总的来说,只要这个页面还在栈中,生命周期走的就是 show 和 hide,如果已经不在栈中了,则走 load 和 unload

这块我们待会梳理

四、Component 接口

  1. Component()
Component({

  behaviors: [],

  properties: {
    myProperty: { // 属性名
      type: String,
      value: ''
    },
    myProperty2: String // 简化的定义方式
  },

  data: {}, // 私有数据,可用于模板渲染

  lifetimes: {
    // 生命周期
    attached: function () { },
    moved: function () { },
    detached: function () { },
  },

  pageLifetimes: {
    // 组件所在页面的生命周期
    show: function () { },
    hide: function () { },
    resize: function () { },
  },

  methods: {
    onMyButtonTap: function(){
      this.setData({
        'A[0].B': 'myPrivateData'
      })
    },
    triggerParentEvent(){
      this.triggerEvent('myevent', myEventDetail, myEventOption)
    }
  }
  
  observers: {
    "A[0].B": function(newVal, oldValue) {
        console.log(newVal)
    }
  }
})

Component 比 Page 主要多了 properties 和 observers

props 是单项数据流,它只能从父亲流进来,但是要注意,properties 和 data 共享同一个 template 作用域,原则上两者不能同 key,在微信中 data 会覆盖 props,vue 中会报错

我倾向于 vue 的处理

observers

observers 和以前的 watch 是一样的,但是性能有了很大的提升,我怀疑是使用了 Proxy 做了写时拷贝

值得一提的是,这块是拿 setData 的 json-patch 和 observer 的 json-patch 直接做对比

json-patch 除了提升 observer 的性能,还能提升 setData 的线程传递的性能

  1. Behavior()
const b = Behavior({
  behaviors: [],
  properties: {
    myBehaviorProperty: {
      type: String
    }
  },
  data: {
    myBehaviorData: {}
  },
  attached: function(){},
  methods: {
    myBehaviorMethod: function(){}
  }
})

这玩意的参数和自定义组件是一样的,它一般的合并策略有三种:

1. 队列,如生命周期
   -> behavior -> component
   -> child -> parent
   -> front -> back
   
2. 合并,比如 data,observer 这种对象
   -> component -> behavior
   -> parent -> child
   -> back -> front

3. 替换,事件,properties
   -> 规则同 2

值得一提的是,这里的生命周期的顺序是冒泡的,接下来继续梳理生命周期

三、生命周期

这块需要单独说,因为小程序是双线程的,而且 Page,Component,甚至 Behavior 都和生命周期有关

  1. Page 的生命周期,这个最简单:
onload:逻辑层加载,入栈

onready:视图层加载完毕,相当于 didmount

onshow:native 端显示 ————————
                             |
onunload:逻辑层卸载,出栈    | onshow 和 onhide 来回切换
                             |
onhide:native 端隐藏 ————————

这里有一点比较重要,也就是 onshow/onhide 这俩生命周期,这和 berial 的 mount/unmount 一样

它们会来回切换,在 berial 中我使用一个有状态的队列做到这一点

  1. Component 的生命周期

微信的 Component 是个纯视图层的实现,它的生命周期本来是 web-component 的生命周期的,所以这事……

attached: 渲染层加载完毕,相当于 didmount

detached: 渲染层卸载完毕,相当于 didunmount
  1. 父子间生命周期的顺序

在 react 中,父子的生命周期顺序是先潜水再冒泡的

<cmp-a>
  <cmp-b>
    <cmp-c></cmp-c>
  </cmp-b>
</cmp-a>

cmp-a - componentWillMount
cmp-b - componentWillMount
cmp-c - componentWillMount
cmp-c - componentDidMount
cmp-b - componentDidMount
cmp-a - componentDidMount

小程序的顺序是一样的,它的 attached 相当于 componentDidMount,自然也是从子到父

四、wxs

这玩意是在视图层的,我猜应该是模拟了一个 cjs 标准,然后用 new Function 跑的

这个没什么好说的好像……

构建数据结构

基本上就这么多了,把所有东西都整理一遍之后,就可以冥思这个结构长啥样了

首先肯定是一棵树,然后大概长这样:

App
  - [ // 这里是个栈
    - Page1
       - [
         - Component1
           -[
            - Component3
           -]
         - Component2
       - ]
    - Page2
  - ]
  
1. 总体结构是个树
2. Pages 是个栈
3. behavior 是个树
4. 生命周期是个队列

有了这个结构,就很容易补 case 了,比如:

export function getCurrentPages(){
  reutn app.pageStack.values()
}

一行代码搞定,啊哈哈

经过这一波整理,我对小程序总体架构理解地更好一点了,相当于一个低配版的 vue

具体的实现细节不能透露,但我觉得我可以构建出一个满意的结构了

望天,第一次写这么长的文章,感觉实在凑不出来一万字,饶了我吧,大家都要讨生活鸭……