近年新出的UI框架,包括React,Flutter, SwiftUI等在内都采用了声明式的方法构建UI,其中基于React的RN,Flutter都是多端框架,能够一套代码多端复用。可是在国内“端”还有一个小程序,因此在国内的跨端,必需要兼顾到小程序。javascript
本文将探讨一种将声明式UI语法在类小程序平台运行的通用方式,这是一种等效运行的方式,对原语法少有限制。html
“Talk is cheap. Show me your code !”。前端
基于这个原理,咱们分别在 React Native
端,Flutter
端进行了实践,这两个项目的代码都托管在了github
,欢迎关注star。java
RN端的实践Alita:github.com/areslabs/al…react
Flutter
端的实践 flutter_mp:github.com/areslabs/fl…。git
先来看下这两个项目:github
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_mp的代码托管在github flutter_mp,因为精力和时间有限,flutter_mp
还处于很早期的阶段。首先咱们根据下文阐述的方式生成 wxml
文件,配合一个极小的 Flutter
运行时(只存在到 Widget
层),最终把 Flutter
的渲染部分替换成小程序环境。小程序
flutter_mp
示例效果:
Flutter | 微信小程序 |
---|---|
![]() |
![]() |
下面咱们探讨把声明式UI运行在类小程序平台的通用方式,这是一种底层渲染机制,他不限于上层是React或是Flutter或是其余,也不限于底层渲染是微信小程序或是支付宝小程序等。
首先咱们看一下两种不一样的UI构建方式。
出于未知缘由的考虑,小程序框架虽然最终的运行环境是webview,可是它禁用了DOM API,这直接致使React
,Vue
等前端流行框架没法直接在小程序端运行。替代性的,在小程序上构建UI须要采用一种更加静态的方式--- wxml
文件,能够当作是一种支持变量绑定的 html
:
<view>Hello World</view>
<view>{{txt}}</view>
<view wx:if="{{condition}}">{{txt}}</view>
复制代码
因为 wxml
文件须要预先定义,且阉割了全部的DOM API,因此小程序“动态”构建UI的能力几乎为0。
声明式的方式构建UI主要在于“描述界面而不是操做界面”,从这个角度 html
, wxml
都属于“声明式”的方式。 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
取不一样值的时候:
state = {x: false, condition1: true}
时: render
结果 UI("View", UI("Text", "Y"), UI("Text", "condition1"))
state = {x: true, condition2: true}
时: render
结果 UI("View", UI("Text", "X"), UI("Text", "condition2"))
上面的App组件,随着 state
的改变,render
返回的“大UI值”理所固然的随着改变,这个“大UI值”由其余“小UI值”组合而成。请注意这里的“UI”只是“普通”的一个数据结构,故而这里能够是一个与平台无关的纯JS过程,这个过程不论是在浏览器,仍是RN,仍是小程序都是同样的。不同的地方在于:把这个声明式构建出来的“大UI值”数据结构渲染到实际平台的方式是不同的。
在浏览器: ReactDOM.render()
,将会遍历这个“大UI值”,调用DOM API渲染出实际视图
在Native端:表示大UI值
的数据经过 js-native 的 bridge
,传递到 native
,native
根据这份数据填充原生视图
在小程序端:怎么在小程序上渲染出这个大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”的基石。
总结一下,以上的工做:
template
对应<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>
复制代码
当 state = {x: false, condition1: true}
时,只须要生成以下的数据:
data = {
child1: {
templateName: "00004"
},
child2: {
templateName: "00001"
}
}
复制代码
当 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"
}
}
复制代码
随着data
在template
上的一步一步展开,全部的”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”的基石,且这种方式不会对上层语法的语义进行推测转化,因此是相对无损的。
Alita
和 flutter_mp
分别是这种渲染方式在 React
和 Flutter
上的具体实现。