本文介紹 "React + Shadow Widget" 應用於通用 GUI 開發的最佳實踐,只聚焦於典型場景下最優開發方法。分上、下兩篇講解,上篇概述最佳實踐,介紹功能塊劃分。javascript
按遵循 ES5 與 ES6+ 區分,Shadow Widget 支持兩種開發方式,一是用 ES5 作開發,二是搭建 Babel 轉譯環境用 ES6+ 作開發,之因此劃分兩大類,由於它們之間差異不只僅是 javascript 代碼轉譯,而是涉及在哪一個層面定義 React Class,進而與源碼在上層仍是下層維護,以及與他人如何協做等相關。css
如本系列博客《shadow-widget 的非可視開發方法》一文介紹,用 ES5 定義 React class 的方式是:html
var MyButton = T.Button._createClass( { getDefaultProps: function() { var props = T.Button.getDefaultProps(); // props.attr = value; return props; }, getInitialState: function() { var state = this._getInitialState(this); // ... return state; } $onClick: function(event) { alert('clicked'); } });
而用 ES6+ 開發,這麼定義 React class:前端
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; } $onClick: function(event) { alert('clicked'); } } var AbstractButton = new MyButton_(); // MyButton_ is WTC var MyButton = AbstractButton._createClass(); // MyButton is React class
因爲 ES6+ 語法能兼容 ES5,因此,即便採用 ES6+ 開發方式,前一種 ES5 的 React class 定義方法仍然適用。但,自定義擴展一個 WTC 類必須用 ES6+,就象上面 "class MyButton_ extends T.Button_"
語法,只能在 ES6+ 下書寫。java
考慮到用 ES5 編程沒必要搭建 Babel 開發環境,ES5 能被 ES6+ 兼容,向 ES6+ 遷移只是總體平移,沒必要改源碼。加上 Shadow Widget 及第 3 方類庫,已提供夠用的基礎 WTC 類(這意味着咱們並不迫切依賴於用 ES6+ 擴展 WTC),因此,咱們將 Shadow Widget 最佳實踐肯定爲:用 ES5 實施主體開發。node
Shadow Widget 最佳開發實踐的大體操做過程以下:react
shadow-widget
庫自身,其上還有 shadow-slide
,pinp-blogs
等擴展庫,各個擴展項目通常會提供它本層的網頁樣板(一般放在 <project>/output/shared/pages/
目錄下)。<script src='your_file.js'></script>
代碼 your_file.js
文件編寫 ES5 代碼。"*.html"
網頁文件中,而後你能夠在 your_file.js
同步編寫 JS 代碼。html, js, css
等文件上傳發布到服務器發佈 js, css, png
等文件位置,或許您需 minify 某個 JS 文件,這些都是前端開發的基本技能,不是 Shadow Widget 特有的。最佳實踐還建議多用 idSetter 函數定義各 component 的行爲,不用(或少用)在 main[path]
定義投影類的方式,由於 idSetter 的函數式風格,讓 MVVM 與 Flux 兩種框架的交匯點處理起來更便利。git
接下來,在展開細節介紹以前,咱們先梳理一下 Shadow Widget 技術體系的幾個特點概念。github
p-state
與 v-state
p-state
與 v-state
是 uglee 在 《少婦白潔系列之 React StateUp Pattern, Explained》 一文提出的概念,咱們借用過來解釋 React 中的數據流轉模式。p-state
指 persistent state,是生命週期超過組件自己的 state 數據,即便組件從 DOM 上銷燬,這些數據仍然須要在組件外部持久化。v-state
指 volatile state,是生命週期和組件同樣的 state 數據,若是組件從 DOM 上銷燬,這些 state 將一塊兒銷燬。編程
結合 Flux 框架,v-state
就是 comp.props.xxx
與 comp.state.xxx
數據,p-state
就是 store 裏的數據,這麼說雖有失嚴謹,但大體如此。若是未使用 Flux 框架,對 comp
的 render()
過程產生影響的全部數據中,全局變量或其它節點(包括上級節點)中的屬性,都算當前節點的 p-state
。
不過,v-state
與 p-state
劃分是靜態的,相對而言的。好比,初始設計界面只要求顯示攝氏度(Celsius)格式的溫度值,而後以爲要適應全球化應用,攝氏度與華氏度(Fahrenheit)都得顯示,再日後發現,Celsius 與 Fahrenheit 並列顯示不夠友好,就改爲動態可配置,取國別信息後自動設成二者中一個。這種設計變遷中,「當前溫度格式」 與 「並列顯示或只顯示一種」 的配置數據常常在 v-state
與 p-state
之間變遷。
React 工具鏈上幾個 Flux 框架主要區別在於,如何定位與使用 p-state
,它們對 v-state
使用基本一致,咱們拿 reflux、redux、shadow-widget 三者分別舉例。
Reflux 採用多 store,其 store 設計與 component 很接近,能夠這麼簡單理解:既然跨 Component 存在數據交互,父子關係能夠用 props
傳遞,非父子關係傳不了,怎麼辦呢?那就設立第三方實體(也就是 store)處理此事。Redux 採用單 store,把它理解成一大坨全局變量就好,它以 action 設計爲提綱,圍繞 action 組織 reducer 函數,而 Reflux 中提綱挈領的東西則是 store 中的數據,圍繞數據組織 action 定義。若對比這二者,Reflux 方式更易理解,需求分解與設計展開過程更人性化,不過,Reflux 沒有突破 React 固有限制,由於多 store 模式,實踐中你們常常很糾結某項數據該放在 component 中,仍是放在 store 中呢?如前所述,一項數據是否爲 v-state
是相對的,產品功能疊代後,數據常常要從 v-state
提高到 p-state
,或者,若原設計偏於寬泛,還需將 p-state
降回 v-state
。Reflux 困境在於 Store 設計與 Component 不對稱,順應來回變遷的成本較高。
Shadow Widget 也是多 Store,Component 自身就是 store,這克服了 Reflux 主要不足。另外結合 MVVM 架構的可視化特色,Shadow Widget 還克服了 redux 主要不足。
Shadow Widget 介紹了一種 「逆向同步 & 單向依賴」 的機制,在以下節點樹中,nodeE 要使用 nodeC 中的數據,但 nodeC 生存週期與 nodeE 並不一致,因此,引入一種機制,在它們共同的父節點 nodeA 設置一個屬性(好比 attrX
),nodeC 中的該數據能自動同步到 nodeA 中,而後讓 nodeE 只依賴 nodeA 中的數據(好比 attrX
),只要 NodeE 還存活,父節點 nodeD 與 nodeA 必然存活。
nodeA +-- nodeB | +-- nodeC +-- nodeD | +-- nodeE
React 官方介紹了一種 "Lifting State Up" 方法,藉助函數式編程的特色,把控制界面顯示效果的變量,從子節點提高到父節點,子節點的事件函數改在父節點定義,就達到 Lift State Up
的效果。
既然提高 state 能突破 React 對數據傳遞的限制,那麼,極端一點,可否把全部用到的數據都改爲全局變量呢?答案固然能夠,不過缺乏意義,這麼作,無非將分散在各節點的邏輯,轉移處處理一堆全局變量而己,設計過程本該分解,而非合併。可視節點分層分佈本是自然的功能劃分方式,放棄它改換門庭無疑把事情搞複雜了,可惡的 Redux 就是這麼幹的。
從本質上看,Redux 把 state 數據全局化了(成爲單 store),但它又以 action 主導切割數據,你並不能直接存取全局 store,而是改由 action 驅動各個 reducer,各 reducer 只孤立處理它自身可見的 state。由此我有兩點推論:
爲方便說明問題,咱們取 React 官方 "Lifting State Up" 一文介紹的,判斷溫度是否達到沸點的應用場景,編寫一段樣例代碼。
咱們想設計以下界面:
若是輸入溫度未超沸點,界面顯示 "The water would not boil"
,若超沸點則顯示 "would boil"
。另外,用於輸入溫度的方框(即後述的 field
節點)要求可配置,用 scale='c'
指示以攝氏度表示,標題提示 "Temperature in Celsius"
,不然 scal='f'
指示華氏度,提示 "in Fahrenheit"
。
咱們在 Shadow Widget 可視設計器中完成設計,存盤後生成的轉義標籤以下:
<div $=BodyPanel key='body' klass='S5'> <div $=Panel key='calculator' klass='hidden-visible-auto row-reverse' height='{null}' width='{300}' $id__='calculator'> <div $=Fieldset key='field' width='{0.9999}' scale='c'> <span $=Legend key='legend'>legend</span> <span $=Input key='input' type='text' default-value='0'></span> </div> <div $=P key='verdict' klass='visible-auto-hidden' width='{0.9999}'></div> </div> </div>
而後在 JS 文件編寫以下代碼:
if (!window.W) { window.W = new Array(); W.$modules = [];} W.$modules.push( function(require,module,exports) { 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; if (W.__design__) return; (function() { // functionarity block var selfComp = null, verdictComp = null; var scaleNames = { c:'Celsius', f:'Fahrenheit' }; idSetter['calculator'] = function(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)); this.duals.temperature = [ sScale, parseFloat(inputComp.duals.value) || 0 ]; } else if (value == 0) { // unmount selfComp = verdictComp = null; } return; } 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]; } }; })(); });
上面 if (W.__design__) return
一句,讓其後代碼在 __design__
態時(即,在可視設計器中)不生效。
按咱們最佳實踐的作法,界面可視化設計的結果保存在頁面 *.html
文件,而界面的代碼實現(包括定義事件響應、綁捆數據驅動等)在 JS 文件編寫。因此,上面例子的設計結果包括兩部分:*.html
文件中的轉義標籤與 *.js
文件中的 javascript 腳本。
多個組件共同完成某項特定功能,他們合起來造成邏輯上的總體叫作 「功能塊」 (Functionarity Block)。典型的 JS 文件一般按這個樣式編寫:
if (!window.W) { window.W = new Array(); W.$modules = [];} W.$modules.push( function(require,module,exports) { // 全局變量定義 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; if (W.__design__) return; // 功能塊定義 (function() { // .... })() // 初始化定義 main.$onLoad.push( function() { // ... }); });
頭部用來定義若干全局變量,而後定義功能塊,功能塊可能有多個,上面舉例的判斷溫度是否超沸點,比較簡單,定義一個功能塊就夠了,最後定義 main.$onLoad
全局初始化函數。
之因此將一個功能塊用一個函數包裹,主要爲了構造獨立的命名空間(Namespace),好比前面舉例的代碼:
(function() { // functionarity block var selfComp = null, verdictComp = null; var scaleNames = { c:'Celsius', f:'Fahrenheit' }; idSetter['calculator'] = function(value,oldValue) { // ... }; })();
由功能塊函數構造的 Namespace 也稱 「功能塊空間」(Functionarity Block Space),在功能塊內共享的變量在此定義,好比這裏的 selfComp, verdictComp, scaleNames
變量。
一個功能塊的入口節點是特殊節點,它的生存週期反映了功能塊的生存週期。它的各層子節點若還存在(即在 unmount 以前),入口節點必然存在。由於入口節點的生存期能完整覆蓋它各級子節點的生存期,因此,咱們通常在入口節點定義 idSetter 函數,承擔本功能塊的主體邏輯處理。
上例的功能塊定義了以下節點樹:
Panel (key=calculator) +-- Fieldset (key=field) | +-- Legend (key=legend) | +-- Input (key=input) +-- P (key=verdict)
入口節點是 calculator
面板,結合該節點的 idSetter 函數書寫特色,咱們接着介紹 Shadow Widget 最佳實踐如何處理 "功能塊" 以內的編程。
1) 爲方便編程,不妨在 「功能塊空間」 多定義變量
由於 「功能塊空間」 的變量不外泄到其它功能塊,咱們沒必要擔憂多定義變量會給其它部分編碼帶來 Side Effects。功能塊裏各個節點,只要不是動態建立、刪除、再建立那種,均可定義成 「功能塊空間」 的變量,咱們通常在入口節點 idSetter 函數的 unmount 代碼段(即 if (value == 0)
),把各個節點的變量置回 null
值。
對於動態增刪的節點,不妨用 this.componentOf(sPath)
動態方式定位。
2) 功能塊內的數據主體流向,宜在界面設計時就指定
在功能塊的 idSetter 函數也能以編程方式設計節點間數據流向,考慮到界面設計與數據流規則直接相關,能以描述方式(轉義標籤形式)表達數據流的,儘可能用描述方式,不方便的才用 JS 編程方式去實現。由於,一方面,Shadow Widget 的指令式 UI 描述能力夠強,另外一方面,這麼作有助於讓 MVVM 中的 ViewModel
集中,從而下降設計複雜度。
界面設計時,不妨多用下述技巧:
$for=''
或 $$for=''
開啓一層 callspace,方便其下節點的可計算屬性用 duals.attr
引用數據。$trigger
同步數據NavPanel
與 NavDiv
),用 "./xx.xx"
相對路徑方式讓節點定位更方便
3) 善用變量共享機制
若按 React 原始開發方式編碼,不借助任何 Flux 框架工具,你們確定以爲編程很不方便,由於各節點除了能往子節點單向傳遞 props
外,與其它節點的交互幾乎隔了一道黑幕。然而,不幸的是,React 幾個主流的 Flux 工具,均沒有妥善解決幾個主要問題,上面提到的 Reflux、Redux 均如此,React 官方的 react-flux
更難用。
相對而言,Shadow Widget 的解決方案好不少,一方面,在 Component 節點引入 「雙源屬性」,功能強大,能讓基於過程組裝的 UI 渲染,過渡到 以屬性變化來驅動渲染,即:除了 「功能塊」 的入口節點需集中編寫控制邏輯,其它節點的編程,基本簡化爲定製若干 duals 函數(用 defineDual()
註冊)。另外一方面,Shadow Widget 藉助 Functionarity Block 抽象層來重組數據,以功能遠近做聚合依據,明顯比以 Action 驅動的 Reducer 分割要高明。
從本質上講,拎取 「功能塊抽象層」 也是 Lift State Up
的一種手段,限制更少,結合於 JS 編程也更天然。虛擬 DOM 樹中的各 component 節點有隔離措拖,不能互相識別,但函數編程沒什麼限制,好比上面例子,selfComp = this
把一個 Component 賦給 「功能塊空間」 的變量 selfComp
後,同在一個功能塊的其它函數都能使用它了。
(未完,下篇待續...)