前端代码的三种设计模式
为了便于理解,以下代码示例采用的都是 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 图更好的代替注释和文字文档来描述业务代码的组成关系。
相关推荐HOT
更多>>线程安全与线程不安全?
线程安全(ThreadSafety)和线程不安全(ThreadUnsafety)是在多线程编程中用来描述代码或数据结构在并发环境中的行为的概念。线程安全指的是在多线...详情>>
2023-06-05 17:26:00如何在javascript中获取当前日期
在JavaScript中,可以使用内置的Date对象来获取当前日期和时间。可以使用以下代码来获取当前日期:这段代码会创建一个新的Date对象,该对象表示...详情>>
2023-03-17 15:41:10JavaScript模块化规范有哪些
在JavaScript中,常用的模块化规范有以下几种:CommonJS规范:主要用于服务器端编程,是Node.js采用的模块化规范。通过require方法加载模块,通...详情>>
2023-03-06 15:33:52kafka的应用场景有哪些?
Kafka 是一个高吞吐量、分布式的消息队列系统,被广泛应用于以下场景:1.日志收集:Kafka 可以作为一个高效的日志收集器,收集分布在不同服务器...详情>>
2023-03-03 10:54:49