聲明式UI框架在類小程序運行的原理

blank

聲明式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範例效果:

React Native效果
小程序效果

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分別是這種渲染方式在React和Flutter上的具體實現。

What do you think?

Written by marketer

blank

雲鳳蝶可視化搭建的推導與實現

blank

NGW,前端新技術賽場:Serverless SSR 技術內幕