☰
Vue 3全家桶实战指南:Vite构建、Pinia状态管理与迁移避坑
2026/10/17 8:11:05 网站建设 项目流程

1. 从 Vue 2 到 Vue 3 全家桶,绕不开的那道坎

Vue 3.0 正式发布到现在已经过了不少年头,如果你还在观望,或者刚接手一个 Vue 2 的老项目准备迁移,那这篇文章就是给你写的。Vue 3 全家桶并不是单纯把框架换了个版本号,而是整套生态工具链的一次集体升级:构建工具从 webpack 换成了 Vite,状态管理从 Vuex 换成了 Pinia,路由从 Vue Router 3 换成了 Vue Router 4,再加上 Composition API 这套全新的逻辑组织方式,说实话,变动幅度比我预想的大得多。

我自己从 Vue 2 项目迁到 Vue 3 的时候,踩过的坑不算少。最典型的就是:代码写着写着,reactive解构之后数据不响应了,页面怎么都不更新;还有路由守卫里拿不到组件的this,整个逻辑得重写;更别提 Vite 的生态兼容问题,很多老的 npm 包在 ESM 模式下直接跑不起来。这些事网上零零散散都有提,但很少有人把全家桶的完整使用场景串在一起讲清楚。

这篇文章以我个人实际使用的经验为主,从项目搭建开始,把 Vue 3 + Vite + Vue Router 4 + Pinia 这一整套东西的选型思路、核心用法、注意事项和排查技巧完整梳理一遍。适合的人群主要有两类:一类是从 Vue 2 迁移过来的老手,想快速知道两版之间到底差在哪、迁移时要改什么;另一类是刚开始学 Vue 3 的新人,希望直接上手一套规范且能落地的全家桶方案。如果你正准备在公司项目里推 Vue 3,这篇文章也能给你做决策时提供一些参考。

2. 全家桶方案选型:为什么是 Vite 加 Pinia

2.1 构建工具:Vite 对比 webpack,不只是快这么简单

很多人在做技术选型时会纠结:公司内部还有一堆老项目走 webpack,要不要统一?我的结论很直接:新项目直接用 Vite,别犹豫。Vite 启动项目的速度是秒级的,大型 webpack 项目冷启动可能要十几秒甚至更久,Vite 基于浏览器原生的 ESM 模块,只在启动时按需加载当前页面涉及到的模块,省去了整包编译的过程,这个差异用一次就回不去了。

但这不只是"快"的问题。Vite 的依赖预构建机制会把 CommonJS 格式的依赖统一转换为 ESM,这个机制帮我提前发现了不少老包的问题。比如有些 npm 包内部引用了window对象、process.env等 Node 环境变量,在浏览器原生 ESM 下直接报错,这时候就需要在 Vite 配置里做兼容处理。还有一个前车之鉴:项目里用了某款老版本的富文本编辑器组件,打包后字体文件路径死活不对,排查半天发现是 Vite 的静态资源处理规则和 webpack 不同,public目录和相对路径的解析方式有差异,解决方案是把资源改放到src/assets里通过模块方式引入。

2.2 状态管理:Pinia 取代 Vuex 的理由

Vuex 在 Vue 2 时代几乎是无处不在的存在,但到了 Vue 3 时代,Pinia 明显更顺手。核心原因有几个:

第一,Pinia 全面拥抱 Composition API,store 的定义方式天然和 setup 语法契合。你用ref、computed怎么写逻辑,在 Pinia 里就怎么写 store,不需要学一套额外的 Vuex 风格 API。第二,Pinia 的 TypeScript 推导非常友好,不需要像 Vuex 那样手写繁琐的模块类型声明。第三,Pinia 不再有mutations的概念,改状态直接调actions里的方法,少了一层概念。

我有个实际迁移的经历:某管理后台项目原来用 Vuex,有十几个 module,每个 module 里都写着 getters、mutations、actions 三件套,光模块文件就快一百个。迁移到 Pinia 后,模块数量没变,但每个文件的代码量少了一半以上,因为不再需要根据同步异步去拆分 mutations 和 actions,也不用写一堆 namespaced 配置。团队新人上手的难度明显降低。

2.3 全家桶版本怎么配

这里给出我验证过的稳定组合,可以直接照抄:

