Vue 3.5发布之后,我们团队的几个项目都陆续升级了。经过一年多的持续迭代,积累了不少经验,也踩了不少坑。

这篇文章我想分享一下在Vue 3.5+项目持续迭代过程中的最佳实践。从项目架构、状态管理、性能优化到工程化,都是我们在实际项目中验证过的经验。

如果你正在使用Vue 3.5+,或者准备升级,希望这些经验能帮到你。

项目架构的演进

先说项目架构。Vue项目的架构不是一成不变的,随着项目规模增长,架构需要持续演进。

我们项目初期用的是比较简单的结构,components、views、router、store几个目录。但随着业务越来越复杂,这种扁平结构很快就不够用了。后来我们改成了按业务模块划分的结构,每个业务模块有自己的components、composables、types、utils。

这种按模块划分的结构有几个好处。第一是模块内聚,相关的代码都放在一起,找起来方便。第二是便于团队协作,每个人负责一个模块,不容易冲突。第三是便于代码复用,一个模块可以很方便地抽出来给其他项目用。

Vue 3.5之后,我们进一步优化了架构。利用Vue 3.5的新特性,比如更好的TypeScript支持、更完善的defineModel、useTemplateRef等,我们把很多通用逻辑抽成了composables,组件变得更薄更纯粹。

这里有一个经验是,不要一开始就设计过于复杂的架构。先从简单的开始,随着项目增长逐步重构。过早的架构优化往往是过度设计,反而增加了复杂度。

Composition API的使用原则

Composition API是Vue 3的核心,但很多人用得不好。我们在持续迭代中总结了几个使用原则。

第一个原则是composable要单一职责。一个composable只做一件事,不要把不相关的逻辑塞到一个composable里。比如useUser和useAuth应该分开,虽然它们都和用户相关,但职责不同。单一职责的composable更容易测试、更容易复用。

第二个原则是composable要返回稳定的引用。Vue 3.5对响应式系统做了优化,但如果composable每次调用都返回新的对象或函数,还是会导致不必要的重渲染。我们的做法是,在composable内部用ref和computed定义状态,用普通函数定义方法,返回的时候保持引用稳定。

第三个原则是合理使用shallowRef和shallowReactive。对于大型对象或者不需要深度响应式的数据,用shallowRef和shallowReactive可以大幅提升性能。比如从后端拿到的列表数据,通常只需要替换整个列表,不需要深度监听每个字段的变化,这时候用shallowRef就够了。

第四个原则是避免在setup顶层写副作用。副作用应该放在onMounted、watchEffect等生命周期或侦听器里,不要在setup顶层直接执行。因为setup在服务端渲染的时候也会执行,顶层的副作用可能会导致问题。

状态管理的选择

状态管理是Vue项目中最容易纠结的问题。Pinia已经成为了官方推荐的状态管理库,但不是所有状态都应该放在Pinia里。

我们的经验是,按状态的作用范围来决定放在哪里。组件内部的状态放在组件里,多个组件共享的状态放在composable里,全局的状态放在Pinia里。

很多人喜欢把所有状态都放到Pinia里,觉得这样统一管理。但实际上,很多状态只在一个组件或者几个相关组件里用,放到全局store里反而增加了复杂度。我们的做法是,先用组件本地状态,需要共享的时候再提升到composable,只有真正全局的状态才放到Pinia。

Pinia的使用也有一些最佳实践。第一是store要按领域划分,不要搞一个大而全的store。比如userStore、cartStore、orderStore,每个store管一个领域。第二是用setup store的写法,比options store更灵活,也更符合Composition API的风格。第三是合理使用getters,不要在组件里重复计算派生状态。

Vue 3.5之后,Pinia也做了相应的优化,和Vue的响应式系统结合得更紧密。升级之后,我们发现store的性能有一定提升,特别是在大型项目中。

性能优化实践

性能优化是持续迭代中永恒的话题。我们在Vue 3.5+项目中做了很多性能优化,这里分享几个效果明显的。

第一个是合理使用v-memo。v-memo是Vue 3.2引入的指令,可以缓存组件或元素的渲染结果,只有依赖变化时才重新渲染。对于大型列表,v-memo的效果非常明显。我们有一个渲染几百条数据的列表,加了v-memo之后,滚动流畅度提升了很多。

第二个是使用shallowRef优化大型数据。前面提到过,对于从后端拿到的大型列表数据,用shallowRef比ref性能好很多。因为ref会深度代理整个对象,数据量大的时候代理的开销不小。shallowRef只代理第一层,性能好很多。

