React 可視化開發工具 Shadow Widget 非正經入門(之二:分離界面設計)

本系列博文從 Shadow Widget 做者的視角,解釋該框架的設計要點。本篇講解轉義標籤、json-x、投影定義,這幾項與 "如何分離界面設計" 有關。javascript

 

1. 找一個 JSX 替代品

如上一篇 "非正經入門(之一)" 所述,Shadow Widget 要克服 "JSX漿糊" 的不利影響,要找一個 JSX 替代品。html

好比下面 JSX 表達方式:java

return <h1 id={user.id}>Dear {user.name}, {welcomeMsg(user)}</h1>;

等效於:node

return React.createElement( H1, {id:user.id},
    "Dear ", user.name, ", ", welcomeMsg(user)
  );

建立一個 Element,需傳遞三項信息:ReactClasspropschildren 列表。咱們把這三項改形成一個 array 數組格式:react

[ [ReactClass, props],
    child1, child2...
  ]

其中,child1, child2 是子節點定義,格式是 string 字串,或 array 數組。這種以 array 數組表達一個 Element 節點的格式叫 json-x 描述方式,與 JSX 徹底等效。webpack

2. 轉義標籤

爲了方便在 html 網頁文件中描述用戶界面,咱們定義 "轉義標籤" 的表達方式,以下:web

<div $=Panel>
    <div $=P>
      <span $=Span>Referece:</span>
      <span $=A src='http://example.com'>example.com</span>
    </div>
  </div>

轉義標籤無非將全部 HTML 標籤劃分爲行內標籤與 block 標籤,前者用 <span> 表達,後者用 <div> 表達,各標籤中用 $=XXX 屬性定義的方式指示實際使用哪一個 React Class,如 $=Panel 表示用 T.Panel$=P 表示用 T.P。全部經 Shadow Widget 擴展出的構件模板類(也稱 WTC,Widget Template Class)都應註冊到 T 之下,這樣,網頁剛打開時,由轉義標籤中 $=XXX 指示,能找到相應的 React class,實現正常掛載。編程

在轉義標籤中定義的屬性,好比上面的 src='http://example.com',在掛載前先整理出 props 表(如 {src:"http://example.com"}),而轉義標籤的上下級節點的從屬關係,以及同級節點之間的先後關係,指明瞭 json-x 數據中的 children 定義。因此,ReactClasspropschildren 三項信息都有了,轉義標籤能轉換成 json-x,因此它與 JSX 也是等效的。json

轉義標籤具備良好可讀性,因此,它在 *.html 文件中能夠直接書寫。另外,這種格式對搜索引擎也友好,若依賴 JSX 定義界面,搜索引擎沒法分析 html 文件中定義了什麼信息。redux

3. 第一眼是妖孽,多半就是妖孽

