Bootstrap

react ui框架_声明式UI框架在类小程序运行的原理

近年新出的UI框架,包括React,Flutter, SwiftUI等在内都采用了声明式的方法构建UI,其中基于React的RN,Flutter都是多端框架,可以一套代码多端复用。但是在国内“端”还有一个小程序,所以在国内的跨端,必须要兼顾到小程序。

本文将探讨一种将声明式UI语法在类小程序平台运行的通用方式,这是一种等效运行的方式,对原语法少有限制。

“Talk is cheap. Show me your code !”,基于这个原理,我们分别在 React Native 端,Flutter 端进行了实践,这两个项目的代码都托管在了github,欢迎关注star。RN端的实践Alita, 在 Flutter 端的实践 flutter_mp。

先来看下这两个项目:

RN端的实践:Alita

Alita的代码托管在github alita,除了使用下文将要说明的方式处理了React语法以外,Alita还对齐处理了 React Native 的组件/API,可以把你的 React Native 代码运行在微信小程序平台,Alita的侵入性很低,使用与否,并不会对你的原有React Native开发方式造成太大影响。另外由于React Native本身就可以运行在Android, IOS,web(react-native-web),在加上Alita可以打造出适配全端的大前端框架。

看下Alita示例效果:

94499944d5c77e832c9d57e769ce3189.gif
React Native效果

1a85856b440cf1cee5f1c0b21d9c6899.gif
小程序效果

Flutter端的实践:flutter_mp

flutter_mp的代码托管在github flutter_mp,由于精力时间有限flutter_mp还处于很早期的阶段。首先我们根据本文阐述的方式生成wxml文件,配合一个极小的Flutter运行时(只存在到Widget层),最终把Flutter的渲染部分替换成小程序环境。

34cc4ec0ed48cb7ce7434315a1551088.png

看下flutter_mp示例效果:

a5c0baf62c2af52cfc4d0a00d453bbb0.png
Flutter效果

1ac70bbd80ff77f55194e76fe91902fa.png
小程序效果

下面我们探讨把声明式UI运行在类小程序平台的通用方式,这是一种底层渲染机制,他不限于上层是React或是Flutter或是其他,也不限于底层渲染是微信小程序或是支付宝小程序等。

两种UI构建方式

首先我们看一下两种不同的UI构建方式。

小程序wxml文件

出于未知原因的考虑,小程序框架虽然最终的运行环境是webview,但是它禁用了DOM API,这直接导致ReactVue 等前端流行框架无法直接在小程序端运行。替代性的,在小程序上构建UI需要采用一种更加静态的方式—- wxml 文件,可以看成是一种支持变量绑定的 html

<view>Hello World</view>
<view>{{txt}}</view>

<view wx:if="{{condition}}">{{txt}}</view>

由于 wxml 文件需要预先定义,且阉割了所有的DOM API,所以小程序“动态”构建UI的能力几乎为0。

React/Flutter等声明式“值UI”

声明式的方式构建UI主要在于“描述界面而不是操作界面”,从这个角度 htmlwxml 都属于“声明式”的方式。 React / Flutter 和html/wxml有什么不同呢?

我们先看一个 React 的例子:

class App extends React.Component {

    f() {
        return <Text>f</Text>
    }

    render() {
        var a = <Text>HelloWorld</Text>
        return (
            <View>
               {a}
               {this.f()}
            </View>
        )
    }
}

在组件的 render 方法内,声明了一个 var a = <Text>HelloWorld</Text>this.f() 返回了另一个 Text 标签,最后通过 View 将他们组合起来。

对比前面的 wxml 方法,可以看出 JSX 非常灵活,UI标签可以出现在任何地方,进行任意自由组合。本质来说这里暗含了一个 “值UI” 的概念。思考一下,我们在写 var a = <Text>HelloWorld</Text> 的时候,并没有把 <Text>HelloWorld</Text> 当成UI标签特殊对待,它更像是一个普通的“值”,它可以用来初始化一个变量,也可以作为函数的返回值。我们是在以“编程”的方式构建UI,“编程”的方式赋予了我们构建UI时极强的能力和灵活性。

我们看下Dan Abramov(React作者之一)的论述:

7500dbfb1baf666f0acda8b1db323c11.png

Flutter Widget的设计灵感来源于 React ,同样是声明式“值UI”,所以本文准确的标题应该叫 “声明式值UI框架在类小程序运行的原理”

我们从“值UI”的角度考虑如下的组件:

class App extends Component {

    f() {
        if (this.state.condition1) {
            return <Text> condition1 </Text>
        }

        if (this.state.condition2) {
            return <Text> condition2 </Text>
        }

        ...
    }

    render() {
        var a = this.state.x ? <Text>X</Text> : <Text>Y</Text>

        return (
            <View>
                {a}
                {this.f()}
            </View>
        )
    }
}

换算成”UI“值的形式(假设有一个UI类型的构造函数):

class App extends Component {

    f() {
        if (this.state.condition1) {
            return UI("Text", "condition1")
        }

        if (this.state.condition2) {
            return UI("Text", "condition2")
        }

        ...
    }

    render() {
        var a = this.state.x ? UI("Text", "X") : UI("Text", "Y")

        return UI("View", a, this.f())
    }
}

state 取不同值的时候:

  1. state = {x: false, condition1: true} 时: render 结果 UI("View", UI("Text", "Y"), UI("Text", "condition1"))
  2. state = {x: true, condition2: true} 时: render 结果 UI("View", UI("Text", "X"), UI("Text", "condition2"))
  3. 等等

