聲明式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 上的具體實現。

相關文章
相關標籤/搜索