自从react hook出世之后,整个前端界框架的数据管理方案就一发不可收拾地涌入组合式API中。其中,Vue2.7的时候,就已经推出一个composition-api方案,到vue3的发布,这个方案转正为Vue3 组合式API,也就是也就是所谓的script setup。
而自从 组合式API问世后,不同人有不同的看法,褒贬不一,连我都写过反对使用组合式API的文章。但是嘛,任何事物都有其存在的合理性,既然setup成为vue3的官方API,必然有其可取之处。
可能你正在犹豫是否要使用组合式API,纠结要不要使用script setup,可能你正在遭受着从选项式转组合式的痛苦之中,也可能你就是坚定地使用选项式。
但无论如何,我都向你分享一下我们团队使用vue3的心路历程,从一开始兴奋无比,到手足无措,到全员嫌弃,再到偶现亮光,最后到全员追捧的过程。
希望通过我们团队的经历,让你能够了解到如何更好地用上组合式API.
0x00 猴王出世:vue3来了
记得是2022年,vue3发布了,恰好我们公司有一个新的内部项目,我们前端团队全员对视一眼,一致通过了使用vue3来开发新项目。
使用新项目如何不令人兴奋呢?于是什么都是新的,直接抛弃掉了我们使用多年的webpack配置,直接上vite;抛弃掉vuex,直接上pinia;抛弃掉vue2,直接上vue3。
这些都好说,毕竟我们团队能在react、vue、微信小程序那套Component框架之间切换自如,何况是小小的vite和vue3?
但在对于写vue组件时,到底还是沿用vue2的选项式,还是采用新的组合式API时,团队里有了不同看法。老同志认为,项目第一,还是沿用旧写法比较好。但对于大部分小同志来说,既然要追求刺激,那就贯彻到底。经过投票表决,整个团队All in组合式API。
0x01 script setup?不过如此嘛
我们团队遵循着vue3官方文档,开始使用<script setup>
来组织vue组件的代码。
破除掉了对象和类条条框框的限制,写起代码来是真的简单,毕竟从面向对象转向命令式编程,有种从C++转C的简单。写起来就是普通的js,相比原来的选项式API,看起来组合式API能够更简单地组织起更复杂的业务逻辑。
除了最开始对于ref
和reactive
这些新API有点懵之外,我们团队也迅速尝到了甜头,因为写起来是真的快,组织代码也是非常简单。
至于如何使用ref
和reactive
,我们后来也总结了一套简单的使用办法:Vue的ref、shallowRef、reactive到底要怎么用!!?😵 - 掘金 (juejin.cn)
简单来说就是:
- 只用
ref
声明响应式变量 - 只有当第三方库的操作对象需要具备响应性时,才使用
shallowRef
- 只有当使用组合式函数需要命名空间时,才使用
reactive
不过那时候,我们只有第一条规则。
0x02 危机初显:产品经理改业务逻辑!
如果没有测试和产品经理,可能我们就会一直懵懵懂懂地保留这种写法一直写下去。
哪个程序员不喜欢写后即焚的程序呢?
可惜这个世界没有十全十美。产品经理过来改逻辑了,然后你不得不去改别人的代码。
一般来说,改自己的代码还好。
但是改别人的代码,那就要命了。
当我们互相打开彼此代码,然后我们彼此都崩溃了。
只见整个组件密密麻麻的const
和function
,然后我们要在密密麻麻的const
和function
里面找到需要更改的变量或者函数,这个行为堪比在屎坑里掘金,恶心又不得不做。
还不如选项式API呢,至少方法定义在哪里,数据是怎么定义的,我们都有规范去约束。
于是我们团队又吵了起来,从最开始相互指责,到后面又达成共识:组合式API就是一坨屎,尤雨溪就是个大傻逼。
0x03 既然难搞,那就别搞了
于是我们都退回了选项式API。
那段时间我也发表了不少setup不行的言论。
选项式API的好处,是在于能够约束好团队各个成员的写法风格,能够让你打开别人代码时不会感受到差异性。选项式API也很优秀,除了无法做到继承,本质上也是一个面向对象的写法。
组合式API太灵活了,本质上就是命令式编程,上手非常简单,去实现单个复杂逻辑时也容易,但是多个逻辑一耦合起来,命令式编程就扛不住了,写着写着就变成了一坨剪不断理还乱的面条。
那能否找到一种方法,既具备命令式的灵活性,又兼具面向对象这种高内聚可复用的特点?
0x04 游戏引擎:你们前端现在才搞组合啊?我们都已经好了!
2023年初,那时游戏《戴森球计划》很火。
这个小团队制作的国产游戏惊艳到的很多玩家,这个游戏的玩法很简单,玩家将在游戏中驾驶机甲,在不同星系中采集资源,建立星际运输物流网络,通过规划铺设基础设施,建造自动生产流水线,解锁新科技,打造庞大的工业帝国,最终建造太空巨构--戴森球!
不仅是玩法和画面上惊艳玩家,在游戏技术上,也是惊艳了很多游戏开发者。
为什么呢?因为游戏后期会有上百万千万个对象在运动,但是游戏帧率保持不变,换句话说,就是量大而不卡,很神奇。他们工作室介绍了很多优化措施,其中有一项叫做"采用ECS架构使得相同类型数据在内存中连续存储,所以寻址查找非常快"。
内存中连续存储的数据查找很快这个我懂,但什么是ECS架构?于是我就去查了一下。
简单来说,过去游戏领域也是使用面向对象编程,但是遇到了两个问题:
第一个问题就是上面说的,面向对象里数据的存储是分块的,比如英雄的血条,在内存查找的时候,你是要先找具体的英雄对象,再找到英雄对象下的血条属性,他们认为查找的效率是低的。
第二个问题就是使用继承的问题,比如你有一个商人的类,又有一个怪物的类,这时候游戏策划了一个叫做"怪物商人"的角色,那实现上,要从商人类继承还是从怪物类里继承?但最终无论如何,是不是另外的类里的属性方法,在怪物商人这个类里都要copy一份。
所以,他们游戏领域基于"组合优于继承"的设计思想,提出了ECS架构:实体(Entity)、组件(Component)和系统(System)。实体是什么呢?实体是一个连续的id列表,组件在这里指的是某种数据的结构体,系统就是实现某个或者多个行为的函数。
具体用法是这样的,以上面的怪物商人为例子,首先,原本属于"怪物"类和"商人"类的所有属性和方法全部拆分出来,比如说,拆成血条组件、攻击力组件、位置组件、商品组件......,拆成攻击系统、移动系统、交易系统,掉落系统......
组件\实体 | 商人 | 怪物 | 怪物商人 | 无敌的商人 |
---|---|---|---|---|
位置组件 | Y | Y | Y | Y |
血条组件 | Y | Y | Y | |
攻击力组件 | Y | Y | ||
商品组件 | Y | Y | Y |
那么当实现商人的时候,就可以创建一个实体,命名为商人,然后挂载血条组件、位置组件、商品组件。
当实现怪物的时候,就可以创建一个实体,命名为怪物,然后挂载上位置组件、血条组件、位置组件、攻击力组件。
当实现怪物的时候,就可以创建一个实体,命名为怪物商人,然后把上面所有组件都塞进去。 然后系统就是根据实体具备什么组件,判断可以执行什么行为的。比如有位置组件就可以触发移动系统,有商品组件就可以触发交易系统等等。
游戏领域通过ECS架构解决了他们的问题,有什么样的数据触发什么样的系统,有什么样的数据构成什么样的角色。
那回过头来看,说了这么多,跟vue3组合式有什么关系呢?
总不能因为是一句"组合优于继承"吧?
0x05 再次实践:来自ECS的启发
当那天我了解了ECS的思想后,再回过头来看之前所谓的"Vue组合式",我笑了,原来团队里大家跟我一样,用面向对象思维去写命令式编程。
怎么说呢?
就是大家都是不约而同地采用选项式的思路,把所有变量都声明在一个地方,把所有函数都声明在另一个地方。页面虽然没有data
属性,但实际上我们用const a = ref()
这种写法代替了data属性,用function
这种声明代替了methods属性。
这种做法本质还是选项式一样的。
而正确的组合式该怎么写呢?
还记得《戴森球计划》使用ECS解决了什么问题了吗?
采用ECS架构使得相同类型数据在内存中连续存储,所以寻址查找非常快
而我们debug的时候,本质上也是一种"寻址",从代码里的每一行找到要的变量或者方法。在选项式中,相同逻辑的数据、计算属性、声明周期、方法都是离散的,CPU查离散的指针都慢,何况是人脑,所以你会觉得乱,查起来痛苦,查起来非常慢。
但只要类比ECS的做法,我们把相同逻辑的代码,以一种连续的方式组织在一起。这样,只要你找到对应的逻辑,那么你的目标就是在接下来这几十行里。所以即使你在一个文件里面塞了几万行代码,运用这个思路,相信也能够很好的维护你的代码。
刚好那段时间我拿了一个小项目,要用uniapp去写小程序,而uniapp又恰好把vue3给支持了,于是我就拿这个小项目来试试水,验证一下我这个思路对不对。
结果效果出奇的好!以前写起来非常复杂的逻辑,采用新的思想来改造后,变得十分简单了。
特别是里面有一个场景,使用App进行医疗会诊功能,在客户强烈要求下,这个功能被要求转写为小程序端。
好在项目初期已经使用uniapp vue3进行开发,好在一开始就使用组合思想进行编写代码,利用逻辑连续的代码和vue3组合式函数的特性,我非常容易地就把共性模块抽离出来,然后花几个小时把小程序不支持的逻辑模块写了一下,其他的照搬App的代码,一下子就把小程序端的医疗会诊功能给做出来了。
放在vue2的时代,这简直是不可能的,首先vue2要把逻辑抽出来非常困难,一般情况下都只能封装成组件又或者整个vue文件复制粘贴一份。
但现在,各个App和小程序都支持的模块,都是以ts文件进行存储的。当需求更改时,仅仅改动一个ts文件,则App端和小程序端就都生效了!
于是,我非常高兴地在团队里分享了我的实践,期望能够在团队推广开来。
0x06 继往开来:vue3的组合优于继承
当我在团队里分享完后,听着同志们一声又一声的"卧槽!?",我就知道,尤雨溪搞的东西真正触动了他们的灵魂,尤大,真神人也!
团队里面也开始进行了实践,并总结出了一个行之有效,并能有效提高我们代码的可阅读性的规范:
- 逻辑模块聚合原则:相同逻辑的代码必须写在一起
- 业务代码优先原则:在无复用的需求下,不需要将业务代码高度封装成组合式函数。
- 核心模块保留原则:一个vue组件改动最频繁且无复用的逻辑代码,属于核心代码,应该保留在vue组件里而不是ts文件里。
js
/**@module XX1模块 - 日常开发时优先采用这种命令式直接写逻辑*/
const a = ref()
const b = computed()
onMounted(()=>{})
function todoA(){}
/**@module XX2模块 - 当XX2模块被复用时,可以采用组合式函数进行封装*/
const {c,d,todoC} = useXX2()
/**@module XX3模块 - 当需要给XX3模块添加一个命名空间时,可以加上reactive包装*/
const xx3 = reactive(useXX3())
如果你是选项式的粉丝,你甚至可以这样玩:
js
/**@module 模块1*/
const datasetMod = reactive(useOptAPI({
data(){
return {
dataset:{}
}
},
methods:{
async req2GetData(){}
}
}))
/**@module 模块2*/
const fileListMod = reactive(useOptAPI({
data(){
return {
atvFile:"",
fileList:[],
fileListRef:null
}
},
computed:{
filterFileList(){}
},
watch:{
atvFile(){}
},
methods:{
async req2GetFileList(){},
async onNextFile(){},
async onPrevFile(){}
}
}))
可自行封装或者找找有没有人做好的现成的hook,然后把相同逻辑的以选项式的方式写一起,然后整个script setup由多个选项式的逻辑API组合在一起。
另外,
在我们医疗业务,过去我们连继承都不搞,直接复制粘贴,比如产品卖给了A医院后又卖给了B医院,而B医院有自己的定制化的需求时,最开始是复制一个vue组件应付B医院,随着B医院的定制化需求越来越多,后面干脆整个项目开一个新分支。
但是当A医院、B医院、C医院、D医院...这些医院都存在需求要改的时候(往往是卫健局的某个红头文件导致的),我们几乎需要把所有医院的分支都给它改一遍。
现在,
我们采用组合式函数的方式,可以做到快速将逻辑分模块,然后模块可以快速拆分,共性模块不动,非共性模块保留在组件内。通过组件封装+组合式函数封装,我们也逐步可以做到像ECS那样,通过不同的模块组合,来完成多家医院的共性需求。
模块\医院 | A医院 | B医院 | C医院 | D医院 |
---|---|---|---|---|
挂号 | Y | Y | 定制 | Y |
缴费 | 定制 | Y | 定制 | Y |
报告 | Y | Y | Y | Y |
上面就是一个简单的例子,我们是实现了【挂号】【缴费】【报告】的组件和相关组合式函数,当C医院要求定制挂号时,定制的是挂号里的一个小流程,我们会去实现一个【use挂号小功能】的组合式函数。当定制的是一个大流程,可复用的内容较少时,我们会直接实现一个【C医院-挂号】的组件,然后替换掉原来页面里面的挂号组件。
最后通过宏编译的模式,根据不同医院,按需编译。