React 支持服務側渲染,這個特性彷佛鼓勵了其生態鏈上若干工具額外拓展服務側功能。好比 react-router 中 Router 組件的 history 屬性,既能夠是 browserHistory,也能夠是 hashHistory。對於前者,客戶側路由(即 URL 路徑)決定服務側如何實現,將兩側的設計捆綁起來的,後者 hashHistory(即 #/some/path)徹底在客戶側自主決定,與服務側無關。很顯然,後一方式優於前者,前者違背了軟件設計的 "關注點分離"(Separation of concerns, SOC)原則,而且在實踐上,服務側只有用 webpack-dev-server (加 --history-api-fallback 參數)才能玩得好,不僅綁架用 JS 語言,並且綁架用特定工具。要命的是,react-router 官方竟然推薦你們首選 browserHistory

這個 browserHistory 就是充滿妖氣的特性,怪里怪氣,表面看起來有用,實則禁不起推敲。React 的 propTypes 也很妖,官方讓它存活了這麼久,最終決定在 v15.5 以後棄用,連 context 也不建議用了,context 本是 React 爲緩解跨節點數據共享不便,弄出的不三不四的東西。

某種程度上 React 的服務側渲染也多少沾點 "妖氣",有些人僅爲了解決 SEO 優化用它,仔細想一想有點本末倒置了。它的初始需求源於 google 之類的搜索引擎不認 JSX,由於 JSX 服務於編程,編程腳本原不應由搜索引擎關注的,該關注的只是一些靜態文本。處理靜態文本不必拉上 React 一家子吧?但事實倒是,咱們非得套用一個客戶側編程風格,用 JS 開發的服務側渲染工具,你說妖不妖?

4. 分離界面設計

在分離界面以前,咱們還需創建路徑索引機制。

Shadow Widget 經過一顆樹(Widget 樹,R 樹)管理由它定義的界面,各節點都有 key 值做標識,既能夠顯示指定一個 key 值,也能夠缺省,缺省時由系統自動生成一個數字來表示。這果顆樹的根節點是 ".body",若是根節點下有一個 key 值爲 "toolbar" 的 Panel 節點,它的絕對路徑就是 ".body.toolbar"

有了路徑索引機制,咱們能將界面描述與它的行爲定義分離開了。好比這麼定義界面:

<div $=BodyPanel key='body'>
  <div $=Panel key='toolbar'>
    <div $=P key='p'>
      <span $=Button key='btn1'>Test</span>
    </div>
  </div>
</div>

這麼定義 Test 按鈕的行爲:

main['.body.toolbar.p.btn1'] = {
  $onClick: function(event) {
    alert('clicked');
  },
};

界面的轉義標籤在 *.html 文件中書寫,界面元素的行爲定義在 *.js 文件進行,如此,界面設計分離出來了,界面描述與相關元素的行爲定義經過該元素的絕對路徑實現關聯。如上例,用 javascript 編寫某元素的行爲定義,也稱 "投影定義"。

5. 表達複雜的 props 數據

json-x 數據與轉義標籤都與 JSX 對等,但傳遞 props 數據有若干限制,好比轉義標籤不支持傳遞函數對象,json-x 可傳函數對象,但也不鼓勵(主要由於不規範)。函數定義應在投影類中定義,就像上面舉例的 $onClick 函數,不經過轉義標籤的屬性來傳遞,只在轉義標籤掛載時,到 main 下找到相應投影定義,而後捆綁相應的函數定義。

除了函數,描述複雜的 props 數據時,json-x 的表達能力是完整的,由於它原本就是 javascript 數據,但轉義標籤受 html 標籤格式的影響,要改用 JSON 字串來表示,好比:

<div $=Panel title='tool bar' width='{400}'>
  </div>

屬性值用 '{''}' 括起來,表示它是 JSON 字串,用 JSON.parse 前要先刪掉首尾兩個花括號,如上面 width 值爲 JSON.parse('400')。另外,對於 string 類型的屬性值,能夠直接傳遞(避開字串首尾是花括號的情形),沒必要按 JSON 字串的方式,如上面 title 屬性。

6. idSetter 函數

實施界面與底層分離除了投影定義,還有一種指定 idSetter 函數的方式,若簡單去理解,該方式是投影定義的一個變種,一樣實現特定界面元素的行爲定義的動態綁捆。

舉例來講,界面這麼描述:

<div $=BodyPanel key='body'>
  <div $=Panel key='toolbar'>
    <div $=P>
      <span $=Button $id__='btn1'>Test</span>
    </div>
  </div>
</div>

Javascript 這麼定義:

idSetter['btn1'] = function(value,oldValue) {
  if (value <= 2) {
    if (value == 1) {      // init process
      this.setEvent( {
        $onClick: function(event) {
          alert('clicked');
        },
      });
      // ...
    }
    else if (value == 2) { // did mount
      // ...
    }
    else if (value == 0) { // will unmount
      // ...
    }
    return;
  }

  // render process ...
};

這種書寫方式與上面投影定義的方式是等效的,投影類中該在 getInitialState() 中書寫的代碼,要挪到 idSetter 函數的 if (value == 1) 分支中,該在 componentDidMount() 中書寫的代碼移到 if (value == 2) 的分支中,該在 componentWillUnmount() 中書寫的代碼移到 if (value == 0) 的分支中。

使用 idSetter 函數的優勢是,相應界面節點的絕對路徑沒必要完整定義,即路徑上各段沒必要顯式給出 key 值,系統由 $id__='xxx' 屬性值,自動找出 idSetter 函數。另外一個優勢是,編程風格更加函數式。

7. 創建 W 樹供隨時節點定位

Flux 框架要求節點間數據流向要遵照嚴格的約束,React 不惜犧牲編程便利性,刻意隱藏了內建的那顆虛擬 DOM 樹,致使編程中跨節點調用很是不便,各節點都被一層黑牆包裹,沒法探知周圍都有哪些節點存在,好在 React 爲這個黑牆開了一扇單向玻璃窗:refs,讓父節點能夠引用子節點,子節點引用不了父節點。克服引用不便的解藥是引入 redux 那樣的框架,把存在交叉影響的兩個或多個節點中的數據,提高到一個公共區域去編程。

既然 Shadow Widget 引入 MVVM 框架,在 Component 的 API 層面限制節點間互通已不合時宜,單向數據流應該在更高層面的設計去保證。因此,Shadow Widget 引入了 "W 樹" 的概念,也就是,全部符合規格的 Component 節點(即源於 WTC 類建立的節點)都串接在一顆樹上。樹中各節點都有惟一 "路徑" 標示,節點之間還能夠用 "相對路徑" 或 "絕對路徑" 引用,好比:

this.componentOf('//')   // get parent component
  this.componentOf('//brother')   // brother node
  this.componentOf('sub.child')   // child node
  this.componentOf('./seg.child') // by relative path
  this.componentOf('.body.top.toolbar') // by absolute path

有了 W 樹設計,router 規劃將變得簡單明瞭,比方下圖界面,把兩個可切換的頁 ArticleTalk 裝到一個導航面板(NavPanel)中,若想切換到 Article 頁,按 "/article" 導航,切換另外一頁用 "/talk" 導航。

router

 

(本文完)

本專欄歷史文章:

  1. 介紹一項讓 React 能夠與 Vue 抗衡的技術

  2. React 可視化開發工具 Shadow Widget 非正經入門(之一:React 三宗罪)

相關文章
相關標籤/搜索