重磅:前端 MVVM 與 FRP 的升階實踐 —— ReRest 可視化編程

ReRest (Reactive Resource State Transfer) 是前端開發領域新興的方法論體系,它繼承了 MVVM 與 FRP 編程理念,在技術上有很多創新。本文從專利稿修改而來,主要介紹 ReRest 原理與若干實踐經驗。javascript

thumbnail

 

說明:文章做者受權任何組織或我的,在不更改原文內容(包括本段)的前提下,能夠自由轉載本文。點擊下載本文 PDF 格式html

 

1. 前言

前陣子 React 附加專利條件的開源協議鬧得沸沸揚揚,國內外有多家大公司開始棄用 React,咱們也深感困惑,是否該將 shadow-widget 全盤改寫,很猶豫。讓底層脫離 React。但專利與開源協議是平行的兩個世界,改底層也不大容易解決問題。Facebook 擁有虛擬 DOM 方面的專利,preact、vue 均可能涉嫌侵權,經過修改底層代碼來規避仍是挺難的。前端

後來咱們決定本身申請專利,以便從此萬一用到,手頭有個專利可爲 shadow-widget 增長話語權。當權利要求書完稿時,Facebook 宣佈 React 迴歸真正的 MIT 開源協議了,真是大喜訊!咱們沒必要擔憂專利的風險了,爲自家申請專利再也不必要 —— 咱們建立 shadow-widget 技術平臺,但無心藉此盈利,源碼開放出來讓你們都受益。(PS:沒必要感謝,若是以爲這項目對您有用,上 github 爲咱們加星吧vue

本文從專利申請稿改寫而來,內容有壓縮,要不文章太長了,另外還增長了可視化編程實踐相關的若干內容。公佈此文還有一個目的,防止他人偷偷拿咱們的技術申請專利,若是之後真發現有人這麼幹了,本文是憑證,你們能夠提請專利無效,把別人的保護條款廢掉。java

說明:本文完稿時,Shadow Widget 最新版本爲 v1.1.2,產品用戶手冊對技術實現有更詳細介紹。node

2. 背景

近些年 Web 前端技術發展,能夠說是框架橫飛的時代,雖然十年前網頁還正常能打開,IE 仍是那個頑固的 IE,但前端開發卻已經歷翻天覆地的變化。近來比較搶眼的是 React 框架,Facebook 開創性的實踐了兩種技術:虛擬 DOM 與 Functional Reactive Programming(FRP,函數式響應型編程),這兩種技術幾乎已成現代前端框架的標準配置。react

Facebook 在虛擬 DOM 上原創較多,鑽研很深刻,這項技術也能夠說很成熟了。FRP 在 React 的實現就是那個 FLUX 框架,它不是 Facebook 獨創,在 React 中用起來也有點磕磕碰碰,尤爲在調和指令式風格與函數式風格方面,並不暢。git

另外,儘管十年來 Web 開發技術發展很快,但在可視化開發方面仍然進展緩慢,全部主流框架都在界面的形式化描述上作文章,Angular 與 Vue 擴展了標籤屬性,增長很多控制指令,React 則全盤引入 JSX 描述方式,他們無一例外的都要求你們,一行行寫腳本去定義界面,而不是 20 年前在 Delphi 與 VB 就已出現的可視化、所見即所得的開發方式。github

本文所提的 ReRest 編程方法,是適應 Web 可視化開發要求,融合虛擬 DOM 與 FRP 技術,並克服它們應用於主流框架的若干不足,而提出的通用型解決方案。ReRest 方法在 shadow-widget 平臺有一些實踐,已取得良好效果。ajax

3. ReRest 要點

ReRest 全稱爲 REactive REsource State Transfer,譯爲 「響應式資源狀態遷移」,與本概念相關的提法還有:

  1. ReRest framework,ReRest 框架
  2. ReRest based programming,基於 ReRest 的編程
  3. ReRest-ful design,ReRest 風格設計

光從字面上看,「響應式資源狀態遷移」 不大好理解,就像縮寫爲 REST 的 「Representational State Transfer」,表現層狀態轉移,只看文字,是不大容易搞清楚講的是啥。

ReRest 提倡以 「資源」 的觀點展開設計,將針對資源的操做規格化,統一抽象成 4 類操做,在程序開發過程當中,可視界面的功能塊分解設計是一個維度,基於資源狀態變遷所帶來的單向數據流,構成另外一個維度,兩個維度共同造成一個正交矩陣,這種開發方式有效平衡了指令式與函數式兩種設計風格,集二者優點於一身。

ReRest 理念與 REST 有某種類似性。REST 核心含義是用 URL 定位資源,用 HTTP 動詞描述操做,它要求服務側提供的 RESTful API 中,只使用名詞來指定資源,原則上不使用動詞,「資源」 概念能夠說是 REST 架構的處理核心,針對資源的操做有 GET, POST, PUT, DELETE 等 HTTP 動詞。在 ReRest 框架中,界面可視控件的屬性數據視做資源,依據 shadow-widget 實踐,「資源(Resource)」 則指 React Component 的屬性數據。

REST API

理解基於 ReRest 的編程,須把握兩個重點:Component 管界面呈現,Resource 管數據流。前者適用靜態思惟,更偏指令式風格,後者適用動態思惟,更偏函數式風格。

4. 兩種思惟模式

主流的前端框架一直並存靜態與動態兩種思惟模式,舉例來講,Vue 與 Angular 更多采用靜態思惟模式,界面是可描述的,React 更多的用動態思惟,界面是可編程的,JSX 看上去也是一種表述形式,但它本質是一段 javascript 代碼,你很難將它 「去編程化」 —— 把 JSX 從上下文環境摳出來獨立使用,事情將變得毫無心義。

咱們沒必要爭論這兩種模式孰優孰劣,二者都有顯著優勢。F.S.菲茨傑拉德曾說:檢驗一流智力的標準,就是頭腦中能同時存在兩種相反的想法,但仍保持行動能力。

何況,在前端開發中,該採用靜態思惟或動態思惟的條件還算清晰。好比開發一個網頁,大塊功能的界面設計應採用靜態思惟,比方,在頂部放一個工具條,左側放導航,中間放內容;簡單界面設計應以靜態思惟爲主,由於界面組件不多動態替換;而應對複雜功能,應以動態思惟爲主,既然 JS 代碼能夠控制一切,局部界面用 JSX 定義會很爽。越是動態變化的界面,應該越傾向於用動態的、編程性思惟。

Angule 靜態思惟太重,React 動態思惟太重,都很差,Vue 從靜態走向動態,易用且適應複雜變化,應該說它正前進在正確道路上,只是,Vue 兼容兩種風格並不是一開始就統籌規劃了,工具複雜性不容易降下來。

5. 從 FRP 到 FLUX,再到 ReRest

ReRest 在前人已有經驗基礎上,提出更優方法,而後驗證,結合實踐再調整、優化,React 生態鏈上系列工具的實踐是其中最重要的經驗基礎。

若是隻把 React 看做虛擬 DOM 庫,它無疑是一項偉大的發明,做爲 DOM 節點對應物,可按任意方式使用它。你徹底能夠在 React 基礎上擴展出像 Vue 那樣的指令式描述系統,甚到回退到 jQuery 方式也行(偷偷告訴你一個關鍵點,用 node.__reactInternalInstance$XXX 能反查 React Component),用 React 搭建 MVVM 也徹底可能,React 團隊在 SoC(關注度分離)方面分寸把握得很好。

React 工具鏈廣泛聽從濃重的函數式編程風格,從函數式拓展命令式較爲容易,但反過來就困可貴多。就像許多編程語言,都從 LISP 普系吸取養分,相對來講,函數式編程更反映事物的本原,今後出發更容易理順具備複雜關係的框架系統。

因爲上面緣由,ReRest 的實踐性探索從 React 開始,而不是 Vue 或其它工具。

5.1 理解 React 的 FRP 機制

FRP 是響應式編程一種範式,由不斷變化的數據驅動界面持續更新,界面更新中,或用戶操做(如鼠標點擊)中又產生新的數據流,再驅動界面更新,如此循環往復。觸發界面更新的數據流也稱事件流,由於它的行爲方式有一些限定,不是常規數據流動,它至少要求單流向、細粒度、按 tick 觸發。

咱們不妨把網頁界面的更新過程,理解成衆多 「驅動更新的時間片」 的集合,一個時間片稱爲一個 tick,各 tick 可能先後緊挨着,但兩個 tick 之間至少都有 「調度間隙」。就像下面 process2 函數緊隨 process1 執行,用 setTimeout(process2,0) 延時 0 秒,這兩函數之間就產生 「調度間隙」 了。

function process1() {
  console.log('in process1');

  setTimeout( function process2() {
    console.log('in process2');
  },0);
}

數據變化致使界面更新(即 React 的 render() 調用),界面更新又觸發數據變化,若是沒有調度間隙,系統可能陷入無限遞歸,遞歸結果必然爆棧。React 的 FLUX 框架首先要讓數據單向流動,只要有 「調度間隙」 區隔,即便數據變化與界面更新無限制的互爲觸發,都算單向流動。

React 以兩種機制保障數據單向流動,一是讓 props 只讀,二是 setState() 延後一個調度間隙執行。後者好理解,前者 「props 只讀」 是間接生效的,由於 props 與 state 同時決定 Component 界面如何表現,但更改 props 屬性只能在父節點的 render() 函數中進行,你得用 ownerComp.setState() 觸發父節點再次 render(),因此,無論你怎麼用,都會插入 「調度間隙」 的。

此外,React 要求在 shouldComponentUpdate() 中結合各屬性的 immutable 是否變化,判斷是否該觸發 render 更新。總之,上述機制支持了 FRP 編程如下要求:按時間切片驅動界面更新,各切片保持細粒度,讓每次更新最小化、無關聯。

5.2 改造雙源驅動

由父節點決定如何更新的 props.attr,與節點本身就能決定的 state.attr,二者共同定義 Component 的界面表現,因此 props 與 state 合稱爲 「雙源」,只是原生 React 是 「隱式雙源」,ReRest 框架要把它改形成 「顯式雙源」。

實現原理大體以下:

  1. 引入一個與 props.attrstate.attr 對等的集合:duals.attr
    該集合中的 attrprops.attr 自動記錄到 state.attr,經過 duals.attr 讀寫接口,可等效實現對相應 state.attr 的存取,即:讀 duals.attr 等效於讀 state.attr,寫操做 duals.attr = value 等效於執行 this.setState({attr:value})
  2. 提供 this.defineDual() 讓用戶手工註冊 duals 屬性
    系統還將傳給標籤內置屬性(如 name,href,src 等)自動註冊爲 duals 屬性,此舉方便了編程,不然大量屬性手工編碼去註冊很麻煩。
  3. defineDual() 實現 setter 回調的捆綁
    好比調用 this.defineDual('a',setter) 註冊後,對它賦值 this.duals.a = value,將自動觸發 setter(value,oldValue) 回調。

經上述改造,更改 Component 自身的 props 就沒必要繞轉到父節點去作了,好比,用相似comp.duals.name = 'new_name' 語句直接賦值就好。

這麼變更將帶來一個重大影響:上層 FLUX 機制能夠捊直了作。如何實現 FLUX,官方給出了框架建議,React 說我只管虛擬 DOM,如何搭 FLUX 是上層的事,Redux 說,我來管這事,增長 action,增長 reducer,增長 store,不過異步的事你本身解決。什麼是 action 呢?就是事件化數據,什麼是 reducer 呢?就是事件處理函數,什麼是 store 呢?那個 Component 限制了數據讀寫,還搞不清關聯子節點、父節點在哪,自個弄一數據集就是 stroe。結果,Redux 繞了很大一個彎,說把事情解決了,但用戶仍報怨寫異步很難受呀,這麼繞的東西不難受就鬼了!

ReRest 的對策很簡單,最直接。事件化數據就是可偵聽的 duals 屬性嘛,事件處理函數就是 duals 的 setter 回調,理不清父子從屬關係,就弄一個 W 樹吧,把各節點串起來,用 this.componentOf() 按相對路徑(或絕對路徑)直接找,至於 store,哪有必要,Component 自身就是 store 嘛!

5.3 資源化

ReRest 嘗試讓 Web 開發迴歸事物本原,網頁開發主要處理兩樣東西:開發界面、與服務器交換數據,它與 Delphi、Qt 等 GUI 開發工具不應有太大差異,爲何 React 就不能支持 MVVM 呢?MVC 難以適應標籤化的界面表達形式,但用 MVVM 是沒問題的。

常規所見即所得開發工具,界面設計的主體過程是:拖入一個樣板建立界面組件,選中它對修改某些屬性,再拖入樣板建立其它組件,設屬性,重複操做直至組裝出複雜界面。外觀設計差很少就這些,剩下工做主要是功能實現,實現相似如何接收鍵盤輸入,如何響應按鈕點擊等函數定義。

原生 React 之因此離常規可視化設計很遠,主要是 Component 屬性成員級別的設計還不夠好,少一層可靜態依賴的錨點,過早套上高度動態變遷的事件流了,全部東西都動態變化,可視設計是沒法支持的。在 ReRest 設計理念中,凡 Component 屬性中公開供控制,或供配置的,都應視做 「資源」,「資源」 是靜態化的概念,就像 RESTful 要求 URL 要用名詞表達資源,動做統一由 HTTP 的 GET, POST, PUT 等表達同樣,將 Component 屬性 「資源化」,纔是問題解決之道。

就 shadow-widget 已有實踐而言,ReRest 所謂的資源,專指 Component 的靜態屬性(即 comp.props.attr)與雙源屬性(即 comp.duals.attr

React 對 Component 渲染組裝在 render 函數中完成,組裝過程是一段 JS 代碼,由於 JS 代碼能夠任意書寫,如何組裝會很是靈活。而靈活是一把雙刃劍,功能雖然強大了,但缺乏穩定形態,對創建 MVVM 框架與可視化開發都不利。

ReRest 但願將渲染過程,改形成開發主體依賴於對 「資源」 的操做,固然,這裏的 「資源」 是動做化了的,也就是,讀寫資源會自動觸發預設的關聯動做。換一句話來講,ReRest 想把 render 函數改形成一種固定格式,沒必要再經過寫一段過程代碼實施控制,而改爲對若干 duals.attr 讀寫,以此驅動渲染過程的定製處理。

ReRest 對渲染的 「資源化」 改造過程,本質是將過程控制邏輯,挪到 「資源」 附屬的動做函數中書寫。

5.4 渲染臨界區

以下示例:

01 render() {
02   // 進入渲染臨界區
03   渲染臨界區的過程處理 ...
04   // 退出渲染臨界區
05 
06   固定程式的其它 render 處理 ...
07 }

「渲染臨界區」 (Rendering Critical Section) 中的代碼(上面 03 行)用來驅動本 Component 各個 duals.attr 附屬動做。上面 06 行,讓附屬動做處理後的結果生效,完成渲染輸出。經此改造,用戶沒必要再定義各 Component 的 render() 函數。

在 「渲染臨界區」 執行的代碼有特別要求,其一,render 函數由於由 React 內核發起,使用有一些限制,好比 render 過程當中再次觸發更新、用 ReactDOM.findDOMNode 查找 node 節點等,不過,隨着 React 版本優化,這些限制逐漸變少(好比目前版本在 render 函數中調用 findDOMNode 再也不報錯了)。其二,ReRest 資源的行爲函數如何被調用,在臨界區中與臨界區外有差異,下文立刻介紹。

5.5 資源的行爲定義

ReRest 區分兩類資源,只讀資源(即 comp.props.attr)與可寫資源(即 comp.duals.attr),對於前者,在 Component 生存週期內,只支持 「讀」 行爲,而對於後者,支持讀、寫、setter 處理、listen 處理共 4 種行爲。

4 種行爲

這 4 種行爲含義以下:


  1. props.attr 直接讀取,或從 duals.attr 讀取由系統返回 state.attr 的值。

  2. duals.attr 賦值,系統除了把值賦給 state.attr 外,還觸發相應的 「setter處理」 與 「listen處理」。
  3. setter 處理
    這個 setter 就是 defineDual(attr,setter)setter 回調函數。對於同一 Component 的同一 attr,能夠調用屢次 defineDual() 註冊多個回調函數,給 attr 賦值後,各回調函數依次被調,調用順序與註冊順序相同。
  4. listen 處理
    對一個已存在 comp.duals.attr,可調用 comp.listen(attr,fn) 登記一項偵聽,當 attr 值發生變化後,系統會自動調用 fn(value,oldValue)。同一 comp.duals.attr 支持在多處偵聽,咱們能夠爲兩個(或多個) duals.attr 創建偵聽關聯,一處更新,其它地方也聯動更新。

setter 處理與 listen 處理的適用場合有明顯差異,setter 函數只在渲染臨界區的處理過程當中被調用,listen 函數在觸發後(即更改 duals 屬性值)一定延後一個 「調度間隙」 才被執行,因此它必然不在任何節點的渲染臨界區內執行。

在同一節點的渲染臨界區內,setter 函數可被連續調用,當前節點中不一樣 duals.attr 的 setter,或同一 attr 的 setter 可串連執行,這意味着,臨界區內對當前節點 duals.attr 賦值可能會引起遞歸重入,各次 setter 調用之間沒有 「調度間隙」 區隔。好比對 comp1.duals.attr1 修改,致使 comp1.duals.attr2comp2.duals.attr3 修改,而 comp1.duals.attr2 修改可能再致使 comp1.duals.attr1 修改,這時對 comp1.duals.attr1 賦值可能致使該屬性的 setter 函數遞歸調用,而引起的 comp2.duals.attr3 更改倒是延後一個 「調度間隙」 的,由於 comp2 的雙源屬性 setter 函數將在 comp2 的臨界區被調用。

setter 與 listen 處理反映了兩類資源聯動的需求,常規狀況下,隔一個 「調度間隙」 可確保數據單向流動,而特殊狀況下,對於緊密相關的資源聯動,若是總有 「調度間隙」 隔着,顯然會影響運行效率,上述機制保留了重入式 setter 回調是有意義的。

6. 範式變換

Redux 是 React 生態鏈中提供 FLUX 框架的一個典型工具,有表明性,接下來介紹範式變換與它有關。

Redux 以 「Action」 的觀點展開設計(其它 FLUX 工具也大都如此),ReRest 則要求以 「Resource」 的觀點展開設計,Action 是動態的動做,Resource 是靜態的資源,二者差異可用 「非 RESTful」 風格與「RESTful」 的差異來類比。基於這兩種觀點的設計存在範式變換關係,下面咱們用 Redux 與 shadow-widget 的 FLUX 實現差別爲例,展開說明。

6.1 單 Store 變多 Store

拿 Redux 用戶手冊提到的 Todo 例子來講,增長一條 todo 記錄,基於 Action 觀點會先設計一個 Action 定義:

const ADD_TODO = 'ADD_TODO';

var actTodo = {
  type: ADD_TODO,
  text: 'Build my first Redux app'
};

而後,設計一個 reducer 響應這個 Action:

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [ ...state, {
        text: action.text,
        completed: false,
      }];
    // ...
  }
  // ...
}

