摘要:是前端開發領域新興的方法論體系,它繼承了與編程理念,在技術上有不少創新。但專利與開源協議是平行的兩個世界,改底層也不大容易解決問題。此外,要求在中結合各屬性的是否變化,判斷是否該觸發更新。
ReRest (Reactive Resource State Transfer) 是前端開發領域新興的方法論體系,它繼承了 MVVM 與 FRP 編程理念,在技術上有不少創新。本文從專利稿修改而來,主要介紹 ReRest 原理與若干實踐經驗。
?
說明:文章作者授權任何組織或個人,在不更改原文內容(包括本段)的前提下,可以自由轉載本文。點擊下載本文 PDF 格式
?
1. 前言前陣子 React 附加專利條件的開源協議鬧得沸沸揚揚,國內外有多家大公司開始棄用 React,我們也深感困惑,是否該將 shadow-widget 全盤改寫,很猶豫。讓底層脫離 React。但專利與開源協議是平行的兩個世界,改底層也不大容易解決問題。Facebook 擁有虛擬 DOM 方面的專利,preact、vue 都可能涉嫌侵權,通過修改底層代碼來規避還是挺難的。
后來我們決定自己申請專利,以便今后萬一用到,手頭有個專利可為 shadow-widget 增加話語權。當權利要求書完稿時,Facebook 宣布 React 回歸真正的 MIT 開源協議了,真是大喜訊!我們不必擔心專利的風險了,為自家申請專利不再必要 —— 我們創建 shadow-widget 技術平臺,但無意借此盈利,源碼開放出來讓大家都受益。(PS:不必感謝,如果覺得這項目對您有用,上 github 為我們加星吧)
本文從專利申請稿改寫而來,內容有壓縮,要不文章太長了,另外還增加了可視化編程實踐相關的若干內容。公布此文還有一個目的,防止他人偷偷拿我們的技術申請專利,如果以后真發現有人這么干了,本文是憑證,大家可以提請專利無效,把別人的保護條款廢掉。
說明:本文完稿時,Shadow Widget 最新版本為 v1.1.2,產品用戶手冊對技術實現有更詳細介紹。
2. 背景近些年 Web 前端技術發展,可以說是框架橫飛的時代,雖然十年前網頁還正常能打開,IE 還是那個頑固的 IE,但前端開發卻已經歷翻天覆地的變化。近來比較搶眼的是 React 框架,Facebook 開創性的實踐了兩種技術:虛擬 DOM 與 Functional Reactive Programming(FRP,函數式響應型編程),這兩種技術幾乎已成現代前端框架的標準配置。
Facebook 在虛擬 DOM 上原創較多,鉆研很深入,這項技術也可以說很成熟了。FRP 在 React 的實現就是那個 FLUX 框架,它不是 Facebook 首創,在 React 中用起來也有點磕磕碰碰,尤其在調和指令式風格與函數式風格方面,并不順暢。
另外,盡管十年來 Web 開發技術發展很快,但在可視化開發方面仍然進展緩慢,所有主流框架都在界面的形式化描述上做文章,Angular 與 Vue 擴展了標簽屬性,增加不少控制指令,React 則全盤引入 JSX 描述方式,他們無一例外的都要求大家,一行行寫腳本去定義界面,而不是 20 年前在 Delphi 與 VB 就已出現的可視化、所見即所得的開發方式。
本文所提的 ReRest 編程方法,是適應 Web 可視化開發要求,融合虛擬 DOM 與 FRP 技術,并克服它們應用于主流框架的若干不足,而提出的通用型解決方案。ReRest 方法在 shadow-widget 平臺有一些實踐,已取得良好效果。
3. ReRest 要點ReRest 全稱為 REactive REsource State Transfer,譯為 “響應式資源狀態遷移”,與本概念相關的提法還有:
ReRest framework,ReRest 框架
ReRest based programming,基于 ReRest 的編程
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 的屬性數據。
理解基于 ReRest 的編程,須把握兩個重點:Component 管界面呈現,Resource 管數據流。前者適用靜態思維,更偏指令式風格,后者適用動態思維,更偏函數式風格。
4. 兩種思維模式主流的前端框架一直并存靜態與動態兩種思維模式,舉例來說,Vue 與 Angular 更多采用靜態思維模式,界面是可描述的,React 更多的用動態思維,界面是可編程的,JSX 看上去也是一種表述形式,但它本質是一段 javascript 代碼,你很難將它 “去編程化” —— 把 JSX 從上下文環境摳出來獨立使用,事情將變得毫無意義。
我們不必爭論這兩種模式孰優孰劣,兩者都有顯著優點。F.S.菲茨杰拉德曾說:檢驗一流智力的標準,就是頭腦中能同時存在兩種相反的想法,但仍保持行動能力。
況且,在前端開發中,該采用靜態思維或動態思維的條件還算清晰。比如開發一個網頁,大塊功能的界面設計應采用靜態思維,比方,在頂部放一個工具條,左側放導航,中間放內容;簡單界面設計應以靜態思維為主,因為界面組件很少動態替換;而應對復雜功能,應以動態思維為主,既然 JS 代碼可以控制一切,局部界面用 JSX 定義會很爽。越是動態變化的界面,應該越傾向于用動態的、編程性思維。
Angule 靜態思維過重,React 動態思維過重,都不好,Vue 從靜態走向動態,易用且適應復雜變化,應該說它正前進在正確道路上,只是,Vue 兼容兩種風格并非一開始就統籌規劃了,工具復雜性不容易降下來。
5. 從 FRP 到 FLUX,再到 ReRestReRest 在前人已有經驗基礎上,提出更優方法,然后驗證,結合實踐再調整、優化,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 框架要把它改造成 “顯式雙源”。
實現原理大致如下:
引入一個與 props.attr 及 state.attr 對等的集合:duals.attr
該集合中的 attr 把 props.attr 自動記錄到 state.attr,通過 duals.attr 讀寫接口,可等效實現對相應 state.attr 的存取,即:讀 duals.attr 等效于讀 state.attr,寫操作 duals.attr = value 等效于執行 this.setState({attr:value})。
提供 this.defineDual() 讓用戶手工注冊 duals 屬性
系統還將傳給標簽內置屬性(如 name,href,src 等)自動注冊為 duals 屬性,此舉方便了編程,否則大量屬性手工編碼去注冊很麻煩。
由 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 種行為含義如下:
讀
從 props.attr 直接讀取,或從 duals.attr 讀取由系統返回 state.attr 的值。
寫
對 duals.attr 賦值,系統除了把值賦給 state.attr 外,還觸發相應的 “setter處理” 與 “listen處理”。
setter 處理
這個 setter 就是 defineDual(attr,setter) 的 setter 回調函數。對于同一 Component 的同一 attr,可以調用多次 defineDual() 注冊多個回調函數,給 attr 賦值后,各回調函數依次被調,調用順序與注冊順序相同。
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.attr2 與 comp2.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 具備兩個特點:
采用多 Store(與 reflux 類似),Store 實體與 Component 重合。
由于數據流動設計針對 Component 下的屬性展開,為方便理解,ReRest 的 Store 也可視為雙層結構,第一層是 Component 實體,第二層是 Component 下視作 resource 的屬性定義,包括 props.attr 與 duals.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 = [... ]; 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 實現的功能定義,與用 “轉義標簽” 描述的界面結合。但在可視化設計狀態,功能定義缺省被忽略(注:也可以不忽略,但要用特殊方式定義)。
上述 “轉義標簽”,就是用類似 desc
,或用類似 title 描述行內標簽 。上述 “投影類”,與 “idSetter 定義” 等效,都用來定義 Component 節點的行為。限于本文篇幅,這三項我們不展開介紹。
前面介紹的資源化改造,還支持了 MVVM 框架在 React 技術體系中得以實現,MVVM 要求數據屬性能夠雙向綁定,duals.attr 的 getter/setter 支持了此項要求。如下圖,ViewModel 就是投影定義與 idSetter 定義,View 是各 Component 從虛擬 DOM 反映到真實 DOM 的界面表現,而 Model 是數據模型,對于前端開發,Model 通常很簡單,一般就是各 Component 的 props.attr 與 duals.attr 規格定義,只有少數需對數據做轉換、存盤、備份等特殊處理的,才會額外設計一個 Model 實體。
MVVM 可視為 MVC 框架在前端環境的最佳適配,它也是可視設計的基礎。可視化設計的主體過程是在創建 Component 構件后,在線設置它的 props.attr 與 duals.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 輸入,當輸入內容不合郵箱格式時,右側圖標出現告警圖標,底部還有詳細提示。
響應式編程的做法是,用戶持續輸入文本,內容是否合規隨即校驗,校驗與輸入同時進行,校驗結果并不打斷用戶輸入。這么理解,手工輸入形成持續的數據流,各次數據都驅動一次校驗處理,校驗對于輸入來說是異步推進的。假定用戶輸入合法的 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.attr 與 duals.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-blog 與 shadow-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.attr 與 duals.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 所有屬性都是對等的,無差別,虛擬節點無需識別各項屬性的語法含義,在底層這么處理沒問題,因為作為底層庫,只聚焦節點虛擬化。但對于上層應用,須區分各屬性的語義,現實應用中,各節點總具備一定 “性狀” 的。比如,你想表達一段文本就創建
節點;如果創建了 節點,也意味著你將在它下面掛入 節點;如果創建 節點,通常連帶 type 屬性也算作 “性狀” 一部分,type="text" 文本框,type="checkbox" 是選項框,兩者形態差異巨大,文本框要用 node.value 取輸入字串,選項框則用 node.checked。
所以,上層應用宜將各節點的固有性狀,視作生存期內不變的常量,動態變化的納入 duals,用作控制量。反之,如果不承認節點固有性狀,就不會有 MVVM 框架形式,可視設計器也無法支持通過拖入樣板來創建 Component。比如假設你創建的是 shadow-widget 還將 className 分裂成 props.className 與 duals.klass 兩個屬性,用 className 表達固有類定義,在構件的生存期內不變,用 klass 表達可變的狀態量。 我們先看一個事實,Bootstrap 提供的 50 多個組件中,大部分由多層節點構成,或者使用時要求與其它組件搭配,一個節點表達完整功能的只是少數,而且都只提供簡單功能,像 Label、Badge 等,這類組件約占總量十分之一。可以說,現實中的前端開發,父子 Component 組合是常態,是主流。 Shadow Widget 有很多機制讓父子節點關聯起來,主要有: 把所有存活的構件(已掛載且未卸載)串接成一顆 W 樹,樹中各節點能方便的互相引用 提供導航面板把多個構件封裝起來,形成一組,組內構件用 "./" 相對路徑索引 上面提到 FB 功能塊的編碼,建立塊內共用 Namespace,讓功能緊密相關的父子節點共享變量 用 $for, $if, $else 等指令描述動態節點,層層嵌套的 callspace 支持在下級節點直接引用上級各層節點的各種屬性 支持 $trigger 機制觸發相鄰節點的動作定義 React 讓 props 屬性只讀的深刻根源是:解決數據依賴性。解決依賴性的同時,順帶保證數據在父子節點之間要單向流動。節點創建有先有后,具有從屬關系的兩個節點,子節點必然在父節點之后創建,并且 unmount 必在父節點之前,也就是,子節點依賴于父節點而存在,子節點的數據也依賴于父節點的屬性先行賦值。所以,React 設計了數據傳遞要借助 props 逐層進行,原則上屬性數據跨層不可見(先撇開 context 不談,那是補救性設計,官方并不推薦你用)。 子節點依賴于父節點,但反過來不是,依賴是單向的,但 React 生態鏈上諸多工具,都按 “隔絕依賴” 來處理了,相當于忽略了單向依賴存在。舉例來說,比方我們要設計下圖 DropdownBtn 與 SplitBtn 兩種按鈕,兩者功能基本一樣,外觀有差別,怎么實現呢? 外層節點用 this.isSplitBtn 指示按鈕是否為 SplitBtn,然后里層節點根據 isSplitBtn 取值,繪制不同外觀的按鈕。如果按 “隔絕依賴” 來處理,只能借助 props 屬性層層傳遞 isSplitBtn,隔了幾層就傳幾層;如果按 “單向依賴” 來處理,里層哪個節點需要要區分 isSplitBtn,就往上層查找,看看 props.isSplitBtn 取什么值。這兩種處理方式差別很大,前者忽略了主從構件的天然關系,以暴露接口的代價實現功能,把無關節點都牽扯進當來傳手,就像打排球的一傳、二傳、三傳,當功能組合較多時,顯得很繞。 從子節點向上查找,分析一級(或多級)父節點的屬性特點,從而確定它自身所處的場景,進而讓當前節點應對不同場景表現不同功能。我們管這種場景推導過程叫 “場景自省”,如上介紹,向上追溯的 “場景自省” 是安全的,因為子節點若存活,父節點必然還存活,反過來從父節點查子節點則不行。 現有 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,整整多出一倍。前者開發只用一個多月,后者遠不止這個投入,當我們的框架沒那么繞時,生產力是大幅提升的。 長期以來 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 逐步融合的發展提供有益經驗。 ? (本文完) 文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。 轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89245.html 摘要:前端日報精選桌面通知精讀前端性能優化備忘錄聊聊組件間通信的幾種姿勢到底該如何配置深入理解高階組件中文第期體系調研報告前端面試總結掘金技術周刊期知乎專欄從試著改進可重用做起掘金式數學作者眾成翻譯為什么企業進行數碼變革要用平臺眾成
2017-10-23 前端日報
精選
HTML5 桌面通知:Notification API精讀《2017前端性能優化備忘錄》聊聊Vue.js組件間通信的幾種姿... 摘要:前言非正經入門是相對正經入門而言的。不過不要緊,正式學習仍需回到正經入門的方式。快速入門建議先學會用拼文寫文檔注冊一個賬號,把庫到自己名下,然后用這個庫寫自己的博客,參見這份介紹。會用拼文寫文章,相當于開發已入門三分之一了。
本系列博文從 Shadow Widget 作者的視角,解釋該框架的設計要點,既作為用戶手冊的補充,也從更本質角度幫助大家理解 Shadow Widget 為什么這... 摘要:明明如日中天,把它與倒過來,給加點東西或可與抗衡。在之后,大版本有十數個,只有最近推的才回歸正常等后人總結歷史,無疑會把與之間的所有都稱為垃圾。讓網頁支持所見即得的可視化設計,是框架的最高形態,以前沒有類似工具,主要因為技術做不到。
好吧,我承認我是標題黨。React 明明如日中天,把它與 Vue 倒過來,給 Vue 加點東西或可與 React 抗衡。不過,這兩年 Vue 干的正是這事... 閱讀 2595·2021-11-17 09:33 閱讀 3936·2021-10-19 11:46 閱讀 910·2021-10-14 09:42 閱讀 2252·2021-09-22 15:41 閱讀 4204·2021-09-22 15:20 閱讀 4628·2021-09-07 10:22 閱讀 2302·2021-09-04 16:40 閱讀 811·2019-08-30 15:52 節點,改改屬性就把它變成
列表,可視設計就沒法做了。
相關文章
2017-10-23 前端日報
React 可視化開發工具 Shadow Widget 非正經入門(之一:React 三宗罪)
介紹一項讓 React 可以與 Vue 抗衡的技術
發表評論
0條評論
Cciradih
男|高級講師
TA的文章
閱讀更多
IBM推出新款量子芯片,預計兩年內擊敗傳統計算機
Linphone和MicroSIP軟電話中暴露嚴重安全漏洞 可致黑客遠程攻擊
【物聯網】12.物聯網服務器發送方式(HTTP,WebSocket ,MQTT )
如何訪問云主機數據庫-云主機跟云數據庫的區別?
云主機怎么安裝軟件-如何選擇云主機部署管理軟件?
4個實時查看臺風路徑的網站平臺(知曉臺風最新消息必備工具)
??身為在軟件測試摸爬滾打多年工程師的感悟,寫給正在迷茫的你??
React事件