工具推荐版本说明
Vue3.4.x 及以上3.4 之后的模板编译器和响应式系统都比较稳定
Vite5.x如果还在用 4.x 也不用急着升,但新项目直接用 5
Vue Router4.2.x匹配 Vue 3 的 API 设计
Pinia2.x配合 Vue 3.3+ 有更好的类型推导
Vite 插件@vitejs/plugin-vue必须装,不然 .vue 文件没法编译

需要提醒的是,版本不要追新到 beta 或者 alpha。我有过一次教训:项目里用了当时还处于 RC 阶段的某个 Vite 插件版本,结果和一个核心依赖产生了兼容问题,排查到深夜才发现是版本冲突。全家桶方案讲究的是整体稳定,用 release 版本里最新的那个次版本号就够了。

3. Vue 3 核心响应式:从 ref 到 reactive 的完整理解

3.1 ref 和 reactive 到底怎么选

Vue 3 的响应式系统是整个组合式 API 的地基,但很多新手一上来就栽在ref和reactive的选择上。

先说结论:默认用ref,只有当你确定要定义一个对象且需要整体替换时才考虑reactive。为什么?ref的底层虽然也走 reactive,但它给值包了一层 Ref 对象,这个包装让它在任何赋值场景下都能保持响应式连接。而reactive直接代理原始对象,当你把对象里的某个属性解构出来单独使用,响应式连接就断了。

我举个实际例子。我之前写过一个表单组件,初始值是从接口拿到的对象:

const form = reactive({ name: '', age: 18, address: {} }) // 不能这样解构 const { name, age } = form // 此时 name 和 age 只是普通字符串和数字,修改它们不会触发视图更新

正确做法是用toRefs或直接全程用ref:

const formRef = ref({ name: '', age: 18, address: {} }) // 修改时通过 formRef.value 操作

实际上在我写过的业务代码里,reactive的使用频率远低于ref,因为ref在模板里自动解包,写起来并不费劲。只有像表单这种整个对象嵌套很深的场景,我会优先用reactive配合toRefs来代替旧版的data()。

3.2 深层监听和 shallowRef 的取舍

Vue 3 的watch默认是浅层的还是深层的,很多人没搞明白。实际上watch监听一个ref时,默认不会做深度监听,但如果你监听的是reactive对象,它又会自动开启深度监听。这个行为之间的差异如果不注意,很容易写出"看起来应该响应但实际没反应"的 bug。

举个例子:

const list = ref([{ id: 1, count: 1 }]) // 这样监听不会触发,因为默认是浅监听 watch(list, (newVal) => { console.log('list changed') }) // 需要加 deep 选项 watch(list, (newVal) => { console.log('list changed') }, { deep: true })

反过来,如果页面里监听的是一个很大的数据对象,每次变更都要做深度比较,性能会受影响。这种场景我推荐shallowRef,它只代理对象第一层,深层变化不会触发响应,适合那些数据结构很大、但只有整体替换时才需要更新的情况。典型的场景是富文本编辑器的初始内容、大表格组件的静态配置项,用普通ref会让性能白白浪费。

3.3 computed 和 watch 的最佳实践

computed适合派生状态,watch适合副作用处理,这个概念要分清。我见过不少项目把computed当成 watch 用,在 computed 里发请求改数据,这是典型的反模式。

正确的做法是:一个值如果能从其他状态计算得到,就用computed,不要额外定义一个变量去手动同步。比如列表的筛选结果:

const keyword = ref('') const allList = ref([]) const filteredList = computed(() => { const kw = keyword.value.trim().toLowerCase() if (!kw) return allList.value return allList.value.filter(item => item.name.includes(kw)) })

这里filteredList不需要手动维护,keyword或allList一变它就自动更新。而watch的使用场景是搜索防抖、路由参数变化后的数据请求、表单某个字段变化后的联动逻辑等。我在实际项目里的经验:先想想这个值能不能算出来,如果能,就不要用 watch 去"同步"它,代码会干净很多。

4. Composition API 的项目级组织方式

4.1 别再整个页面塞一个 setup 了

Vue 3 引入 Composition API 的初衷是把同一逻辑的代码聚合到一起,但如果你只是在setup里从上到下把变量和方法全部写完,效果跟 Vue 2 的 options 写反了——代码比 before 还乱。

我推荐按业务场景封装成组合式函数,就是所谓的composables。比如用户列表页,可以把加载、筛选、分页、删除这四件事拆成多个 hooks,这样每个函数文件只做一件事,测试和维护都简单。

一个典型的可复用组合式函数:

// composables/useTableList.js import { ref, computed } from 'vue' export function useTableList(fetcher, { pageSize = 10 } = {}) { const list = ref([]) const loading = ref(false) const currentPage = ref(1) const total = ref(0) const totalPages = computed(() => Math.ceil(total.value / pageSize)) async function loadList() { loading.value = true try { const { rows, count } = await fetcher({ page: currentPage.value, pageSize }) list.value = rows total.value = count } finally { loading.value = false } } function changePage(page) { currentPage.value = page loadList() } return { list, loading, currentPage, total, totalPages, loadList, changePage } }

页面里的用法就是:

const { list, loading, changePage } = useTableList(fetchUserList)

这种模式特别适合中后台系统,因为大量页面的列表行为是同构的,抽成 composable 后,新页面的列表功能十分钟就能写完。但这里我要泼一盆冷水:不要一上来就抽 hook,只有当你发现至少两三个页面都在用同一段逻辑时,再抽才值得。过度抽象在业务代码里是很常见的技术债来源。

4.2 setup 语法糖带来的双刃剑

<script setup>极大地简化了组件的写法,不需要写export default { setup() {...} }那一层,变量和函数可以直接在模板里使用。但方便归方便,它也有需要注意的地方。

最典型的坑有两个:

一是组件不能再通过config的方式被外部直接访问,如果你在 Vue 2 里习惯了通过this.$refs.child拿子组件实例,再调用子组件的方法,那在 script setup 里你得通过defineExpose主动暴露方法,否则外部拿到的实例是封闭的。

<script setup> function openDialog() { ... } defineExpose({ openDialog }) </script>

二是defineProps的默认值写法变了。Vue 2 写props: { name: { type: String, default: 'vue' } },Vue 3 用类型标注时要注意:

<script setup> const props = withDefaults(defineProps<{ name?: string age?: number }>(), { name: 'vue', age: 18 }) </script>

withDefaults帮你处理了默认值问题,但它只是一个编译期的工具,运行时的类型校验还需要额外处理。如果你需要更严格的 props 校验逻辑(比如自定义 validator),用纯 TypeScript 类型定义就不够用了,需要在defineProps里写完整的运行时配置。

我自己项目里的做法是:组件的 props 尽量用 TS 类型定义,配合withDefaults给默认值,简单场景已经够用;只有当组件需要对外提示传参格式时才回头用运行时校验。

4.3 生命周期和 Vue 2 的对应关系

Vue 3 把大部分生命周期钩子都改名了,但逻辑上是平移的,拿张对应表查就行:

Vue 2 选项Vue 3 组合式 API说明
beforeCreate不需要(setup 阶段)setup 本身就替代了这个阶段
created不需要(setup 阶段)同上
beforeMountonBeforeMount挂载前
mountedonMounted挂载完成
beforeUpdateonBeforeUpdate更新前
updatedonUpdated更新后
beforeDestroyonBeforeUnmount卸载前
destroyedonUnmounted卸载完成

这里有个容易忽视的点:setup的执行时机比created早得多,在组件实例创建之前,所以setup里拿不到this,也不能访问组件的 data 和 methods。这逼着你把所有逻辑外移到独立的函数或者直接用参数传递。

还要注意一个问题:如果项目里有第三方插件依赖实例化时绑定的事件(比如某个图表库监听页面的 resize 事件),你必须在onBeforeUnmount里清理掉,否则组件卸载后事件还在,页面会多出很多幽灵监听,性能慢慢就吃光了。

5. Vue Router 4 与 Pinia 的完整落地

5.1 路由配置的差异和动态路由处理

Vue Router 4 的配置 API 大框架没变,但有几个细节很不一样。首先是创建方式从new VueRouter()变成了createRouter({ history: createWebHistory(), routes }),简单理解就是路由变成了函数式工厂,这也是整个 Vue 3 风格统一的一部分。

动态路由这个东西,中后台项目几乎天天用。我的做法是:登录后根据后端返回的权限标识,动态添加路由。

const router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', component: LoginPage }, { path: '/', component: LayoutPage, children: [] } ] }) function setupDynamicRoutes(permissionList) { const routes = permissionList.map(perm => ({ path: perm.path, name: perm.name, component: () => import(`@/views${perm.componentPath}.vue`), meta: { title: perm.title, icon: perm.icon } })) routes.forEach(route => router.addRoute('LayoutPage', route)) }