Redux 採用單一的大 Store 結構,ReRest 要求的資源倒是小數據,至關於把 Redux 的大 Store 分割成許多小塊,一個小塊就是一個資源。針對 todo 列表,資源項用 duals.todoList 表示,指定它的初值是空數組。

this.defineDual('todoList',null,[]);

而後以下代碼添加一條 todo 記錄,就對等實現了上述 reducer 功能:

utils.update(this,'todoList', {$push: [{
  text: 'Build my first Redux app',
  completed: false,
}]});

ReRest 的 Store 具有兩個特色:

  1. 採用多 Store(與 reflux 相似),Store 實體與 Component 重合。
  2. 因爲數據流動設計針對 Component 下的屬性展開,爲方便理解,ReRest 的 Store 也可視爲雙層結構,第一層是 Component 實體,第二層是 Component 下視做 resource 的屬性定義,包括 props.attrduals.attr

Component 下的 resource,本質是數據,與 Store 同屬一類,Redux 的 reducer 定義,對應 ReRest 變成 4 種資源行爲定義(讀、寫、setter、listen),而 Redux 的 Action 則弱化成一條操做資源的常規語句。強調一句,Redux 設計用 Action 提綱挈領,ReRest 設計用 Resource 提綱挈領,弱化 Action 是很天然的事,由於相關操做能夠隨時添加,抓住數據定義纔是核心本質。Redux 編程中,給 Action 指定一個常量名,再定義 Action 結構,而後用 switch..case 處處判斷 action.type,就沒人以爲煩嗎?