上面的App组件,随着 state 的改变,render 返回的“大UI值”理所当然的随着改变,这个“大UI值”由其他“小UI值”组合而成。请注意这里的“UI”只是“普通”的一个数据结构,故而这里可以是一个与平台无关的纯JS过程,这个过程不管是在浏览器,还是RN,还是小程序都是一样的。不一样的地方在于:把这个声明式构建出来的“大UI值”数据结构渲染到实际平台的方式是不一样的。

  • 在浏览器: ReactDOM.render(),将会遍历这个“大UI值”,调用DOM API渲染出实际视图
  • 在Native端:表示大UI值的数据通过 js-native 的 bridge,传递到 nativenative 根据这份数据填充原生视图
  • 在小程序端:怎么在小程序上渲染出这个大UI值表示的实际视图呢???

小程序wxml等效表达“值UI”的方式

前文说了构建“大UI值”的构建过程是平台无关的,主要问题在于如何利用小程序静态的 wxml 渲染出这个“大UI值”,也就是下图的渲染部分

712a92d303f86d1a4d5ec39b5a02ddbe.png

首先,一块“UI值” 在小程序上是有等效概念的,小程序上表示“一块”这个概念的是 template, 比如 UI("Text", "X"), 可以等效为:

<template name="00001">
    <text>X</text>
</template>

比较难处理的是“UI值”之间的动态绑定,如下:

render() {
    var a = this.state.x ? UI("Text", "X"): UI("Text", "Y")
    return UI("View", a, this.f())
}

对于 UI("View", a, this.f()) 这样的“一块UI值”要怎么对应呢?这里的 a, this.f() 是一个运行期才能确定的值,且随着 state 的变化而变化,这样的一个“UI值”,如何用template表示呢? 这里我们使用一个占位 tempalte 来表达动态的未知。

<template name="00002">
    <View>
        <template is="{{some dynamic value1}}"/>   
        <template is="{{some dynamic value2}}"/>  
    </View>
</template>

我们用形如<template is="{{some dynamic value}}"/> 这样的占位template表达一个运行时动态确定的“UI值”,利用is属性的动态性来表达“UI”值的动态组合。

这里 is 属性的“一丢丢动态性”将成为使用 wxml 构建整个“值UI”的基石。

99c1b30b147b381ae2270a74cbf24f3b.png

总结一下,以上的工作:

  1. 每一个“UI值”,用 template 对应
  2. “UI值”动态组合的地方,使用占位 <template is=/> 替代,

实际上基于这两点构建的 wxml 文件,已经具备了表达组件所有render结果 的能力,只需要在不同 state 下,赋予占位 template 正确的 is 值即可(是个嵌套过程),这里有些跳跃,思考一下。

比如以上面的App组件为例,生成的 wxml 文件大致如下:

<template name="00001">
    <Text> condition1 </Text>
</template>

<template name="00002">
    <Text> condition2 </Text>
</template>

<template name="00003">
    <Text> X </Text>
</template>

<template name="00004">
    <Text> Y </Text>
</template>

<view>
    <template is="{{child1.templateName}}" data="{{... child1}}" />
    <template is="{{child2.templateName}}" data="{{... child2}}" />
</view>
  1. state = {x: false, condition1: true} 时,只需要生成如下的数据:
    data = { child1: { templateName: "00004" }, child2: { templateName: "00001" } }
  2. state = {x: true, condition2: true} 时,只需要生成如下的数据:
    data = { child1: { templateName: "00003" }, child2: { templateName: "00002" } }

随着state的改变,data数据结构也在不断改变,最终会把此 state 对应的所有 is 值设置到对应 template 上。更进一步的,当组件树结构越来越复杂,data结构也会嵌套越来越深。当上面的 a 变量如下的时候

var a = this.state.x ? <View>{this.f()}</View> : <Text>Y</Text>

这里 a 变量<View>{this.f()}</View> 本身包含了另一个“动态”组合{this.f()}, 这个时候产生的 data:

data = {
           child1: {
               templateName: "00003"

               child1: {
                   templateName ...  // 
               }    
           },
        child2: {
            templateName: "00002"
        }
    }

随着datatemplate上的一步一步展开,所有的”UI值“组合关系将通过is属性被正确设置,这是一个嵌套过程。

那么现在的问题变成了如何在不同的 state 下,构造出正确的 data 结构。

这正是 ReactMiniProgram.render 的工作。类比 ReactDOM.render遍历组件树构建DOM节点的行为, ReactMiniProgram.render 在执行过程中,遍历整个组件树,不断收集聚合构建出正确的渲染data数据,最终把这部分数据传递给小程序,小程序根据这份数据渲染出最终的视图。

上文虽然大部分针对 React 在讨论,但是 Flutter 其实是一样的情况,他们都是“声明式值UI”,处理“值UI”的方式是完全一样的,只不过最后的底层渲染部分换成了小程序wxml的方式。

总结一下这个通用方式的完整过程:首先根据上层语法生成 wxml 文件,在 wxml 文件生成的过程中,由于不会做任何语义上的推断和转化,所以并不存在语法损耗。同时上层存在一个“运行时”,这个“运行时”运行的仍然是原平台代码,负责对“UI值”的处理,最终构建出一个表达“大UI值”的 data 结构,这是一个纯JS过程。然后把这个 data 数据传递到小程序,配合之前生成的 wxml 文件,渲染出小程序版本的视图。

总结

template is 属性的动态性是在小程序上等效构建“声明式值UI”的基石,且这种方式不会对上层语法的语义进行推测转化,所以是相对无损的。

Alitaflutter_mp分别是这种渲染方式在React 和Flutter上的具体实现。

;