React 可視化開發工具 shadow-widget 最佳實踐(上)

本文介紹 "React + Shadow Widget" 應用於通用 GUI 開發的最佳實踐,只聚焦於典型場景下最優開發方法。分上、下兩篇講解,上篇概述最佳實踐,介紹功能塊劃分。javascript

Thumbnail

1. 最佳實踐概述

按遵循 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

  1. 建立一個新的工程,參見《Shadow Widget 用戶手冊》(下面簡稱《手冊》)中 「5.1.1 建立工程」 一節
    應選擇一個合適的 "網頁樣板" 來建立,Shadow Widget 是一個可繼承重用的 lib 庫體系,最基礎的是 shadow-widget 庫自身,其上還有 shadow-slidepinp-blogs 等擴展庫,各個擴展項目通常會提供它本層的網頁樣板(一般放在 <project>/output/shared/pages/ 目錄下)。
  2. 在建立的網頁文件追加 <script src='your_file.js'></script> 代碼
    而後在 your_file.js 文件編寫 ES5 代碼。
  3. 使用 Shadow Widget 的可視設計器設計用戶界面
    用戶界面設計的結果以轉義標籤的形式,保存在你的 "*.html" 網頁文件中,而後你能夠在 your_file.js 同步編寫 JS 代碼。
  4. 完成開發與測試後,把相關的 html, js, css 等文件上傳發布到服務器發佈
    由於沒必要作 ES6 轉譯,發佈操做很直接。或許您要調整 js, css, png 等文件位置,或許您需 minify 某個 JS 文件,這些都是前端開發的基本技能,不是 Shadow Widget 特有的。

最佳實踐還建議多用 idSetter 函數定義各 component 的行爲,不用(或少用)在 main[path] 定義投影類的方式,由於 idSetter 的函數式風格,讓 MVVM 與 Flux 兩種框架的交匯點處理起來更便利。git

接下來,在展開細節介紹以前,咱們先梳理一下 Shadow Widget 技術體系的幾個特點概念。github

2. p-statev-state

p-statev-state 是 uglee 在 《少婦白潔系列之 React StateUp Pattern, Explained》 一文提出的概念,咱們借用過來解釋 React 中的數據流轉模式。p-statepersistent state,是生命週期超過組件自己的 state 數據,即便組件從 DOM 上銷燬,這些數據仍然須要在組件外部持久化。v-statevolatile state,是生命週期和組件同樣的 state 數據,若是組件從 DOM 上銷燬,這些 state 將一塊兒銷燬。編程

結合 Flux 框架,v-state 就是 comp.props.xxxcomp.state.xxx 數據,p-state 就是 store 裏的數據,這麼說雖有失嚴謹,但大體如此。若是未使用 Flux 框架,對 comprender() 過程產生影響的全部數據中,全局變量或其它節點(包括上級節點)中的屬性,都算當前節點的 p-state

不過,v-statep-state 劃分是靜態的,相對而言的。好比,初始設計界面只要求顯示攝氏度(Celsius)格式的溫度值,而後以爲要適應全球化應用,攝氏度與華氏度(Fahrenheit)都得顯示,再日後發現,Celsius 與 Fahrenheit 並列顯示不夠友好,就改爲動態可配置,取國別信息後自動設成二者中一個。這種設計變遷中,「當前溫度格式」 與 「並列顯示或只顯示一種」 的配置數據常常在 v-statep-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 主要不足。

3. 幾種 Lift State Up 方式

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。由此我有兩點推論:

  1. 棄用界面現成的分解方式,改建另外一套體系並不明智
    就像描述雙人博擊,最直接的方式是先區分場上誰是誰,誰出擊,誰防守,出擊者揮拳,防守者縮頭躲避。Redux 行事風格是先設計 「揮拳」、「縮頭」 之類的 action,而後分解實施這些 action,來驅動各類 state 變化。該模式之因此行得通,不是 Redux 有多好,而是人腦太奇妙,編程中除了腦補產品應用場景,偶爾還會插幀處理俊男靚女圖片 :)
  2. 數據隔離是必需的,不然沒法應對大規模產品開發
    後文咱們將介紹最佳實踐中的數據隔離方法,以功能場景爲依據。

4. 功能塊

爲方便說明問題,咱們取 React 官方 "Lifting State Up" 一文介紹的,判斷溫度是否達到沸點的應用場景,編寫一段樣例代碼。

咱們想設計以下界面:

Temperature

4.1 樣例程序的功能

若是輸入溫度未超沸點,界面顯示 "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__ 態時(即,在可視設計器中)不生效。

4.2 功能塊

按咱們最佳實踐的作法,界面可視化設計的結果保存在頁面 *.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 變量。

4.3 功能塊入口節點

一個功能塊的入口節點是特殊節點,它的生存週期反映了功能塊的生存週期。它的各層子節點若還存在(即在 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 集中,從而下降設計複雜度。

界面設計時,不妨多用下述技巧:

  1. $for=''$$for='' 開啓一層 callspace,方便其下節點的可計算屬性用 duals.attr 引用數據。
  2. 善用 $trigger 同步數據
  3. 若是節點層次複雜,不妨採用導航面板(NavPanelNavDiv),用 "./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 後,同在一個功能塊的其它函數都能使用它了。

(未完,下篇待續...)

相關文章
相關標籤/搜索