这段代码有个坑:router.addRoute添加的路由如果和已有路由的 name 冲突,它不会覆盖原来的路由,而是完全忽略新路由。所以动态添加路由前,最好先router.removeRoute('xxx')清理掉旧的,否则权限更新后会看到路由一直不生效的诡异问题。

此外,动态 import 的路径不能是纯变量拼接,这是 Vite 的限制。import(@/views${perm.componentPath}.vue)这种写法,Vite 需要在构建时静态分析依赖路径,纯变量拼出来的路径它根本分析不到,构建产物里就没有那个组件。解决方法是维护一张完整的组件映射表:

const viewModules = { 'system/user': () => import('@/views/system/user.vue'), 'system/role': () => import('@/views/system/role.vue') }

这个坑我印象太深了,因为一开始没看 Vite 文档,动态路由加完后页面一直在转圈,控制台报"Failed to resolve import",排查了大半天才发现是构建工具的限制。

5.2 路由守卫里的异步顺序

路由守卫是每个后台系统的必经环节,最常见的需求是:进入页面前检查登录状态、拉取用户信息、判断权限。但这里非常容易出现死循环,因为beforeEach里一旦做异步操作,要特别注意别让路由跳转重复触发守卫。

我的做法是:在路由守卫里通过pinia中的 store 判断用户信息是否已加载,如果没加载就先拉取,加载完再next()放行。要注意的是 "放行后不会再触发一次守卫" 这个概念,所以在守卫里要避免再次跳转到当前页。