第三个是合理使用defineAsyncComponent做路由懒加载。这个大家都知道,但很多人只做了路由级别的懒加载,没有做组件级别的。对于一些不常用的大组件,比如复杂的弹窗、编辑器、图表,可以用defineAsyncComponent异步加载,减少首屏包体积。

第四个是利用Vue 3.5的响应式优化。Vue 3.5对响应式系统做了很多优化,比如更好的依赖收集、更少的内存占用。升级之后,我们发现很多页面的渲染性能有自然提升,不需要额外优化。

第五个是避免不必要的响应式。不是所有数据都需要响应式。对于常量、配置项、不需要触发视图更新的数据,不要用ref或reactive包裹。直接用普通变量就行,这样可以减少响应式系统的负担。

TypeScript的最佳实践

Vue 3.5对TypeScript的支持更加完善了。我们项目全面启用了TypeScript,这里分享一些经验。

第一个是充分利用Vue 3.5的类型推导。Vue 3.5的defineProps、defineEmits、defineModel等宏的类型推导比以前更准确了。尽量用类型推导而不是手动标注类型,这样代码更简洁,也不容易出错。

第二个是为composable写好返回类型。虽然TypeScript能推导composable的返回类型,但显式声明返回类型有几个好处:一是作为文档,让人一眼看出composable返回什么;二是防止内部实现变化导致返回类型意外改变;三是IDE的提示更准确。

第三个是合理使用类型断言。有时候TypeScript的推导不够准确,需要用类型断言。但不要滥用类型断言,每一个断言都应该有充分的理由。如果发现自己频繁使用类型断言,说明类型设计可能有问题。

第四个是用vue-tsc做类型检查。不要只靠IDE的类型提示,在CI流程里加上vue-tsc的类型检查。这样可以在提交代码的时候就发现类型错误,而不是等到运行时才发现。

工程化和持续迭代

持续迭代不仅仅是写代码,工程化也很重要。

第一个是建立完善的代码规范。我们用ESLint加Prettier,配合Vue官方的eslint-plugin-vue,统一代码风格。规范不是为了限制自由,而是为了让团队的代码保持一致,降低维护成本。

第二个是自动化测试。Vue 3.5之后,Vitest和Vue Test Utils的配合更加顺畅了。我们为composable和工具函数写单元测试,为关键组件写组件测试,为核心流程写端到端测试。测试不是越多越好,而是要覆盖关键路径。有了测试,重构的时候就有底气。

第三个是CI和CD。我们用GitHub Actions做CI,每次提交自动运行lint、类型检查、测试、构建。通过之后自动部署到测试环境。这样可以保证主干代码的质量,也加快了迭代速度。

第四个是定期重构。持续迭代的过程中,技术债务会不断积累。我们每个迭代都会留一部分时间做重构,把之前欠下的技术债务还掉。不要等到技术债务积累到无法维护的时候才重构,那时候成本就太高了。

升级Vue 3.5的注意事项

最后说说升级Vue 3.5的注意事项。

第一个是检查依赖兼容性。升级之前,先检查项目用到的第三方库是否支持Vue 3.5。大部分主流库都已经支持了,但一些小众库可能还没适配。如果有不兼容的库,需要找替代方案或者等库更新。

第二个是关注废弃API。Vue 3.5废弃了一些旧的API,比如某些全局配置、某些生命周期写法。升级之后,控制台会有警告,根据警告逐一修复就行。

第三个是充分测试。升级之后一定要做充分的测试,特别是核心业务流程。虽然Vue 3.5是向后兼容的,但总有一些边缘场景可能出问题。我们升级的时候就遇到了一个第三方组件和Vue 3.5不兼容的问题,好在测试的时候发现了。

第四个是渐进式升级。如果项目很大,可以先升级一个模块或者一个页面,验证没有问题之后再全面升级。不要一下子全部升级,出了问题不好定位。

写在最后

Vue 3.5是一个很成熟的版本,在性能、TypeScript支持、开发体验上都有很大提升。但工具只是工具,真正决定项目质量的还是工程实践和团队协作。

这篇文章分享的经验都是我们在实际项目中踩坑踩出来的。每个项目的情况不同,这些经验不一定完全适合你,但希望能给你一些参考。

持续迭代是一个长期的过程,没有银弹,只有不断地学习、实践、反思、改进。保持对新技术的关注,同时脚踏实地做好每一个细节,项目才能越做越好。

最后用一句话来结束这篇文章:框架的价值不在于它有多新,而在于你用得有多好。

愿每一个Vue开发者,都能在持续迭代中写出高质量的代码。