6.2 數據定義用做事件

偵聽一個 duals.attr 後,偵聽函數就是事件處理函數,FLUX 框架要求的 Dispatcher 能夠簡化,好比咱們用 duals.receivedData = data 表示接收到外部一條指令,對它賦值即觸發偵聽它的事件處理函數立刻被調。

若是對 duals.receivedData 賦值時,新舊值沒有變化,系統將忽略觸發偵聽函數。要是不想忽略,調整一下數據定義,好比用 duals.receivedData = [data,ex.time()],加一個時間戳,就保證每次對 duals.receivedData 賦值,都能觸發偵聽函數了。

儘管 ReRest 聚焦於如何配置資源,duals.attr 的組織形式很簡單,卻完整支持事件流機制,包括多源頭偵聽,等所有事件來齊後再觸發回調函數,例如:

utils.waitAll(comp1,'attr1',comp2,'attr2', function(value1,value2) {
  // do something ...
});

6.3 渲染器

若是一個節點的結構比較穩定,好比它渲染輸出的標籤名不變,其子節點構成也不變,這時,對該節點的屬性作 「資源化」 改造很容易。但若是節點結構不穩定,好比,有時單節點,隨時變爲多層節點,甚至有時輸出的標籤名也在變。咱們還得另尋方法實現資源化定義,解決對策即是 「渲染器」。

