我们团队是如何用好vue3 setup组合式API的?

自从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能够更简单地组织起更复杂的业务逻辑。

除了最开始对于refreactive这些新API有点懵之外,我们团队也迅速尝到了甜头,因为写起来是真的快,组织代码也是非常简单。

至于如何使用refreactive,我们后来也总结了一套简单的使用办法:Vue的ref、shallowRef、reactive到底要怎么用!!?😵 - 掘金 (juejin.cn)

简单来说就是:

  1. 只用ref声明响应式变量
  2. 只有当第三方库的操作对象需要具备响应性时,才使用shallowRef
  3. 只有当使用组合式函数需要命名空间时,才使用reactive

不过那时候,我们只有第一条规则。

0x02 危机初显:产品经理改业务逻辑!

如果没有测试和产品经理,可能我们就会一直懵懵懂懂地保留这种写法一直写下去。

哪个程序员不喜欢写后即焚的程序呢?

可惜这个世界没有十全十美。产品经理过来改逻辑了,然后你不得不去改别人的代码。

一般来说,改自己的代码还好。

但是改别人的代码,那就要命了。

当我们互相打开彼此代码,然后我们彼此都崩溃了。

只见整个组件密密麻麻的constfunction,然后我们要在密密麻麻的constfunction里面找到需要更改的变量或者函数,这个行为堪比在屎坑里掘金,恶心又不得不做。

还不如选项式API呢,至少方法定义在哪里,数据是怎么定义的,我们都有规范去约束。

于是我们团队又吵了起来,从最开始相互指责,到后面又达成共识:组合式API就是一坨屎,尤雨溪就是个大傻逼。

0x03 既然难搞,那就别搞了

于是我们都退回了选项式API。

那段时间我也发表了不少setup不行的言论。

选项式API的好处,是在于能够约束好团队各个成员的写法风格,能够让你打开别人代码时不会感受到差异性。选项式API也很优秀,除了无法做到继承,本质上也是一个面向对象的写法。

组合式API太灵活了,本质上就是命令式编程,上手非常简单,去实现单个复杂逻辑时也容易,但是多个逻辑一耦合起来,命令式编程就扛不住了,写着写着就变成了一坨剪不断理还乱的面条。

那能否找到一种方法,既具备命令式的灵活性,又兼具面向对象这种高内聚可复用的特点?

0x04 游戏引擎:你们前端现在才搞组合啊?我们都已经好了!

2023年初,那时游戏《戴森球计划》很火。

这个小团队制作的国产游戏惊艳到的很多玩家,这个游戏的玩法很简单,玩家将在游戏中驾驶机甲,在不同星系中采集资源,建立星际运输物流网络,通过规划铺设基础设施,建造自动生产流水线,解锁新科技,打造庞大的工业帝国,最终建造太空巨构--戴森球!

不仅是玩法和画面上惊艳玩家,在游戏技术上,也是惊艳了很多游戏开发者。

为什么呢?因为游戏后期会有上百万千万个对象在运动,但是游戏帧率保持不变,换句话说,就是量大而不卡,很神奇。他们工作室介绍了很多优化措施,其中有一项叫做"采用ECS架构使得相同类型数据在内存中连续存储,所以寻址查找非常快"。

内存中连续存储的数据查找很快这个我懂,但什么是ECS架构?于是我就去查了一下。

简单来说,过去游戏领域也是使用面向对象编程,但是遇到了两个问题:

第一个问题就是上面说的,面向对象里数据的存储是分块的,比如英雄的血条,在内存查找的时候,你是要先找具体的英雄对象,再找到英雄对象下的血条属性,他们认为查找的效率是低的。

第二个问题就是使用继承的问题,比如你有一个商人的类,又有一个怪物的类,这时候游戏策划了一个叫做"怪物商人"的角色,那实现上,要从商人类继承还是从怪物类里继承?但最终无论如何,是不是另外的类里的属性方法,在怪物商人这个类里都要copy一份。

classDiagram 角色 <-- 商人 角色 <-- 怪物 商人 <-- 怪物商人:??? 怪物 <-- 怪物商人:??? class 角色{ x:number y:number 血条:number $移动(x:number,y:number) $受伤(n:number) } class 商人{ 商品:string[] 买(e:string) 卖(e:string) } class 怪物{ 攻击力:number $攻击() $死亡掉落() } class 怪物商人{ ? }

所以,他们游戏领域基于"组合优于继承"的设计思想,提出了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的组合优于继承

当我在团队里分享完后,听着同志们一声又一声的"卧槽!?",我就知道,尤雨溪搞的东西真正触动了他们的灵魂,尤大,真神人也!

团队里面也开始进行了实践,并总结出了一个行之有效,并能有效提高我们代码的可阅读性的规范:

  1. 逻辑模块聚合原则:相同逻辑的代码必须写在一起
  2. 业务代码优先原则:在无复用的需求下,不需要将业务代码高度封装成组合式函数。
  3. 核心模块保留原则:一个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医院-挂号】的组件,然后替换掉原来页面里面的挂号组件。

最后通过宏编译的模式,根据不同医院,按需编译。

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax