javascript
JS 装饰器(Decorator)场景实战
本文不會大篇幅介紹裝飾器(Decorator)的概念和基礎用法,核心介紹我們團隊如何將裝飾器應用于實際開發,和一些高級用法的實現。
裝飾器簡介
Decorator 是 ES7 的一個新語法,正如其“裝飾器”的叫法所表達的,他可以對一些對象進行裝飾包裝然后返回一個被包裝過的對象,可以裝飾的對象包括:類,屬性,方法等。Decorator 的寫法與 Java 里的注解(Annotation)非常類似,但是一定不要把 JS 中的裝飾器叫做是“注解”,因為這兩者的原理和實現的功能還是有所區別的,在 Java 中,注解主要是對某個對象進行標注,然后在運行時或者編譯時,可以通過例如反射這樣的機制拿到被標注的對象,對其進行一些邏輯包裝。而 Decorator 的原理和作用則更為簡單,就是包裝對象,然后返回一個新的對象描述(descriptor),其作用也非常單一簡單,基本上就是獲取包裝對象的宿主、鍵值幾個有限的信息。
關于 Decorator 的詳細介紹參見文章:zhuanlan.zhihu.com/FrontendMag…
簡單來說,JS 的裝飾器可以用來“裝飾”三種類型的對象:類的屬性/方法、訪問器、類本身,簡單看幾個例子吧。
針對屬性/方法的裝飾器
// decorator 外部可以包裝一個函數,函數可以帶參數 function Decorator(type){/*** 這里是真正的 decorator* @target 裝飾的屬性所述的類的原型,注意,不是實例后的類。如果裝飾的是 Car 的某個屬性,這個 target 的值就是 Car.prototype* @name 裝飾的屬性的 key* @descriptor 裝飾的對象的描述對象*/return function (target, name, descriptor){// 以此可以獲取實例化的時候此屬性的默認值let v = descriptor.initializer && descriptor.initializer.call(this);// 返回一個新的描述對象,或者直接修改 descriptor 也可以return {enumerable: true,configurable: true,get: function() {return v;},set: function(c) {v = c;}}} }復制代碼注意這里的 target 對應的是被裝飾的屬性所屬類的原型,如果是裝飾一個 A 類的屬性,并且 A 類是繼承自 B 類的,這時候你打印 target,獲取到的是 A.prototype,它的結構是這樣的,這里一定要注意:
[image:A944761A-E0FA-4C04-BD90-BE179C46B641-35651-00001223828250C5/187FCC2A-8CC4-46C4-B8A3-A7FD5E0376F6.png]
如果需要操作 target,可能需要搞清楚這個問題。
針對 訪問操作符的裝飾
與屬性方法類似,就不詳述了。
class Person {@nonenumerableget kidCount() { return this.children.length; } }function nonenumerable(target, name, descriptor) {descriptor.enumerable = false;return descriptor; }復制代碼針對類的裝飾
// 例如 mobx 中 @observer 的用法 /*** 包裝 react 組件* @param target*/ function observer(target) {target.prototype.componentWillMount = function() {targetCWM && targetCWM.call(this);ReactMixin.componentWillMount.call(this);}; }復制代碼其中的 target 就是類本身(而不是其 prototype)
真實場景應用
今天,我們要介紹的主要是,如何將 Decorator 這個特性應用于數據定義層,實現一些類似于類型檢查、字段映射等功能。
關于數據定義層(Model),其實就是應用內出現的各種實體數據的定義,也就是 MVVM 中的 M 層,注意,和 VM 層做好區分,Model 本身不提供數據的管理和流通,只負責定義某個實體本身的屬性和方法,例如頁面里有一輛車的模塊,我們就定義一個 CarModel,它用來描述車輛的顏色、價格、品牌等信息。
關于為什么要在前端應用內定義明確的 Model,這個我之前在知乎上也早有論述,核心幾點:
- 提高可維護性。將數據源頭的實體做一個固定而準確的描述,這個對于串聯理解整個應用非常重要,特別是在重構或者接手別人的代碼的時候,你需要準確的知道一個頁面(或者是一個模塊)它會包含哪些數據,這些數據分別有哪些字段,這樣更便于理解整個應用的數據邏輯。
- 提高確定性。當你要給你的界面增加幾個車輛字段的時候,你不清楚之前是否已經定義過這些字段,服務端是否會返回這些字段,可能要請求一下(并且要有權限取到所有字段)才能知道,但是如果有 model 的明確定義,有什么字段就一目了然了。
- 提高開發效率。在這一層統一做一些數據映射和類型檢查等工作,這也是今天要講的重點。
以我們團隊 RN 開發框架中 Model 部分的實現為例,我們至少提供了三個基礎的基于 Decorator 的功能:類型檢查,單位轉換,字段映射。接下來我會先簡單介紹下這幾個功能是做什么的,隨后介紹如何實現這些 Decorator。
先來看看最終調用時候的代碼
class CarModel extends BaseModel {/*** 價格* @type {number}*/@observable@Check(CheckType.Number)@Unit(UnitType.PRICE_UNIT_WY)price = 0;/*** 賣家名* @type {string}*/@observable@Check(CheckType.String)@ServerName('seller_name')sellerName = ''; }復制代碼可以看到我們有三個自定義的 decorator :
@Unit, // 單位轉換裝飾器 @Check, // 類型檢查裝飾器, @ServerName // 數據字段映射裝飾器,當前后端定義的字段名不一致的時候用復制代碼@Unit 是一個比較特殊的裝飾器,它的作用是在前后端之間自動轉換單位,也就是前端和后端交換某些帶單位的數據的時候,會把根據各端的注解和裝飾器,把真實值轉換成帶單位的值傳給另一端,然后另一端會在框架層自動轉成它定義的單位,以此解決前后端單位不一致,交換數據時混亂導致的問題。
被 @Unit 裝飾過的屬性,讀寫的時候都是按照前端的單位讀寫,然后再轉換成 JSON 的時候就會特殊處理成類似 12.3_$wy 這樣的格式,表示這個數的單位是萬元。
@Check 更為容易理解,就是用來檢查字段類型,或者檢查字段格式,或者一些自定義檢查,例如正則表達式等。
@ServerName 則用來做映射,例如前后端對同一個界面元素的命名不同,這時候不需要完全按照服務端的命名來決定,可以在前端用另外一個屬性名,然后將其裝飾成服務端的字段名。
基礎實現
我們的目標就是實現這幾個 Decorator,按照之前對 Decorator 的科普,其實要獨立實現這幾個功能其實非常簡單。
以 @Check 為例,我們改寫被包裝屬性的 descriptor,返回一個新的 descriptor,將被包裝屬性的 getter 和 setter 重新定義,然后在其調用 setter 的時候先檢查傳入參數的類型和格式,做一些對應的處理。
非常簡單,其他幾個 Decorator 的實現也類似,可能像@Unit 這種實現起來會稍顯復雜,不過只要在 Decorator 中記住每個屬性標注的單位,在序列化的時候獲取對應的屬性對應的單位然后做轉換就可以了。
基礎實現的問題
但是,到這里,問題其實還沒有完!
我們的確實現了一個可用的 Decorator,但是這些 Decorator 可以疊加使用嗎?另外可以和業界常用的一些 Decorator 混用嗎?例如 mobx 中的 @ observable。也就是我上面最開始的實例的用法:
如果你按照我剛才的方式實現 @Check 和 @ServerName 的話,你會發現兩個致命的問題:
- 這兩個自己實現的 Decorator 首先就沒法疊加使用。
- 這兩個 Decorator 都無法和 @observable 這個同時使用。
為什么呢?問題就出在我們改寫屬性的 getter 和 setter 的實現原理上。首先,每次給一個屬性定義 getter 和 setter 都會覆蓋前一次的定義,也就是這個動作只能有一次。然后,mobx 的實現非常依賴對 getter 和 setter 的定義(可以參考我之前的文章:如何自己實現一個 mobx - 原理解析)
事實上,Decorator 本身疊加使用時沒問題的,因為你的每次包裝,都會將屬性的 descriptor 返回給上一層的包裝,最后就是一個函數包函數包函數的效果,最終返回的還是這個屬性的 descriptor 。
進階實現
那我們就需要摒棄掉定義 getter 和 setter 的實現方式。其實除了這種方式,還有很多方式可以實現上述的功能,核心就是一點,在裝飾器函數里,將你需要處理的屬性和對這個屬性需要做的處理的對應關系都記錄下來,然后在處理實例化數據和序列化數據的時候,把對應關系取出來,執行相關邏輯即可。
廢話不說,我們直接上一種將這個對應關系掛載到類的原型上的一個實現方式。
function Check (type) {return function (target, name, descriptor) {let v = descriptor.initializer && descriptor.initializer.call(this);/*** 將屬性名字以及需要的類型的對應關系記錄到類的原型上*/if (!target.constructor.__checkers__) {// 將這個隱藏屬性定義成 not enumerable,遍歷的時候是取不到的。Object.defineProperty(target.constructor, "__checkers__", {value: {},enumerable: false,writeable: true,configurable: true});}target.constructor.__checkers__[name] = {type: type};return descriptor} }復制代碼注意,我前面提到的一個信息,裝飾函數的第一個參數 target 是包裝屬性所屬的類的原型(prototype),這個通過看 babel 編譯后的結果可以看到。然后我這里為什么將對應關系掛載到 target.constructor 上,是因為我所有的 Model 類,都是繼承自我提供的一個 Model 基類的(BaseModel),target 拿到的不是子類的原型,而是基類的原型,target.constructor 拿到的才是最終的子類。也就是我把對應關系掛載到了開發定義的子類上。
接下來看看基類的代碼,核心提供兩個方法,分別是映射數據和序列化的方法。
class BaseModel {/*** 將后端數據直接映射到當前的示例上*/__map (json) {let alias = this.constructor.__aliasNames__;let units = this.constructor.__unitOriginals__;let checkers = this.constructor.__checkers__;for (let i in this) {if (!this.hasOwnProperty(i)) return;// 如果有多層裝飾器,需要經過多個邏輯處理最終產生一個最終值 realValuelet realValue = json[i];// 接下來一步一步處理數據// 首先檢查別名數據,并做映射if (alias && typeof(alias[i]) !== 'undefined') {// ......}// 然后針對數據檢查類型if (checkers && checkers[i]) {// ......}// 最終,對數據做單位轉換if (units && units[i]) {// ......}// 賦值this[i] = realValue;}}/*** 復寫 JSON.stringify 時自動調用的函數*/toJSON () {let result = {};let units = this.constructor.__unitOriginals__;for (let i in this) {if (!this.hasOwnProperty(i)) return;if (units && units[i]) {// 序列化時,有需要加單位的加上單位result[i] = this[i] + '_$' + units[i];} else {result[i] = this[i];}}return result;} }復制代碼在 __map 函數中,我們將當前類(this.constructor)上的對應關系都取出來,然后做數據校驗和映射,這里應該不難理解了。
最終應用的代碼就是我們開篇貼出來最終使用的代碼,只要相應的 Model 類繼承自 BaseModel 即可。
通過這樣的方式實現的 Decorator ,因為沒有用到任何 getter setter 相關的功能,所以可以和 mobx 這樣的庫完美融合,并且可以無限疊加使用,不過如果你用到了多個三方庫,他們都提供了對應的 Decorator,然后又都修改了 getter 和 setter,那就沒有辦法了!
總結
Decorator 雖然原理非常簡單,但是的確可以實現很多實用又方便的功能,目測前端領域很多框架和庫都會大規模使用這個特性,但是也希望這些庫在實現 Decorator 的時候考慮下通用性,考慮下疊加和共存的問題。像上面 mobx 的 @observable,不關無法疊加,而且和我自己實現的 Decorator 的順序都不能亂,必須在最外層,因為它改變了整個屬性的性質,不寫在最外層的時候,會發現一些莫名其妙的問題。
總結
以上是生活随笔為你收集整理的JS 装饰器(Decorator)场景实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从Adobe Photoshop CC
- 下一篇: Spring Cloud构建微服务架构—