無論你用 React,Vue,還是 Angular,你還是要一遍一遍寫相似的 CRUD 頁面,一遍一遍,一遍一遍,一遍又一遍……
“天下苦秦久矣”~~
現在的前端開發,我們有了世界一流的 UI 庫 React,Vue,Angular,有了樣式豐富的 UI 元件庫 Tea (騰訊雲 UI 元件庫,類似 Antd Design), 有了友善強大的腳手架工具(例如,create react app)。但是我們在真正業務代碼之前,通常還免不了寫大量的樣闆代碼。
現在的 CRUD 頁面代碼通常:
太輕的“Model”或着“Service”,大多時候隻是一些 API 調用的封裝。
胖”View“,View 頁面中有展示 UI 邏輯,生命周期邏輯,CRUD 的串聯邏輯,然後還要塞滿業務邏輯代碼。
不同的項目業務邏輯不同,但是清單頁,表單,搜尋這三闆斧的樣闆代碼,卻要一遍一遍占據着前端工程師的寶貴時間。
特别是 CRUD 類應用的樣闆代碼受限于團隊風格,後端 API 風格,業務形态等,通常内在邏輯相似書寫上卻略有差別,無法通過一個通用的庫或者架構來解決(上圖中背景越深,越不容易有一個通用的方案)。
說好的“資料驅動的前端開發”呢?
對于這個“痛點”——怎麼盡可能的少寫模版代碼,就是本文嘗試解決的問題。
我們嘗試使用 JavaScript 新特性<code>Decorator</code>和<code>Reflect</code>元程式設計來解決這個問題。
從 ECMAScript 2015 開始,JavaScript 獲得了 <code>Proxy</code> 和 <code>Reflect</code> 對象的支援,允許你攔截并定義基本語言操作的自定義行為(例如,屬性查找,指派,枚舉,函數調用等)。借助這兩個對象,你可以在 JavaScript 元級别進行程式設計。MDN
在正式開始之前,我們先複習下<code>Decorator</code>和<code>Reflect</code>。
這裡我們簡單介紹 Typescript 的<code>Decorator</code>,ECMAScript 中<code>Decorator</code>尚未定稿,但是不影響我們日常的業務開發(Angular 同學就在使用 Typescript 的<code>Decorator</code>)。
簡單來說,<code>Decorator</code>是可以标注修改類及其成員的新語言特性,使用<code>@expression</code>的形式,可以附加到,類、方法、通路符、屬性、參數上。
TypeScript 中需要在<code>tsconfig.json</code>中增加<code>experimentalDecorators</code>來支援:
比如可以使用類修飾器來為類擴充方法。
Reflect 是 ES6 中就有的特性,大家可能對它稍微陌生,Vue3 中依賴 Reflect 和 Proxy 來重寫它的響應式邏輯。
簡單來說,<code>Reflect</code>是一個人内置的對象,提供了攔截 JavaScript 操作的方法。
Reflect Metadata 是 ES7 的一個提案,Typescript 1.5+就有了支援。要使用需要:
<code>npm i reflect-metadata --save</code>
在 <code>`tsconfig.json`</code> 裡配置 <code>`emitDecoratorMetadata`</code> 選項
簡單來說,Reflect Metadata 能夠為對象添加和讀取中繼資料。
如下可以使用内置的<code>design:key</code>拿到屬性類型:
回到正題——使用 Decorator 和 Reflect 來減少 CRUD 應用中的樣闆代碼。
CRUD 頁面無需多言,清單頁展示,表單頁修改 ……包括 API 調用, 都是圍繞某個資料結構(圖中<code>Person</code>)展開,增、删、改、查。
基本思路很簡單,就像上圖,Model 是中心,我們就是借助<code>Decorator</code>和<code>Reflect</code>将 CRUD 頁面所需的樣闆類方法屬性元程式設計在 Model 上。進一步延伸資料驅動 UI的思路。
借助 Reflect Matadata 綁定 CRUD 頁面資訊到 Model 的屬性上
借助 Decorator 增強 Model,生成 CRUD 所需的夜班代碼
下文,我們用TypeScript和React為例,元件庫使用騰訊Tea component 解說這個方案。
首先我們有一個函數來生成不同業務的屬性裝飾函數。
一個類裝飾器,處理通過資料裝飾器收集上來的中繼資料。
TypeScript 項目中第一步自然是将後端資料安全地轉換為<code>type</code>,<code>interface</code>或者<code>Class</code>,這裡 Class 能在編譯後在 JavaScript 存在,我們選用<code>Class</code>。
重點在<code>handle?: string | ServerHandle</code>函數,在這個函數處理 API 資料和前端資料的轉換,然後在<code>constructor</code>中集中處理。
清單頁中一般使用 Table 元件,無論是 Tea Component 還是 Antd Design Component 中,樣闆代碼自然就是寫那一大堆 Colum 配置了,配置哪些 key 要展示,表頭是什麼,資料轉化為顯示資料……
首先我們收集 Tea Table 所需的<code>TableColumn</code>類型的 column 中繼資料。
然後在 EnhancedClass 中收集,生成 column 清單。
自然我們封裝一個更簡易的 Table 元件。
<code>getConfigMap<T>(F: any, cachekey: symbol,metaKey: symbol): Map<string,T></code> 收集中繼資料到 Map
<code>static getColumns<T>(): EnhancedTableColumn<T>[]</code> 得到 table 可用 column 資訊。
效果很明顯,不是嗎? 7 行寫一個 table page。
表單,自然就是字段的 name,label,require,validate,以及送出資料的轉換。
Form 表單我們使用Formik + Tea Form Component + yup(資料校驗)。Formik 使用 React Context 來提供表單控件所需的各種方法資料,然後借助提供的 Field 等元件,你可以很友善的封裝你的業務表單元件。
照貓畫虎,我們還是先收集 form 所需的中繼資料
有了中繼資料,我們可以在 EnhancedClass 中生成 form 所需:
initialValues
資料校驗的 validationSchema
各個表單元件所需的,name,label,required 等
送出表單的資料轉換 handle 函數
上文包含了不少的代碼,但是大部頭在如何将中繼資料轉換成為頁面元件可用的資料,也就是元程式設計的部分。
而業務頁面,7 行的 Table 頁面,40 行的 Form 頁面,已經非常精簡功能完備了。根據筆者實際項目中估計,可以節省至少 40%的代碼量。
寫到尾聲,你大概會想到某些配置系統,前端 CRUD 這個從古就有的需求,自然早就有方案,用的最多的就是配置系統,在這裡不會過多讨論。
簡單來說,就是一個單獨的系統,配置類似上文的元資訊,然後使用固定模版生成代碼。
思路實際上和本文的元程式設計類似,隻是元程式設計成本低,你不需要單獨做一個系統,更加輕量靈活,元程式設計代碼在運作時,想象空間更大……
上面隻是 table,form 頁面的代碼展示,由此我們可以引申到很多類似的地方,甚至 API 的調用代碼都可以在元程式設計中處理。
元程式設計——将中繼資料轉換成為頁面元件可用的資料,這部分恰恰可以在團隊内非常好共享也需要共同維護的部分,帶來的好處也很明顯:
最大的好處自然就是生産效率的提高了,而且是低成本的實作效率的提升(相比配置系統)。一些簡單單純的 CURD 頁面甚至都不用寫代碼了。
更易維護的代碼:
“瘦 View“,專注業務,
更純粹的 Model,你可以和 redux,mobx 配合,甚至,你可以從 React,換成 Angular)
最後更重要的是,元程式設計是一個低成本,靈活,漸進的方案。它是一個運作時的方案,你不需要一步到羅馬,徐徐圖之 …… - ……
前端元程式設計,較少你的樣闆代碼,加速前端開發