在 React 中內容組裝在 render() 函數進行,一般由 comp.setState() 驅動render() 函數反覆調用。render 是動做,按資源化方式理解,把它變名詞,是 rendering,就是渲染器,咱們假想 render() 由一個渲染器驅動,渲染器內部用一個計數器(記爲 id__)控制渲染刷新,好比:comp.duals.id__ = 2 賦值致使 render() 被調用,運行 comp.setState({attr:value}) 也促使 render() 調用,並且 id__ 會自動取新值。也就是說,每次 render() 運行,渲染器的計數器都會自動取不一樣值,等效於執行 duals.id__ = value 語句。

按以下方式註冊 duals.id__

01 this.defineDual('id__', function(value,oldValue) {
02   // this.state['tagName.'] = 'div';
03   // this.state.attr1 = xxx;
04   // this.duals.attr2 = xxx;
05   
06   // prepare jsx_list ...
07   // var jsx_list = [ <SomeTag ...props> ... </SomeTag> ];
08   
07   // utils.setChildren(this,jsx_list);
10 });

上述渲染器 duals.id__ 的 setter 函數,咱們稱爲 idSetter 函數,這種以 「渲染器資源」 指代 render() 渲染過程的定義形式,稱爲 idSetter 定義

在 idSetter 函數中編寫代碼,等效於在 render() 編程,能夠隨意組裝子節點,而後用 utils.setChildren() 設進去。還可修改當前節點的 state.attr, duals.attr,甚至節點的標籤名也能夠改,如上面 02 行代碼。

