Shadow Widget 提倡在可視設計器中開發用戶界面,輸出轉義標籤,而非 JSX。許多童鞋可能不知道 SW 一樣支持用 JSX 設計界面,開發體驗比原生 React 編程好出不少,本文就介紹這方面知識。javascript
Shadow Widget 區別於其它前端框架的關鍵特點是可視化設計,由於 JSX 與 javascript 混寫,不能直接支持可視化設計。因此,SW 用 「轉義標籤」 表達可視設計的輸出,由於 SW 強調可視化,因此如何運用 JSX 的內容,在官方文檔中被弱化了,有一些零星介紹,分散在手冊各個章節,本文將它們串接起來說。html
在 Shadow Widget 下作開發,既能夠是主流的 「正交框架」 模式(也就是遵循 MVVM 思路設計可視界面,再用 Flux 框架組織橫向數據流的開發方式),也能夠徹底順從 React 原生模式,只把 SW 看做更好的 lib 庫來使用。**下面咱們結合代碼實例,講解後一開發方式。前端
因爲 JSX 界面設計與用鼠標拖拉配置界面的設計是等價的,咱們以 React 原生模式作開發,相對 SW 主流方式,主要損失可視化的直觀特性,其它並不損失。固然,目前使用 JSX 還得藉助 Babel 轉譯環境,搭建 "Babel + Browserify" 或 "Babel + Webpack" 開發環境是不得已的選擇。java
如何建立新工程及如何搭建 Browerify 或 Webpack 環境,請參考《Shadow Widget 用戶手冊》的 「3.1 搭建工程環境」 一章。react
1) json-x
git
json-x
是 「轉義標籤」 的數據化形式,用 javascript 的 Array 數據表達各層嵌套節點。json-x
與轉義標籤的關係,就像 xml 與 HTML DOM 的關係。json-x
與 JSX
是對應的,是同一類東西,不過 json-x
不如 JSX 易讀。github
2) WTC(Widget Template Class,構件模板類)編程
WTC 對應於 React 中各 Component 的 class 類定義。React 要求這麼定義:json
class MyButton extends React.Component { constructor(props) { // ... } componentDidMount() { // ... } render() { // ... } }
WTC 要求這麼定義(必須從已有的 WTC 類繼承,並且只能用 ES6+ 語法才作獲得):segmentfault
class MyButton_ extends T.Button_ { constructor(name,desc) { super(name,desc); } getDefaultProps() { var props = super.getDefaultProps(); // props.attr = value; return props; } getInitialState() { var state = super.getInitialState(); // ... return state; } componentDidMount() { // ... } $onClick: function(event) { alert('clicked'); } }
WTC 不是 React class,不能在 JSX 中直接使用,應先轉成 React class。
3) render 函數
若是想把 React 程序寫得更嚴謹,減小 Side Effects,應該在 render()
函數集中處理各類控制邏輯,對其它函數的功能,諸如 getInitialState, componentDidMount, componentWillUnmount
等,不妨簡單這麼理解:爲 render()
提供配置服務。當配置縮簡到能夠忽略時,Component 定義便退化爲單個函數(即 Functional Component),該函數等效提供 render
功能。
Shadow Widget 按這個思路,把控制邏輯集中在 render()
函數中處理,包括:duals 雙源屬性的偵聽、觸發、聯動,$if, $for
等控制指令與可計算屬性自動更新等。由於複雜性在此封裝,render
不便公開給用戶定製,另外,Shadow Widget 爲了支持可視化設計,儘可能將 render()
中的邏輯控制分解,分離出屬性項供配置,因此,Shadow Widget 再也不鼓勵用戶自定義 render()
函數,雖然你們能夠 hack 各 WTC 的 render
實現過程,而後仿照着本身寫一個,但這不是建議的作法。
藉助如下途徑,在 Shadow Widget 可實現 render 過程的邏輯控制:
props.attr, duals.attr, state.attr
取值控制當前節點的界面表現 props.attr
反映了 Component 生存週期內的不變量,duals.attr
是可變量,state.attr
也是可變量,傾向用做節點內私有控制(不供其它節點調用)。comp.setChild()
來增、刪、修改子節點 render()
調用的過程之中調用 comp.setChild()
idSetter
函數中,調用 utils.setChildren()
來增、刪、修改子節點上面 3 點,第一點比較好理解,第二點是 Shadow Widget 增長的,原生 React 不提供這種操做,比方說,在一個頁面提交一條反饋意見,用戶能夠點一下 「刪除」 按鈕可刪掉剛提交的意見。若用原始 javascript 實現,大概用這麼一條語句:
commentNode.parentNode.removeChild(commentNode);
原生 React 處理這種需求要稍微繞一下,給 commentNode
的父節點發個 "刪除指令",而後由它代爲實施 "刪除操做"。Shadow Widget 提供 comp.setChild()
至關於補回 javascript 本可直接實現的操做。
上面第三點,在傳入的 props.id__
函數中,能夠經過修改 duals.attr
與 state.attr
改變本節點的顯示效果,還能經過調用 utils.setChildren()
來改變子節點怎麼顯示。其實現原理與在 render()
函數中寫代碼是等價的,將在後文細述。
同爲定義 component 類,WTC 繼承鏈與 React class 繼承鏈是不相干的兩條鏈,前者起始於 T.Widget_
,後者起始於 React.Component
。當前者 WTC 類實例調用 _createClass()
獲得值,才與後者等效。
好比:
var AbstractButton = new MyButton_(); // MyButton_ is WTC var MyButton = AbstractButton._createClass(); // MyButton is React class var jsx = <MyButton>test</MyButton>; var MyButton2 = AbstractButton._createClass( { $onClick: function(event) { alert('another onClick'); } }); var jsx2 = <MyButton2>test2</MyButton2>; var MyButton3 = T.Button._createClass( { $onClick: function(event) { alert('yet another onClick'); } }); var jsx3 = <MyButton3>test3</MyButton3>;
簡單理解 WTC,可把它看做 React class 的 class 定義,即,它是一種用於生成 React class 的模板,因此 WTC 是 "構件模板" 的類(Widget Template Class)。
咱們之因此要插入 「模板」 一級的抽像物,主要爲了適應可視化編程,Widget Template 不僅用來生成 React class,也爲可視設計器提供支持。另外,一個 Component 的行爲在 WTC 中定義,仍是在 _createClass(defs)
的傳入參數(即投影類)定義是可選的,好比上例中 $onClick
事件函數,在哪一個地方均可定義。這麼設計的好處是:習慣用 ES6 編碼的童鞋,在 WTC 編程,習慣用 ES5 的,用投影類編程,沒必要搭建 Babel 轉譯環境。
Shadow Widget 提供 utils.getWTC()
接口用來批量從 T 模塊取得 React class,好比:
var t = utils.getWTC('*'); // or, utils.getWTC(['Panel','P']) var jsx = ( <t.Panel width={300}> <t.P>Hello, world!</t.P> <t.P><button>Test</button></t.P> </t.Panel> );
請注意,使用來源於 WTC 的 React class 構造界面,系統會自動生成一顆樹,各節點按層次串接起來,任何非 WTC 節點都不能成爲 WTC 節點的父節點,反過來能夠,即:非 WTC 節點能掛到 WTC 節點下,但 WTC 節點不能掛到非 WTC 節點下。好比上面 button
不是 WTC 節點,能夠掛到 WTC 節點 t.P
之下,成爲末梢節點。
React 有兩種純渲染函數,其一是 "Stateless Functional Component"(SFC),以下:
function HelloMessage(props) { return <div>Hello {props.name}</div>; }
還有一種 "Pure Render Component"(PRC),所謂 pure 是指,若是 props
及 state
不變,則 render 結果不變。好比:
import PureRender from 'react-addons-pure-render-mixin'; class HelloComponent extends React.Component { constructor(props) { super(props); this.shouldComponentUpdate = PureRender.shouldComponentUpdate.bind(this); } render() { return <div>Hello {this.props.name}</div>; } }
SFC 彷佛簡單,易讀易維護,也方便 "Lifting State Up"(後文有論述),但 SFC 比 PRC 缺乏用 shouldComponentUpdate()
避免重複刷新,性能會受影響,另外,JSX 中把 SFC 的函數用做 tag,動詞用做名詞,閃着一絲詭異的光。
Shadow Widget 開創性的設計 idSetter 機制,將 SFC 與 PRC 的優勢結合起來了。一方面,你不須要非得用 class 類定義一個 Component,在層層嵌套的函數式風格中,不宜隨便找個地方就定義 class,另外,idSetter 是函數,在一個函數中定義 Component 全部行爲。
好比:
function btn__(value,oldValue) { if (value <= 2) { if (value == 1) { // init process } else if (value == 2) { // mount } else if (value == 0) { // unmount } return; } // rendering for evey render() // ... } var jsx = <MyButton $id__={btn__}>test</MyButton>
若用 SFC 方式編程,先定義的一個 MakeButton
函數,而後用 <MakeButton>test</MakeButton>
描述 UI 界面。MakeButton()
最終返回的 tag 是 button
,仍是 span
,或是其它是動態變化的。但這裏 <MyButton $id__={btn__}>test</MyButton>
倒是明確指定 tag 是 MyButton
,很直接。
設計界面時,手頭的 tag 標籤至關於食材(好比 「米飯」),給各 tag 指定各類屬性來控制它的外觀,這是最直接的設計方式。Stateless Functional Component 是 「紫菜包飯」,用函數形式包裹食材,idSetter 方式則至關於 「飯包紫菜包飯」,外觀表現還是 「米飯」,內層用紫菜包裹過。
傳遞給 $id__
的函數是 idSetter,這是全能的,由於系統在基類的 componentWillMount, componentDidMount, componentWillUnmount, render
這 4 個函數中增長了對 idSetter 函數自動調用。其中,value <= 2
下的 3 個條件分支,分別等效於在 componentWillMount, componentDidMount, componentWillUnmount
中編碼,其它條件(即 value > 2
)至關於在 render
函數中編碼。
這麼處理有幾個好處:
Functional Component
功能受限,由於不能插入 componentDidMount, componentWillUnmount
時的處理,若用 React class 定義一個 Component 的行爲,你擁有 componentDidMount, componentWillUnmount
等專項處理函數,但 React class 定義是靜態聲明,非單項函數,把 class 定義在層層嵌套的任一函數中,比較彆扭。idSetter 同時克服了這兩種缺陷。props.attr
逐層傳遞的數據共享方式,用起來不方便。React 官方爲此提供一種 「上舉 state」 的解決方案,參見 Lifting state up。咱們取 React 官方 Lifting State Up
一文介紹的,判斷溫度是否達到沸點的場景,舉個例子:
var React = require('react'); var ReactDOM = require('react-dom'); var W = require('shadow-widget'); var main = W.$main, utils = W.$utils, ex = W.$ex; var idSetter = W.$idSetter, t = utils.getWTC('*'); function calculatorUI() { var selfComp = null, verdictComp = null; var scaleNames = { c:'Celsius', f:'Fahrenheit' }; function onInputChange(value,oldValue) { var scale = this.parentOf().props.scale || 'c'; // 'c' or 'f' var degree = parseFloat(value) || 0; // take NaN as 0 selfComp.duals.temperature = [scale,degree]; } function calculator__(value,oldValue) { if (value <= 2) { if (value == 1) { // init selfComp = this; this.defineDual('temperature', function(value,oldValue) { if (Array.isArray(value) && verdictComp) { var scale = value[0], degree = value[1]; var isBoil = degree >= (scale == 'c'?100:212); verdictComp.duals['html.'] = isBoil? 'The water would boil.': 'The water would not boil.'; } }); } else if (value == 2) { // mount verdictComp = this.componentOf('verdict'); var field = this.componentOf('field'); var inputComp = field.componentOf('input'); var legend = field.componentOf('legend'); var sScale = field.props.scale || 'c'; legend.duals['html.'] = 'Temperature in ' + scaleNames[sScale]; inputComp.listen('value',onInputChange.bind(inputComp)); selfComp.duals.temperature = [ sScale, parseFloat(inputComp.duals.value) || 0 ]; } else if (value == 0) { // unmount selfComp = verdictComp = null; } return; } } return ( <t.Panel key='panel' width={300} $id__={calculator__}> <t.Fieldset key='field' width={0.9999} scale='c'> <t.Legend key='legend'></t.Legend> <t.Input key='input' type='text' defaultValue='0' /> </t.Fieldset> <t.P key='verdict' width={0.9999} /> </t.Panel> ); } main.$onLoad.push( function() { var bodyComp = W.body.component; var jsx = calculatorUI(); bodyComp.setChild(jsx); });
在 Flux 框架中,由 Store 直接驅動的那個 View 也叫 Controller View
,其下層由各層 View 由 Controller View
往下逐層傳數據來驅動。當 Store + Controller View
的處理邏輯越集中,可理解性就越好,編碼與維護也越容易。反過來,若是 Controller View
的下層節點還處理複雜的控制邏輯,你就不得不將它設計成 "Store + Controller View",程序複雜性無疑會增長。
Shadow Widget 的 "Lift State Up" 比 React 原生方式更好用。
1) 首先,Shadow Widget 有雙源屬性,更多過程處理轉爲對 duals 屬性的讀寫,更簡單,更直接,好比上面 legend
節點,用 legend.duals['html.'] = sDesc
直接改變界面文本,是外掛的,若在 render
函數中 return <t.Legend>{sDesc}</t.Legend>
則是過程處理,是內嵌的,不利於將子節點業務邏輯提高到父節點。
2) 其次,雙源屬性的 listen 機制,也有助於 "Lift State Up",好比上面 inputComp.listen('value',onInputChange)
,在 <input>
輸入框輸入文本將驅動 onInputChange
調用,有關響應函數能輕鬆 "Lift Up"。
3) 還有,idSetter 是函數,函數套函數很容易,很天然,若是下層節點須要處理複雜邏輯,裏層嵌套定義另外一個 idSetter 函數即可。咱們能夠把存在關聯的上下多層節點的邏輯控制代碼,都歸入外層節點的 idSetter 函數中。
在 idSetter 函數中編寫代碼,等效於在 React class 的 render
函數中編碼。調用 render 函數,最後返回 「本節點定義」,idSetter 函數實際在 render()
調用中被調起的,至關於在 render()
入口位置,先調用 idSetter 函數。等效代碼以下所示:
function id__(comp) { // will call idSetter() // comp.state.xxx = xxx; // comp.$gui.comps = xxx; } class NewWTC extends T.BaseWTC_ { // ... render() { id__(this); var tagName = xxx, props = xxx, children = comp.$gui.comps; return React.createElement(tagName,props,children); } }
本處代碼僅爲概要示例,以僞碼方式解釋工做原理,xxx
表示省略過程的處理結果。其中嵌入的 id__
函數會調用前面介紹的 idSetter 函數,只要在 idSetter
函數中本節點屬性更改,及針對子節點的更改被保存,render()
根據修改過的信息生成 React Element,就達到讓 idSetter 中編碼與 render 中編碼等效的目標。
與在 render 中編碼等效的 idSetter 函數舉例:
var fieldWidth = 0.9999; var sTitle = 'Temperature in Celsius'; function fieldset__(value,oldValue) { if (value <= 2) { // ... return; } this.duals.width = fieldWidth; utils.setChildren(this, [ <t.Legend key='legend'>{sTitle}</t.Legend> , <t.Input key='input' type='text' defaultValue='0' /> ]); }
函數 utils.setChildren()
用來設置或更新子節點定義,需注意,各級子節點應指定 key
值,若是不指定,系統會認爲你要建立新節點,而不是更新已存在節點。
儘管在 idSetter 的 value > 2
條件段編碼,等效於在 render 中編程,大部分狀況下咱們沒必要這麼作(也儘可能避免這麼作)。由於 Shadow Widget 的雙源屬性功能很強大,邏輯控制總能分解的,把控制分解到雙源屬性的 setter 函數,或偵聽數據源變化來驅動特定動做,都能實現等效功能。這麼細化分解,代碼更易理解,更好維護。
即使放棄 Shadow Widget 的可視化開發特性,只把它看成一個常規的 lib 庫使用也是頗有價值的,主要表如今兩方面:
過於 「函數式」 對於界面類開發確定很差,好比 UI 設計時,咱們想擺一個文本框,再擺一個按鈕,分解設計的思路是,文本輸入變化了(onChange
)該作什麼,文本框輸入完成(鍵入了回車鍵)該作什麼,按鈕被點擊後該怎麼響應。細化設計的思考過程,是從界面一個個可視的 Component 出發的,是按對象化方式作分析的。無論你的代碼寫成啥樣,設計過程仍離不開一個個對象(如文本框、按鈕等),越是高層設計越是如此。
因爲 React 偏心函數式編程,加上 Flux 強化了數據流設計,容易引導你們一開始就從數據設計入手,着眼於數據如何分解、如何傳遞、如何驅動響應函數等。採用 Shadow Widget 後,產品開發會往面向對象設計拉回一些,把握這一點就容易理解 Shadow Widget 的設計精髓了。
本專欄歷史文章: