为了便于理解,以下代码示例采用的都是 React + rdeco 编写,设计模式本身是高度抽象的,并不局限于某一类特定的框架
组件模式
组件模式是我们用的最多的或者说目前大家都唯一能够理解的模式,组件模式的特点是,予以每个组件独立的上下文,组件和组件之间有严格的代码隔离,通常在不考虑全局变量的影响下组件之间是完全无潜在交互的。
const Table = createComponent({
name:'table',
state:{
data:[],
},
view:{
render(){
return(
<div>{this.state.data.map(d=>{
return d
})}<div>
)
}
}
})
const Page = createComponent({
name:'page',
view:{
render(){
return(
<Table />
)
}
}
})
复制代码
这种模式我们都很熟悉,Page 和 Table 是两个拥有独立上下文的组件,在不同的 UI 框架里有不同的组件交互方式,在 React 中,Page 如果要和 Table 进行交互,可以使用 props 传递,或者借助 Context 来共享一部分上下文。
但是这种模式在很多场景下并不是完全有效的,只有当我们非常明确两个组件之间的边界时,模式和实际情况才是相符合的,例如考虑这样一种场景:
const HeadTitle = ({text})=>{
return(
<p>{text}</p>
)
}
const Page = createComponent({
name:'page',
state:{
text:'page',
},
view:{
render(){
<HeadTtile text={this.state.text}>
}
}
})
复制代码
在这个示例中,乍看是没啥问题,平时我们都会将一些无状态的 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(
<div>{this.state.data.map(d=>{
return d
})}<div>
)
}
}
})
const head = createCompose({
state:{
text:'page'
},
view:{
renderHead(){
return(
<p>{text}</p>
)
}
}
})
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(
<div>{this.state.name}</div>
)
}
}
}
}
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 图更好的代替注释和文字文档来描述业务代码的组成关系。