藉助 duals.attr 的資源化形式(包括 duals.id__ 渲染器),ReRest 實現了 render() 渲染過程的範式變換。現有實踐代表,基於 ReRest 的編程與 React 原生方式等效,表達能力近乎等同。

7. 可視化設計與 MVVM 框架

爲了支持可視化編程,像 JSX 這種與 JS 代碼混寫的界面描述方式須要改進,由於界面設計應獨立進行。在可視化設計器中,被設計的界面,不能像產品正常運行那樣表現功能,鼠標點擊在可視化設計器中表示選擇一個構件,接下來要配置它的屬性,而對於正式運行的產品,多是按鈕點擊、跳轉連接點擊等,因此,基於 ReRest 的編程,要求咱們改用一種 「功能定義可選捆綁」 的界面描述方式

shadow-widget 採用 「轉義標籤」 描述界面,界面的功能實現則在投影類或 idSetter 函數中實施,這二者分開定義。產品正常運行時,在頁面導入初始化階段,二者自動捆綁,讓相似 onClick 在 JS 實現的功能定義,與用 「轉義標籤」 描述的界面結合。但在可視化設計狀態,功能定義缺省被忽略(注:也能夠不忽略,但要用特殊方式定義)。

上述 「轉義標籤」,就是用相似 <div $=Ul>desc</div> 的方式描述非行內標籤 <Ul>desc</Ul>,或用相似 <span $=Button>title</span> 描述行內標籤 <Button>title</Button>。上述 「投影類」,與 「idSetter 定義」 等效,都用來定義 Component 節點的行爲。限於本文篇幅,這三項咱們不展開介紹。

前面介紹的資源化改造,還支持了 MVVM 框架在 React 技術體系中得以實現,MVVM 要求數據屬性可以雙向綁定,duals.attrgetter/setter 支持了此項要求。以下圖,ViewModel 就是投影定義與 idSetter 定義,View 是各 Component 從虛擬 DOM 反映到真實 DOM 的界面表現,而 Model 是數據模型,對於前端開發,Model 一般很簡單,通常就是各 Component 的 props.attrduals.attr 規格定義,只有少數需對數據作轉換、存盤、備份等特殊處理的,纔會額外設計一個 Model 實體。

MVVM

MVVM 可視爲 MVC 框架在前端環境的最佳適配,它也是可視設計的基礎。可視化設計的主體過程是在建立 Component 構件後,在線設置它的 props.attrduals.attr 屬性值。正由於 MVVM 中 ViewModel 是雙向綁定的,屬性取值與界面表現才能自動保持一致,這也是 MVC 框架不能適應前端可視化開發,而 MVVM 適應得很好的主要緣由。

多說一句,屬性取值與界面表現並不是簡單的直接對應關係,而是屬性取值變動要關聯一系列變化,須有自動 setter 調用的機制才行。舉例來講,設置一個按鈕的 duals.disabled 爲真,不止是設置 DOM 節點的 disabled 屬性,還要讓按鈕外觀變灰,再改換 cursor 配置爲 "not-allowed"

8. 函數式風格

相比 Angular 與 Vue,React 生態鏈上各工具廣泛追求純正的函數式開發,這既與 React 團隊傾向性推進有關,也與 React 技術特徵有關,越傾向函數式開發就越適應它的 FLUX 模型。

8.1 函數式是 FRP 編程的自然姻親

FLUX 框架是 FRP 編程理念(Functional Reactive Programming)的一種實現,一個重要技術路徑是,以 CPS 風格(Continuation-Passing Style)應對響應式接續處理。

