前端代碼的三種設計模式

語言: CN / TW / HK

為了便於理解,以下代碼示例採用的都是 React + rdeco 編寫,設計模式本身是高度抽象的,並不侷限於某一類特定的框架

組件模式

組件模式是我們用的最多的或者説目前大家都唯一能夠理解的模式,組件模式的特點是,予以每個組件獨立的上下文,組件和組件之間有嚴格的代碼隔離,通常在不考慮全局變量的影響下組件之間是完全無潛在交互的。

``` const Table = createComponent({   name:'table',   state:{     data:[],   },   view:{     render(){       return(         

{this.state.data.map(d=>{           return d         })}
      )     }   } })

const Page = createComponent({   name:'page',   view:{     render(){       return(                )     }   } })

```

這種模式我們都很熟悉,Page 和 Table 是兩個擁有獨立上下文的組件,在不同的 UI 框架裏有不同的組件交互方式,在 React 中,Page 如果要和 Table 進行交互,可以使用 props 傳遞,或者藉助 Context 來共享一部分上下文。

但是這種模式在很多場景下並不是完全有效的,只有當我們非常明確兩個組件之間的邊界時,模式和實際情況才是相符合的,例如考慮這樣一種場景

``` const HeadTitle = ({text})=>{   return(     

{text}

  ) } const Page = createComponent({   name:'page',   state:{     text:'page',   },   view:{     render(){            }   } })

```

在這個示例中,乍看是沒啥問題,平時我們都會將一些無狀態的 UI 提取為無狀態的函數組件,但經過實踐你會發現實際上,HeadTitle 大概率僅服務於 Page,也就是説 HeadTitle 並不是為了複用而被提取,更多是因為大型組件的文件需要拆解從而減小體積,降低管理難度。

但是以此為目的進行組件化拆解會破壞原有組件的完整性,導致大量的參數傳遞,這和我們過度提取代碼到函數其實是一個效果。

``` function print(name){   console.log(name) } function main(){   const name = 'main'   print(name) } // 如果 print 在 main 函數內部則不需要再次傳遞 name function main(){   const name = 'main'   function print(){     console.log(name)   }   print(name) } // 因此對於 main 來説 print 是一個獨立函數?,還是一個代碼片段?

```

為了解決組件提取導致的上下文隔離問題,我們實踐了一種模式,我們稱之為組合模式

組合模式

和組件模式相比,組合模式是一種輕量化的方案,相比組件模式兩者有明顯的區別

  • 組件模式擁有獨立的上下文,組件和組件之間組合成新的組件需要進行上下文的傳遞,而組合模式則只是組件的一個片段,若干個組合體組成了一個完整組件,組合體之間共享上下文,不需要額外傳遞,但組合體本身實現了相關邏輯的內聚
  • 組件和組件之間因為上下文隔離,因此可以擁有相同的內部成員,組合體只是組件的一個片段,組合體之間不能用相同的內部成員。
  • 組件有實例,需要命名標識,組合體沒有實例,不需要命名標識

參照以上區別我們來看看的代碼示例

``` const table = createCompose({   view:{     renderTable(){       return(         

{this.state.data.map(d=>{           return d         })}
      )     }   } })

const head = createCompose({   state:{     text:'page'   },   view:{     renderHead(){       return(         

{text}

      )     }   } })

const Page = createComponent(compose({   name:'page',   state:{     data:[]   },   view:{     render(){       <>         {this.view.renderHead()}         {this.view.renderTable()}            }   } },[table, head]))

```

現在 head 和 table 都成了組合體,通過組合變成了 page 的一部分,為此他們可以共享彼此的上下文,而不用額外通過 props 或者 Context 來傳遞或者共享參數

除了組合模式,我們還總結了第三種模式,membrane 模式,這種模式我在早期的文章中有提到過,今天我們將其簡化。

Membrane 模式

和組合模式相比,membrane 模式具有一些共通性,例如同樣沒有獨立的上下文,不需要命名標識,不過兩者也有極大的區別

  • membrane 是一種抽象模式,和組合模式相比,每個 membrane 只能有一個模板
  • compose 和 membrane 可以聯合使用

``` const pageTemplate = () => {   return {     state:{       name:'',     },     service:{       init(){}     },     controller:{       onMount(){         this.service.init()       }     },     view:{       render(){         return(           

{this.state.name}
        )       }     }   } }

const Page1Membrane = createMembrane(pageTemplate(), {   name:'page-1-membrane',   service:{     init(){       this.setter.name('page-1-membrane')     }   } })

const Page1Membrane = createMembrane(pageTemplate(), {   name:'page-1-membrane',   service:{     init(){       this.setter.name('page-2-membrane')     }   } })

const Page1 = createComponent(Page1Membrane) // render Page1 name === page-1-membrane

const Page2 = createComponent(Page2Membrane) // render Page2 name === page-2-membrane

```

如果你熟悉面向對象設計,那麼可能會很快聯想到 membrane 和 抽象類的特性有些相似,不過相比抽象類,membrane 可以包含具體的實現,因此兩者也不完全等價,但是從設計上是有一定的共通性的

在實際實踐中,我們結合上述三種模式,藉助類似 mermaid 這樣的 UML 圖形庫,在日常迭代中增加了前端設計相關的內容,從實際結果看我們認為這些模式有助於改善前端設計的粗糙和非專業性,同時可以改善前端代碼的標準化程度,利用 UML 圖更好的代替註釋和文字文檔來描述業務代碼的組成關係。