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

做者:严康 刘小夕

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

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

“Talk is cheap. Show me your code !”。前端

基于这个原理,咱们分别在 React Native 端,Flutter 端进行了实践,这两个项目的代码都托管在了github,欢迎关注starjava

RN端的实践Alita:github.com/areslabs/al…react

Flutter 端的实践 flutter_mp:github.com/areslabs/fl…git

先来看下这两个项目:github

RN端的实践:Alita

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

Alita示例效果:编程

RN 微信小程序

Flutter端的实践:flutter_mp

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

flutter_mp示例效果:

Flutter 微信小程序

下面咱们探讨把声明式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做者之一)的论述:

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值”,也就是下图的渲染部分

首先,一块“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”的基石。

总结一下,以上的工做:

  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 分别是这种渲染方式在 ReactFlutter 上的具体实现。

相关文章
相关标签/搜索