函數式編程正是 CPS 變換的最佳載體。舉一個簡單例子,以下提供 Email 輸入,當輸入內容不合郵箱格式時,右側圖標出現告警圖標,底部還有詳細提示。

Input Email

響應式編程的作法是,用戶持續輸入文本,內容是否合規隨即校驗,校驗與輸入同時進行,校驗結果並不打斷用戶輸入。這麼理解,手工輸入造成持續的數據流,各次數據都驅動一次校驗處理,校驗對於輸入來講是異步推動的。假定用戶輸入合法的 Email 地址後,系統用它自動向服務器查詢進一步信息,好比獲得用戶別名、上次登陸時間等,這些信息用來輔助下一步表單填寫。能夠這麼編碼:

01 comp.listen('validation', function(value,oldValue) {
02   if (value == 'success') {
03     var sEmail = comp.duals.email;
04     utils.ajax( {
05       url: '/users/' + encodeURIComponent(sEmail),
06       success: function(data) {
07         // ...
08       },
09     });
10   }
11 });

這裏 01 行與 06 行調用都是 CPS 風格,實際調用雖是異步,但代碼寫一塊兒,上下文變量共享。這種代碼風格在響應式編程中大量使用,不難看出,函數式是 FRP 編程的必然選擇。

8.2 ReRest 中的函數式編程

雖然 「資源」 是靜態化的概念,但 ReRest 對資源的動做定義,還是可適用 CPS 的函數方式,並未破壞總體函數式風格。簡單這麼理解,前面所提 ReRest 資源化,實質是提供了 帶錨點的函數式編程,錨點依附於 Component 實體而存在。因此,在可視設計器中,建立 Component 後,資源錨點(即 props.attrduals.attr)就存在了,這讓所見即所得的在線配置所以成爲可能。換一種說法,至關於 ReRest 在原設計基礎上,插入一排方便思考、易於可視設計的 「抓手」。

用來實現 Component 功能定義的投影類,以對象方式編碼,屬於命令式風格。而與之對等提供功能的 idSetter 定義,是函數式的,以下舉例:

this.defineDual('id__', function(value,oldValue) {
  if (oldValue == 1) {
    // init process just after all duals-attr registed
  }
  
  if (value <= 2) {
    if (value == 1) { // init process, same to getInitialState()
      // this.setEvent({$onClick:fn});
      // this.defineDual('attr',fn);
      // ...
    }
    else if (value == 2) { // same to componentDidMount()
      // ...
    }
    else if (value == 0) { // same to componentWillUnmount()
      // ...
    }
    return;
  }
  
  // other render process ...
});

前面已介紹 idSetter 如何組裝渲染內容,既然渲染器每次計數變化表明一次渲染調用,那能不能留出幾個特殊計數值表達 Component 狀態變化呢?idSetter 確實這麼作了,好比上面代碼,計數值爲 0 是初始狀態,變爲 1 是 Component 的雙源屬性還沒有預備的初始化狀態,至關於 getInitialState(),變爲 2 是 componentDidMount() 狀態,再變回 0 表示立刻要返回初態,對應於 componentWillUnmount()。這樣,一個完整的 React Class 定義,咱們用一個 idSetter 函數就表達了,實現了命令式風格的函數式表達。

idSetter 函數既適應可視化設計時界面描述與功能定義分離,還適應函數式編程。好比當有多層 Component 嵌套時,你能夠將裏層 Component 的行爲定義任意 「Lifting State Up」 到外層 Component 的函數空間。

8.3 Lifting State Up

採用 JSX 描述界面時,行爲定義與虛擬 DOM 描述混在一塊兒,這時僅依賴 props.attr 逐層傳遞實現數據共享方式,用起來很不方便。React 官方介紹提供一種 「上舉 State」 的解決方案,以輸入溫度值判斷是否達到沸點爲例,參見 Lifting State Up

將上舉 State 用在 ReRest 編程中,除了收穫 React 官方所提幾個好處,還有兩項特別收益。其一,原有 React 基於一個過程組織渲染內容,而 ReRest 主體是基於 duals.attr 資源驅動渲染,跨節點 listen 更容易,處理邏輯也更清晰;其二,定義節點行爲的 idSetter 是函數,原生 React Class 定義要用 class MyClass extends React.Component {} 方式,層層嵌套使用時,確定沒有 idSetter 用得方便。

若是仔細琢磨 「Lifting State Up」 方案,你們不難發現,上舉 State 解決了部分 Reflux 或 Redux 已支持的需求,被上舉共享的 state 其實也是一種 Store 數據。

9. 可視化設計實踐

ReRest 編程在 shadow-widget 平臺的實踐已持續一年多時間,多個項目採用了 ReRest 編程,較典型的有 pinp-blogshadow-bootstrap。在這一年多時間裏,shadow-widget 底層庫也在 ReRest 實踐推進下不斷完善,尤爲是 idSetter 與可計算表達式方面,優化幅度較大。

在接下來幾節,咱們補充介紹前文還沒有涉及的,與實踐相關的若干知識與編程體驗。

9.1 正交框架分析模式

先介紹 「功能塊」 Functionarity Block(簡稱 FB)的概念。一組 Component 節點合起來提供某專項功能,稱爲一個 FB。以上面提到 Lifting State Up 判斷溫度是否達到沸點爲背景,咱們能夠開發兩個功能塊,其一是配置溫度格式(config FB),用來配置當前採用攝氏 Celsius 仍是華氏 Fahrenheit 做計量單位,其二是計算沸點(calculator FB),提供輸入框,判斷輸入溫度是否達到沸點。

後一 FB 的界面以下:

