近年新出的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
上的具體實現。