router.beforeEach(async (to, from, next) => { const useUserStore = getUserStore() const userStore = useUserStore() if (!userStore.token) { if (to.path !== '/login') { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } return } // 已登录但没有用户信息,先拉取 if (!userStore.userInfo) { try { await userStore.fetchUserInfo() next() } catch (e) { userStore.clearToken() next({ path: '/login' }) } return } next() })

这里必须强调的是:useUserStore()要在beforeEach回调里调用,不能放在模块顶层。因为在模块顶层执行时,Pinia 实例可能还没初始化,会直接报 "getActivePinia was called with no active Pinia"。这个报错我见了不少人问过,根源就在这里。

5.3 Pinia store 的正确拆法

Pinia 的 store 定义一个大文件放所有状态是我极力反对的。我建议按业务模块拆,比如用户相关、订单相关、配置相关都各自独立成一个 store 文件,每个文件只维护自己的状态和动作。store 之间如果需要互相访问,直接引用对方的实例,不需要像 Vuex 那样做模块间复杂映射。

一个用户模块的 Pinia store 示例:

// store/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: null, permissionList: [] }), getters: { isLogin: (state) => !!state.token, isAdmin: (state) => state.userInfo?.role === 'admin' }, actions: { setToken(token) { this.token = token }, async fetchUserInfo() { const res = await getUserInfoApi() this.userInfo = res.data }, async logout() { await logoutApi() this.token = '' this.userInfo = null this.permissionList = [] } } })

用起来很方便,组件里:

const userStore = useUserStore() // 模板中直接用 userStore.isAdmin、userStore.userInfo

可能你已经注意到 Pinia 的使用方式相比 Vuex 更像"一个带状态的管理对象",这也是大家说 Pinia 是"Vuex 的最终形态"的原因。没有 mutations 以后,一个人就能把一个模块的状态管理全部搞定,也减少了团队成员之间理解 API 的成本。

5.4 持久化方案:不装插件也能写

Pinia 本身不带持久化功能,如果你要刷新页面后保留登录状态,就得自己处理。这一块有两个方向:一个是用插件(比如某个叫 pinia-plugin-persistedstate 的库),另一个是自己在 action 里手动存。

我个人的经验是:不要在项目里为了一个持久化需求引入太多插件,因为插件本身也有兼容性和体积问题。简单做法是在关键 state 变更时手动写入 localStorage,初始化时读取:

export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null') }), actions: { setToken(token) { this.token = token localStorage.setItem('token', token) }, clearAll() { this.token = '' this.userInfo = null localStorage.removeItem('token') localStorage.removeItem('userInfo') } } })

如果你项目里的 store 状态特别多、又希望一次搞定所有字段的持久化,那用插件确实省事,但要注意插件的存储键名冲突和 JSON 序列化的性能问题。我在自己的项目里试过两种方案,最终选择了手写,因为可以精确控制哪些字段不持久化(比如临时角色切换的状态不需要存)。

6. Vite 构建配置与生产环境优化

6.1 路径别名和代理配置

Vite 配置里最常见的两个需求,一个是路径别名@指向src,另一个是开发环境代理后端接口。我给出一个经过验证的基础配置:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'node:path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, build: { sourcemap: false, chunkSizeWarningLimit: 1500 } })

路径别名这里要特别注意:__dirname在 ESM 的配置文件中不一定可用,如果报错就改用fileURLToPath(new URL('./src', import.meta.url))的方式。Vite 的配置文件默认是 ESM 格式,这是它和 webpack 配置文件最大的区别之一。

代理配置中rewrite的作用是把请求路径里的/api前缀去掉再发给后端,这要根据你后端的实际接口路径决定,不要照抄。如果后端接口本身就带/api,那就不需要 rewrite,直接 target 指过去就行。

6.2 按需加载与分包策略

首屏加载速度优化,Vue 3 和 Vite 时代有两个利器:懒加载组件和手动分包。

路由组件用动态 import 就已经实现了按需加载,但页面上有些业务组件没有路由概念,比如"详情弹窗"这种只有在用户点击后才出现的组件,应该也用动态 import 包一层:

<script setup> import { defineAsyncComponent } from 'vue' const DetailDialog = defineAsyncComponent(() => import('@/components/DetailDialog.vue')) </script>

分包方面,Vite 默认会把所有第三方依赖打进一个 vendor 包,但如果你项目里有比较大的第三方库(比如图表库、编辑器),我建议手动拆开,避免某个库一更新,整个缓存都失效。

build: { rollupOptions: { output: { manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'charts': ['echarts'], 'editor': ['@wangeditor/editor'] } } } }

手动分包有个潜在的坑:如果两个 chunk 之间有循环依赖,控制台会报警告,严重时可能到页面白屏。遇到这种情况,优先把公共模块单独抽到一个 chunk 里,而不是把所有第三方全部塞进一个 vendor。我踩过一次:把两个内部库一起放进了vendor块里,结果它们互相依赖,构建后跑起来报 "Cannot access before initialization",最后拆开才解决。

6.3 开发环境与生产环境的差异

开发阶段和生产构建的差异性需要特别注意。我在生产环境遇到过一个奇怪的问题:本地开发一切正常,部署到服务器上之后刷新二级路由页面 404。这个和 Vite 本身没关系,是服务器没有做 history rewrite,也就是所有请求都回到了服务器根路径,找不到就返回 404。解决方法是让服务器配置"所有路径都指向 index.html",不同服务器的配置方式不一样,但思路是统一的。

另一件常被忽略的事情是:Vite 的public目录里的文件会被原样拷贝到构建产物根目录,如果你在组件里用/images/logo.png这种绝对路径引用,构建后必须保证服务器根目录下有这个文件。我建议所有需要打包处理、压缩或改名的静态资源都放在src/assets里通过 import 引入,不要放到 public。这样构建时能正确加 hash,还能通过构建工具压缩。

7. 全家桶项目的常见问题排查实录

7.1 模板 ref 访问不到

Vue 3 中访问模板 ref,如果你在onMounted里拿到的 ref 是null,或者拿到的不是 DOM 元素,大概率是因为模板 ref 的绑定时机问题。一个常见场景是v-for渲染的列表项做 ref,拿到的是一个数组,但如果你把 ref 定义成了普通 ref,不会自动转换。

<template> <div v-for="item in list" ref="itemRefs" :key="item.id">{{ item.name }}</div> </template> <script setup> const itemRefs = ref([]) </script>

这里必须注意:v-for里的 ref 会自动收集成一个数组,但你需要在组件挂载后再读取itemRefs.value,否则可能拿到的还是空数组。如果你在路由组件里使用,可能还会遇到"路由切换后 ref 不更新"的情况,这基本是因为v-for的 key 没配对,导致 Vue 复用了旧的 DOM 节点。

7.2 watch 监听不到对象内部的变化

这个问题在 Vue 3 里很典型,就是你以为对象的属性变了 watch 总能触发,但实际上监听的是引用地址。如果你监听一个ref且这个 ref 指向一个 object,那不写deep: true,只有整个对象被替换时才会触发。这种情况我踩过好几次,尤其是接口返回超大数据、然后你在回调里改了对象某个字段,watch 竟然没反应。

解法有两种:加deep: true,或者监听对象的某个具体字段:

watch( () => form.value.address?.province, (newVal) => { console.log('province changed to', newVal) } )

这种监听方式只关心具体某个字段,不触发深层比较,性能和逻辑清晰度都更好。我的经验是:尽量监听最小粒度的数据,不要为了图省事监听整个大对象。

7.3 Pinia store 中调用路由或组件实例

在 Pinia 的 action 里要做路由跳转,第一反应是引入 router,但要注意 Pinia store 文件里直接import router from '@/router',这在大多数场景下没问题,但如果你把 store 定义到了组件内部域里(用工厂函数创建的 store),router 的导入时机和 store 的实例化时机可能会有冲突。

我以前遇到一个诡异问题:在登录页调用 user store 里的loginaction,这个 action 里要跳转首页,但跳转后页面空白。排查到最后发现是因为另一个 store 文件里引用了同一个 router 实例,但那个 store 在路由初始化之前就被调用了,导致 router 还没有完整的路由表。

稳妥的做法是不要在 action 里跳路由,把跳转逻辑放在组件层。组件里先调用 action 处理状态,成功后再用router.push跳转,这样职责也清晰,排查也容易。

7.4 Vite 构建后资源路径 404

这个问题是 Vite 项目的"新手三连坑"之一。明明本地预览都好,部署上线后图片和 JS 全 404。原因通常是base配置不对。如果你部署在服务器根目录,base: '/'没问题;如果部署在子目录,比如https://example.com/admin/,那base要设成/admin/或相对路径'./'。

export default defineConfig({ base: process.env.NODE_ENV === 'production' ? '/admin/' : '/' })

但注意base: './'在部分路由和服务端渲染场景下会有问题,因为相对路径计算的是当前页面路径,如果路由是/admin/user/list,相对路径就会算错。最稳妥的还是写死一个绝对路径前缀,然后让部署层面保证资源在那个目录下。

7.5 问题排查速查表

现象可能原因解决方案
reactive 对象解构后不响应解构拿的是普通值用 toRefs 或直接用 ref
页面白屏某个包在 Vite 下报 ESM 错误在 vite.config 的 optimizeDeps.exclude 排除该包
路由切换了但页面没变route.name 冲突检查 addRoute 的 name,先 removeRoute
Pinia 报 no active Piniastore 在 app.use(pinia) 前被调用确保 useStore 在组件/路由守卫回调内部
打包后资源 404base 配置不对根据部署路径设置 base
动态路由组件加载失败Vite 无法分析动态 import使用组件映射表替代字符串拼接

这个表是我自己压箱底的排查手册,每一个都对应着一次真实踩坑,希望能帮后来者省下一点排查时间。实际项目里你还可能会遇到别的奇怪问题,但有一条通用排查思路:先确认版本兼容,再查构建配置,最后看运行时调用顺序,百分之八十的问题都能按这个顺序定位出来。

8. 项目实战中的几条个人建议

最后说几条我从实战中总结出来的建议。全篇说的都是我的真实体验,不是从文档里抄出来的结论,能用得上就收下。

第一,新项目不要急着全家桶一步到位。你先用 Vue 3 + Vite 搭一个空壳工程,跑通一个最简单的页面,再逐步引入 Router、Pinia、UI 组件库。一次引入全部依赖,出问题时很难分清是哪个环节导致的。

第二,组件库的选择要谨慎。Vue 3 时代的组件库生态已经成熟了,但是不同组件库的样式定制能力、打包体积、兼容性差异很大。我建议先看看团队最常用的组件库对 Vue 3 的支持程度,别因为某个组件库的单点功能就放弃主力选择,这会直接影响后期开发效率。

第三,TypeScript 全家桶势在必行。Vue 3 对 TS 的支持明显比 Vue 2 好,组合式函数天然适合做类型推导。如果你还在写纯 JavaScript,我的建议是在迁移到 Vue 3 的同时把 TS 也引进来,一次到位,不要先用 JS 以后再补。

第四,也是最重要的一条:多写组合式函数,少写组件。组件只负责模板和布局,业务逻辑全部下沉到 composables。这个习惯养成后,你会发现组件体积骤减、复用率提高、测试也更好写了。我在项目里把用户、权限、字典、文件上传这些公共能力都抽成了 composables,后续新功能的开发速度明显提升。

Vue 3 全家桶的生态已经非常稳定,如果你还在犹豫是否要迁移,我认为关键是先在一个非核心项目里试点跑通全流程,积累一套适合自己团队的踩坑清单。等这些坑都提前排掉,正式的全面迁移就会顺利得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询