計算沸點

編寫 FB 代碼塊以下:

(function() { // functionarity block: calculator

var scaleNames = { c:'Celsius', f:'Fahrenheit' };
var selfComp = null, verdictComp = null;

idSetter['calculator'] = function(value,oldValue) {
  // ...
};

})();

一個 FB 宜用一個函數包裹,主要爲了構造獨立的命名空間(Namespace),本功能塊內共享的變量在這個地方定義,好比上面代碼中 scaleNames, selfComp, verdictComp 變量,把命名空間獨立出來,也防止 FB 內部使用的變量污染外部全局空間。

既然一個 FB 內某些 Component 很經常使用,把它定義成 FB 內共享的變量會更方便。

var selfComp = null, verdictComp = null;

idSetter['calculator'] = function(value,oldValue) {
  if (value <= 2) {
    if (value == 1) {      // init
      selfComp = this;
      // ...
    }
    else if (value == 2) { // mount
      verdictComp = this.componentOf('verdict');
      // ...
    }
    else if (value == 0) { // unmount
      selfComp = verdictComp = null;
    }
    return;
  }
};

產品開發明顯可分兩個階段:界面可視化設計與功能實現,在前一階段,應考慮有哪些 FB 功能塊可分解,再針對各 FB 設計界面,按用戶使用習慣逐級擺放各構件,各層構件都是 W 樹中節點。以上述 config 與 calculator 功能塊爲例,咱們畫出 FB 分佈爲橫軸,W 樹爲縱軸的示例圖。

正交矩陣

以後進入開發第二階段:功能實現。這時要解決數據如何在 FB 之間流動,前一功能塊 config 配置當前採用哪一種溫度格式,記錄到 duals.scale,後一功能塊 calculator 根據自身 duals.scale 配置指示界面如何顯示,並決定用 100 度仍是 212 度判斷沸點,兩個 scale 屬性的數據流向以下圖,咱們只需讓後一 duals.scale 偵聽前一 duals.scale,即實現二者自動同步。

數據流分析

本處舉例比較簡單,複雜些產品的設計過程大體也是這幾個步驟。

總結一下,整個 HTML 頁面是一顆 DOM 樹,是縱向的(上圖縱軸),將這顆樹劃分爲若干 FB 功能塊(上圖橫軸),劃分過程主要依據 MVVM 逐步拆解;而處理各功能塊之間的橫向聯繫,則以 FRP 思路爲主導。這一縱一橫的思考方式,咱們稱爲 「正交框架」 分析模式。

可視化設計時,提供在線配置的最小單位是各 Component 的 props.attrduals.attr,就是 ReRest 所說的 「資源」 項。而處理各 FB 之間數據如何流動的思考起點,也是這類 「資源」 項,MVVM 與 FRP 分析的交匯處正是 ReRest 資源化的落腳點。

9.2 Component 屬性定位的變化

props.attr 是隻讀的,用來驅動本節點組織渲染數據,凡涉及狀態變化的要用 state.attr,而後一樣用 props 驅動子節點的內容更新。現有 React 生態鏈上各種工具對 props.attr 定位彷佛只有兩項:一是用做 Component 的入口驅動數據,二是以只讀特性保障數據單向流動。

shadow-widget 對 props 與 state 的使用定位作了優化。其一,用 duals.attr 表達一個 Component 對外公開的控制接口,再也不建議用 setState() 動態更新 「非自身節點」 的數據了,相應的 state.attr 也收縮到 「只供 Component 內部編程」 時使用,相似於用做私有變量。其二,props.attr 當入口驅動數據的定位沒變,但刨去轉換成 duals.attr 與事件函數,剩下的常規屬性在生存週期內被看做常量,在節點 unmount 以前不會變化。

這兩點定位調整的背後有深入緣由,開發理念變了。在 React 支持的虛擬 DOM 庫級別,各 Component 全部屬性都是對等的,無差異,虛擬節點無需識別各項屬性的語法含義,在底層這麼處理沒問題,由於做爲底層庫,只聚焦節點虛擬化。但對於上層應用,須區分各屬性的語義,現實應用中,各節點總具有必定 「性狀」 的。好比,你想表達一段文本就建立 <p> 節點;若是建立了 <ul> 節點,也意味着你將在它下面掛入 <li> 節點;若是建立 <input> 節點,一般連帶 type 屬性也算做 「性狀」 一部分,type='text' 文本框,type='checkbox' 是選項框,二者形態差別巨大,文本框要用 node.value 取輸入字串,選項框則用 node.checked

因此,上層應用宜將各節點的固有性狀,視做生存期內不變的常量,動態變化的歸入 duals,用做控制量。反之,若是不認可節點固有性狀,就不會有 MVVM 框架形式,可視設計器也沒法支持經過拖入樣板來建立 Component。好比假設你建立的是 <table> 節點,改改屬性就把它變成 <ul> 列表,可視設計就無法作了。

shadow-widget 還將 className 分裂成 props.classNameduals.klass 兩個屬性,用 className 表達固有類定義,在構件的生存期內不變,用 klass 表達可變的狀態量。

9.3 父子結節的單向依賴

咱們先看一個事實,Bootstrap 提供的 50 多個組件中,大部分由多層節點構成,或者使用時要求與其它組件搭配,一個節點表達完整功能的只是少數,並且都只提供簡單功能,像 Label、Badge 等,這類組件約佔總量十分之一。能夠說,現實中的前端開發,父子 Component 組合是常態,是主流。

