我们团队是如何用好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医院-挂号】的组件,然后替换掉原来页面里面的挂号组件。

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

相关推荐
轻口味5 分钟前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami7 分钟前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
吃杠碰小鸡42 分钟前
lodash常用函数
前端·javascript
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O1 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235951 小时前
web复习(三)
前端
迷糊的『迷』1 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886351 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app