Shadow Widget 有不少機制讓父子節點關聯起來,主要有:

  1. 把全部存活的構件(已掛載且未卸載)串接成一顆 W 樹,樹中各節點能方便的互相引用
  2. 提供導航面板把多個構件封裝起來,造成一組,組內構件用 "./" 相對路徑索引
  3. 上面提到 FB 功能塊的編碼,創建塊內共用 Namespace,讓功能緊密相關的父子節點共享變量
  4. $for, $if, $else 等指令描述動態節點,層層嵌套的 callspace 支持在下級節點直接引用上級各層節點的各類屬性
  5. 支持 $trigger 機制觸發相鄰節點的動做定義

React 讓 props 屬性只讀的深入根源是:解決數據依賴性。解決依賴性的同時,順帶保證數據在父子節點之間要單向流動。節點建立有先有後,具備從屬關係的兩個節點,子節點必然在父節點以後建立,而且 unmount 必在父節點以前,也就是,子節點依賴於父節點而存在,子節點的數據也依賴於父節點的屬性先行賦值。因此,React 設計了數據傳遞要藉助 props 逐層進行,原則上屬性數據跨層不可見(先撇開 context 不談,那是補救性設計,官方並不推薦你用)。

子節點依賴於父節點,但反過來不是,依賴是單向的,但 React 生態鏈上諸多工具,都按 「隔絕依賴」 來處理了,至關於忽略了單向依賴存在。舉例來講,比方咱們要設計下圖 DropdownBtn 與 SplitBtn 兩種按鈕,二者功能基本同樣,外觀有差異,怎麼實現呢?

DropdownBtn and SplitBtn

外層節點用 this.isSplitBtn 指示按鈕是否爲 SplitBtn,而後裏層節點根據 isSplitBtn 取值,繪製不一樣外觀的按鈕。若是按 「隔絕依賴」 來處理,只能藉助 props 屬性層層傳遞 isSplitBtn,隔了幾層就傳幾層;若是按 「單向依賴」 來處理,裏層哪一個節點須要要區分 isSplitBtn,就往上層查找,看看 props.isSplitBtn 取什麼值。這兩種處理方式差異很大,前者忽略了主從構件的自然關係,以暴露接口的代價實現功能,把無關節點都牽扯進當來傳手,就像打排球的一傳、二傳、三傳,當功能組合較多時,顯得很繞。

從子節點向上查找,分析一級(或多級)父節點的屬性特色,從而肯定它自身所處的場景,進而讓當前節點應對不一樣場景表現不一樣功能。咱們管這種場景推導過程叫 「場景自省」,如上介紹,向上追溯的 「場景自省」 是安全的,由於子節點若存活,父節點必然還存活,反過來從父節點查子節點則不行。

9.4 不繞彎也是生產力

現有 React 生態鏈上諸多主流工具都很繞,不像 shadow-widget 那麼直接,主要表現如下幾個方面。

繩結

其一,主流工具廣泛忽視父子節點的主從關係是隱含豐富信息的,把全部 Component 擺同等位置來解決跨節點數據傳遞問題。

源頭在於 Facebook 官方的 FLUX 框架有缺陷,FLUX 在虛擬 DOM 的上層實現,但它繼續無視 Component 屬性帶語義特性,都無差異對待。藉助 Dispatcher 分發 Action,構造獨立的 Store,統一處理各 Action 消息。另設 Store 與 Action 另行驅動的過程,至關於換個地方重建各節點的場景信息。

其二,這些工具廣泛過於依賴函數式風格,靜態化概念只停留在 Component 層面,沒往下探一層。各 Component 互相關聯,造成網格,這網格直接用函數式編程去編織了。由於代碼量沒減,該作的事情一件很多,重建場景的各個處理環節又衍生很多概念,比較繞。基於 ReRest 的編程則將 Component 下的屬性視做資源,把靜態化概念深刻一層,而後在 「資源粒子」 層面,用函數式風格編織網格。這樣更直接了當,也符合開發者思考習慣。

shadow-bootstrap 項目按 ReRest 理念去實踐的,該項目核心功能是將 Bootstrap 往 shadow-widget 平臺適配。與之相似,業界還有一個知名項目 react-bootstrap,把 Bootstrap 往 React 適配。這兩項目的功能對等,封裝的組件幾乎能一一對應,若是對比二者源碼,shadow-bootstrap 明顯簡潔許多,react-bootstrap 不容易讀,繞來繞去的。最終代碼 minify 後,前者 103 Kb,然後者 213 Kb,整整多出一倍。前者開發只用一個多月,後者遠不止這個投入,當咱們的框架沒那麼繞時,生產力是大幅提高的。

10. 總結

長期以來 GUI 開發工具與 Web 前端工具是兩條獨立主線,並行發展。MFC、Delphi、VB、WxWidget、Qt 等納入前者,沒人將前端開發也視做 GUI 一類,不過,大概沒人否定前端開發主要工做是設計圖形用戶界面(Graphical User Interface),就目的而言,前端開發無疑也是 GUI 開發。

這兩條主線靠攏發展的時代已來臨,虛擬 DOM 技術結合 FRP 理念,再結合 ReRest 資源化改造,基於 MVVM 框架 —— 對應主流 GUI 工具的 MVC —— 的可視化開發已經走通了。ReRest 方法論嘗試讓前端開發迴歸可視化 GUI 工具序列,其實踐已在 shadow-widget 平臺走出第一步,但願這一步對 Web APP 與 Native APP 逐步融合的發展提供有益經驗。

 

(本文完)

相關文章
相關標籤/搜索