每次开新项目,最烦的就是搭环境。vue3项目搭建这个标题听起来不过六个字,但真正落成一套能跑、能维护、能部署、能应付业务的工程骨架,涉及到的细节和坑,比官方文档里写的多得多。我前后用Vue3从零搭过后台管理端、商城前端、还有嵌在客户端壳子里的内嵌页面,每一步都踩过不止一遍。这篇就把我自己沉淀下来的一套搭建流程和避坑清单整理出来,从环境准备、工程配置、核心功能落地,到Vue2老项目迁移和常见问题排查,一次讲透,希望能让准备上手Vue3的朋友少走些弯路。
这篇内容不挑基础,刚看完Vue3文档想动手写第一个项目的人能跟着操作,已经被业务项目折磨过一阵子的开发者也能从中找到几处值得对照的细节。我尽量说人话,不堆概念,把每个关键选择背后的原因也一并讲清楚,毕竟只给结论不给理由,换个环境你照样不会变通。
1. 先把地基打牢:环境准备与脚手架选型
1.1 Node版本和包管理器是第一道闸门
很多人搭Vue3项目一头扎进命令,跑完create-vite就报错,十有八九是Node版本不对。Vite 5及以上版本要求Node 18+,Vite 6甚至建议Node 18.18+或20+。我见过不少同事机器上还躺着Node 14,那跑npm create vue@latest直接提示UnhandledPromiseRejection,一眼看上去像网断了,实际上是版本太旧。
建议第一步先确认Node版本:
node -v npm -v低于18的,优先用nvm切换版本,别直接去官网覆盖安装。同一台机器上可能会有多个老项目依赖旧Node,全局覆盖是给自己埋雷。装完nvm之后,当前项目目录里可以放一份.nvmrc文件,内容就写20或者18.18这种,团队其他人进来nvm use一下就能同步版本,这是我在实际项目中养成的习惯。
包管理器方面,npm、yarn、pnpm都能用,但我个人强烈建议新项目直接上pnpm。原因很简单:Vue3生态里很多包,比如vite-plugin-vue-setup-extend、unplugin-auto-import,在npm的扁平化依赖结构下偶尔会出一些奇奇怪怪的幽灵依赖问题,而pnpm的硬链接和严格依赖隔离机制能把这些坑拦在门外。项目跑起来之后的安装速度差距也明显,尤其在依赖上百个的规模,pnpm的优势不是一点点。安装方式也很简单:
npm install -g pnpm pnpm --version1.2 新项目脚手架怎么选:Vite当主力,Vue CLI只用来接手旧项目
官方现在主推的脚手架是create-vue,也就是npm create vue@latest,它本质上是基于Vite的上层封装,会问你需不需要TS、JSX、Router、Pinia、Vitest等,按需勾选特别舒服。我通常的勾选组合是:TypeScript必勾,JSX看团队成员熟悉程度,Router和Pinia必须,Vitest看项目要不要做单测,ESLint+Prettier必勾。
如果你想更轻一点,直接用create-vite然后手动装Vue插件也可以,但对于一个正经要长期维护的项目,我还是建议用create-vue,它生成的文件组织是官方团队整理过的,后续维护成本低。
这里得说清楚,Vue CLI(webpack版本)怎么选。如果是从零开始的新项目,真没必要再用vue create了。Vite冷启动和热更新的体感是碾压级差异的,一个几千条路由的大型后台项目,webpack dev server冷启动可能要几十秒,Vite基本在1-2秒内。但如果你要接手的是一个跑了好几年的Vue2老工程,那@vue/cli依旧有它的价值,因为它能稳定地处理webpack配置里的各种历史包袱,这时候别强行切Vite,等后续做渐进迁移再动。
1.3 一分钟跑起一个基础项目
我直接把常用的命令写在这里,这是经过多次验证的稳定路径:
# 创建项目,my-vue3-app 换成你的项目名 npm create vue@latest my-vue3-app # 进入目录 cd my-vue3-app # 安装依赖 pnpm install # 启动开发服务器 pnpm devcreate-vue的交互式提问里有个细节:Add TypeScript?如果团队里有人不熟TS,可以先选No,但现实是我在多个项目里最后都后悔没一开始就上TS。Vue3的响应式系统对类型推导支持已经非常成熟,写业务代码时编辑器提示带来的效率提升远大于学习成本。哪怕现在不熟,也建议选上TS,然后找时间补基础,业务代码可以先用any顶着。这是实用主义,不是理论洁癖。
2. 工程化必备配置:让项目从“能跑”变成“好维护”
2.1 SCSS全局变量和路径别名,第一天就配好
很多热词里都在问vue3安装scss,这里直接把步骤给全。在Vite项目里装SCSS只需要两个开发依赖:
pnpm add -D sass注意,装sass这一个包就够了,node-sass那套老掉牙的方案别碰了,node-sass在Node 18以上的安装经常要过编译,折腾死人。新版本的sass是纯Dart实现,安装即用,不需要额外配置,就能在<style lang="scss">里写SCSS语法。
但你如果想把全局变量、mixin共享到每个组件里,光装sass还不够,需要在vite.config.ts里配置额外的css.preprocessorOptions:
// vite.config.ts export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: '@use "@/styles/variables.scss" as *;' } } } })这里要提一个新手极其容易踩的坑:以前webpack时代很多人用@import引入全局SCSS,但在dart-sass的新版本里,@import已经被标记为废弃,建议一律用@use。同时@use的变量默认是带命名空间的,不加as *你在组件里写$primary-color会直接报“undefined variable”,这属于实操中频率最高的SCSS配置错误。
路径别名同样是第一天就要配好的事。默认的./相对路径在组件嵌套深一点之后写着写着就变成../../../,维护成本极其痛苦。配置方式是在vite.config.ts里加:
import { fileURLToPath, URL } from 'node:url' export default defineConfig({ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })同时要在tsconfig.json里加对应的路径映射,否则TS会报红:
{ "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }2.2 路由、状态管理库怎么选,什么时候上
Vue Router 4和Pinia是Vue3官方钦定的组合,这已经是生态共识,不用犹豫。安装:
pnpm add vue-router@4 piniaPinia相比Vuex最大变化是去掉了mutation,state、getters、actions一把梭,而且支持组合式写法,在组件外面也能直接调用store。我在新项目里建议直接用组合式写法定义store,配合setup语法大概长这样:
// src/stores/user.ts import { ref } from 'vue' import { defineStore } from 'pinia' export const useUserStore = defineStore('user', () => { const token = ref('') const nickname = ref('') function setToken(value: string) { token.value = value } return { token, nickname, setToken } })这种写法对TS支持最友好,也符合Composition API的心智模型,团队里如果本来就熟悉setup语法,上手零成本。
路由方面,后台管理系统最常涉及的是动态路由和路由守卫两个点。动态路由的思路一般是:登录后从后端拿菜单权限数据,前端根据权限数据递归生成路由表,用router.addRoute()动态注册。这里要特别提醒一点,addRoute添加的路由在刷新后会丢失,所以必须在main.ts或者App.vue的初始化逻辑里重新拉权限并注册。就这个问题我在热词里看到很多人搜vue3搜索条件保留,实际上很多“刷新后状态丢失”的问题根源不是前端业务代码,而是路由和Pinia的初始化顺序没处理好。
2.3 代码规范和提交规范,别等代码多了再补
ESLint + Prettier在create-vue的交互式提问里可以直接勾选,这样生成的就是已配好的版本。但很多项目在创建时觉得“先跑起来再说”,等代码写到几百个文件之后再补lint,那成本就是指数级了,改起来全是历史文件。所以这一步我强烈建议在脚手架阶段就完成。
代码规范之外,husky + lint-staged也是我每个项目的标配。作用就是提交代码之前自动对暂存区的文件跑一遍eslint和prettier,不通过的代码根本进不了仓库。
pnpm add -D husky lint-staged npx husky initinit执行完会在.husky/pre-commit里生成示例钩子,把内容改成:
npx lint-stagedpackage.json里加:
{ "lint-staged": { "*.{js,ts,vue,tsx,jsx}": ["eslint --fix", "prettier --write"] } }这个配置看起来简单,但实际体验是:团队里每个人写的代码风格都能保持一致,code review时再也不用纠结一个组件里有人用单引号有人用双引号这种无意义争论。
3. 核心功能实现:业务需求背后那点通用套路
3.1 Composition API和script setup的日常打开方式
Vue3的<script setup>语法是现在写组件的主流,直接省掉了export default那一层壳,变量和函数直接暴露给模板。一个典型的Vue3+Sass+Pinia组合的组件头部长这样:
<script setup lang="ts"> import { ref, computed, onMounted } from 'vue' </script>ref和reactive的选择,我的经验是能用ref就用ref。ref在TS下类型推导更自然,而且在模板里自动解包,不需要写.value。reactive最大的问题是它在解构时会丢失响应性,稍微不注意就踩坑:
const state = reactive({ count: 0 }) // 这样解构之后 count 就不是响应式了 const { count } = state热词里提到vue3 composition api和option api,这两个写法的核心区别在哪里?本质上是逻辑复用的方式变了。Options API把同一个功能的data、methods、watch、computed拆散在不同的对象字段里,代码一长就得上下来回翻。Composition API允许你把相关逻辑写在一起,再进一步封装成自定义hook。我举个实际业务常见场景——搜索条件保留:
// composables/useSearchForm.ts import { reactive, ref } from 'vue' export function useSearchForm(fields: Record<string, any>) { const form = reactive({ ...fields }) const loading = ref(false) function resetForm() { Object.keys(form).forEach(key => { form[key] = fields[key] }) } return { form, loading, resetForm } }页面里用的时候,搜索表单相关的数据、重置逻辑、加载状态全部打包在一起,这才是Composition API真正值钱的地方。
3.2 动态表单、规则校验和JSX的实践位置
热词里出现频率很高的vue3 rules 日期检验和vue3动态添加删除form表单一行数据,明说这俩在Element Plus里都有原生解法,但很多人只停留在会用组件的层面。动态添加删除一行表单数据,本质是操作一个数组:
const formList = ref([{ name: '', date: '', time: '' }]) function addRow() { formList.value.push({ name: '', date: '', time: '' }) } function removeRow(index: number) { formList.value.splice(index, 1) }配合Element Plus的v-for遍历渲染,每一行绑定自己的索引,删除时索引对了就不会串行。这里面真正容易出问题的反而在rules的日期校验,尤其是“开始日期不能晚于结束日期”这种联动场景。常规的日期格式校验直接写pattern就能过,但联动校验必须在validator里拿另一个字段的值比对:
const rules = { startDate: [{ validator: (_rule: any, value: string, callback: any) => { if (!value) { callback(new Error('请选择开始日期')) } else if (form.endDate && value > form.endDate) { callback(new Error('开始日期不能晚于结束日期')) } else { callback() } }, trigger: 'change' }] }这里有个坑值得单独说:用trigger: 'change'还是trigger: 'blur',日期选择器类的组件,blur事件有时候根本不会触发,导致校验表单不执行。要么统一用change,要么两个都写上,这是我在项目里改过好几轮才摸清的规律。
至于vue3使用jsx,我明确说:日常业务组件用模板就够了,但如果你要写高阶封装组件、函数式弹窗、或者是像表格列配置那样动态渲染的场景,JSX的灵活性能让你少掉一半头发。Vite下用JSX只需要官方插件@vitejs/plugin-vue-jsx:
pnpm add -D @vitejs/plugin-vue-jsx// vite.config.ts import vueJsx from '@vitejs/plugin-vue-jsx' export default defineConfig({ plugins: [vue(), vueJsx()] })3.3 文件预览、在线编辑和前后端对接的注意点
热词里的vue3 pdf预览是后台系统常见需求,最简单的方式是让后端传PDF文件的URL,前端直接用<iframe>或者浏览器内置的PDF预览能力。但跨域和文件名带中文的场景下,更稳的方案是后端返回文件流,前端用URL.createObjectURL生成临时地址再嵌入<iframe>:
const res = await fetch('/api/file/pdf', { method: 'GET' }) const blob = await res.blob() const url = URL.createObjectURL(blob)记得在组件卸载时调用URL.revokeObjectURL(url)释放内存,否则单页应用长时间切换页面,浏览器内存会哗哗涨。
另外热词里有vue3 +onlyoffice 在线编辑和cefsharp vue3 window.cefbridge 注册,这类都是偏嵌入式场景。嵌入客户端壳子时,核心问题不是Vue3本身,而是跨端通信安全性和初始化时机。CefSharp场景里,前端要等window.cefSharp对象就绪之后再绑定事件,但页面资源加载和C#端注入对象的时机有可能错位,我用的稳妥方案是在onMounted里轮询检测,确认存在后再注册;同时要注意暴露给外部壳子的接口要做参数白名单校验,别把内网页面里的一些内部方法全部挂到全局,能被外部任意调用就容易出问题。
4. 老项目迁移:Vue2工程能不能迁、该不该迁、怎么迁
4.1 先摸清楚家底再谈迁移,别指望一把梭
热词里有一条特别扎眼:成熟项目 vue2 能转 vue3 吗。我的标准答案是,能迁,但没人能保证一行不改。Vue2到Vue3是破坏性升级,不是版本号平滑过渡。根据我帮别人迁项目还有自己业务迁移的经验,以下是较高的几个改动点:
Vue.prototype.$xxx这类全局挂载方法没了,要改成app.config.globalProperties.$xxx- 过滤器
filter在Vue3里彻底移除 $children、$listeners也被移除了- 事件总线
$on、$off不再支持,跨组件通信要改用mitt或者Pinia v-model的行为变了,Vue2里一个组件可以用多个v-model吗?也得靠.sync,Vue3里统一用多个v-model:xxxslot语法变了,slot-scope废弃,统一定为v-slotfunctional组件标记和函数式组件的写法也改了,不过日常开发里函数式组件占比不大,影响相对可控
这些都是拍着脑袋能列出来的,真正到了迁移现场还有大量隐性差异会从角落里杀出来。比如$set在Vue3里不需要了,因为Proxy代理数组下标赋值和对象新增属性都是可响应的;又比如事件修饰符的写法变化。在迁之前,先把项目依赖梳理一遍,像vue-router要升到4、vuex要升到3.5以上然后往Pinia过渡、UI组件库要对齐Vue3版本,这些前置动作不准备好,迁移会变成一场灾难。
4.2 迁移的务实策略:新页面走新写法,老页面按批次改
团队里的成熟老项目如果要迁,不建议停下来专门搞大迁移,业务方不会给你那么多时间窗口。我实际验证过可行的路径是“增量迁移”,核心思路是:
- 先把工程依赖升级到兼容Vue3的全家桶版本,比如Vue 2.7开始就已经自带部分Composition API能力
- 新写的页面一律用Vue3的
<script setup>语法和组合式风格 - 老页面在需求迭代时顺手改,改一个是一个
- 对于依赖老框架特性的代码,比如全局事件总线、filter这种,先在代码里搜索定位,集中做一个兼容层或者替换方案
等老代码房占比降到一定比例,再找一个合适的发版窗口组织全量切换,这样项目一直在往前走,不会出现“迁移三个月没法交版”的尴尬局面。
4.3 若依这类低代码框架的报错要怎么处理
热词里出现的若依vue3 ts报错也值得单独拉出来说说。这类框架类工程因为自带一套代码生成和通用逻辑,对开发者来说最大的问题不是不会写Vue3,而是框架本身有隐含的约定。最常见的TS报错集中在:路由meta上的类型定义、全局属性类型声明、自定义指令的类型声明。解决套路很统一,先找到项目里的env.d.ts把缺的类型补声明上。
比如路由meta一般会自定义title、icon、hidden这些字段,TS默认的RouteMeta里没有,就得自己扩展:
import 'vue-router' declare module 'vue-router' { interface RouteMeta { title?: string icon?: string hidden?: boolean keepAlive?: boolean } }这类报错本质是TS的类型收窄问题,跟框架本身没关系,理解了声明合并,报错就能一手按住。
5. 常见问题排查与避坑实录
5.1 UI框架引入不生效和样式覆盖问题
热词有vue3引入所有的ui框架都不生效,这种表述大概率是把Vue2组件库直接装到Vue3项目里了。Vue3只能跑支持Vue3的组件库版本,比如Element Plus对应Element UI,Vant 4对应Vant 2,Naive UI原生就是Vue3的。装完还不行的话,检查这几处:
- 组件库样式文件有没有在
main.ts里引入,多数组件库是组件和样式分离的,比如Element Plus走unplugin-auto-import和unplugin-vue-components按需引入时,组件样式往往由插件自动处理,但某些特殊组件的样式需要手动引入 - 项目里如果配了SCSS的
additionalData,里面如果写了比较重的全局样式,可能在某些场景下意外覆盖了组件库的部分样式 - 深色模式或自定义主题覆盖时,很多组件库用的是CSS变量,覆盖时要找到具体的变量名,而不是直接写类名选择器
我在后台项目里调Element Plus主题色是有固定套路的:先引入全量样式,再覆盖--el-color-primary这类CSS变量。比起用/deep/去硬杠,用变量体系匹配组件库的设计思路会稳定得多。
5.2 局域网访问白屏、菜单按钮消失这些“玄学问题”
热词里vue3 vite dev 局域网打开空白,这问题我碰到太多次了,原因一般是Vite默认监听localhost,局域网内其他设备访问不到。解法是在vite.config.ts里设置:
server: { host: true }host: true让Vite监听所有网络地址,这样就能通过局域网IP访问。如果配完之后白屏,那大概率是页面报错但没被看到,打开浏览器控制台检查一下;还有一种情况是用history模式路由直接访问非根路径,刷新后静态服务器没配置回退到index.html,开发环境里Vite默认为SPA做回退,但到生产环境部署到Nginx后必须自己配try_files:
location / { try_files $uri $uri/ /index.html; }热词里vue3 tabs标签页样式和vue3 on-success监听不到这些细粒度问题,背后逻辑基本都是一类:组件库的DOM结构变更导致样式选择器失效,或者事件名的驼峰命名转换导致监听不上。Element Plus的tab切换事件是tab-change,在模板里写作@tab-change,如果你在onMounted里用原生事件绑定的方式去监听它建议不要,统一走组件派发事件。至于上传组件的on-success监听不到,先检查函数是否真的传到组件上了,而不是写到自定义的options对象里去了。
5.3 深度优化:首屏加载、搜索条件保留和内存泄漏
vxetable怎么避免vue3首屏加载这类性能问题,在大数据量表格场景里是必考题。vxe-table本身支持虚拟滚动,但真正导致首屏卡的往往不是表格,而是加载到全局的路由和组件。我优化过几个项目的固定套路是:
- 路由全部改用懒加载,大型后台可以进一步按模块分包
- 组件库按需自动引入,别全量打包
- 对不常用的第三方库(比如PDF预览、Excel导入导出类库)用动态import按需加载
- 对大表格套
vxe-grid配scroll-y,保证可视区之外的行不渲染
至于vue3搜索条件保留,比较常见的是用户填了一堆搜索条件后,点进详情页再返回,表单被清空了。我的解法是:把搜索表单状态放到Pinia里,或者用keep-alive缓存列表页组件。要注意的是keep-alive的使用要围绕router-view做出调整,并且设置include或exclude控制哪些列表页需要缓存,不能所有页面一揽子缓存,否则详情页的数据永远是旧的。另外缓存的组件里如果有定时器、轮询逻辑,在onActivated和onDeactivated生命周期钩子里要分别处理开启和暂停,不然切走页面定时器还在跑,典型的内存泄漏源。
5.4 与后端接口联调的常见阻塞点
热词里vue3访问后端是个范围很宽的问题。新项目最容易卡壳的其实是跨域。开发环境跨域推荐在Vite里配代理,而不是让后端开CORS,这样上线后不会有额外风险:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }生产环境如果前后端分离部署,Nginx上把/api反向代理到后端服务地址即可。联调时还有一个细节:axios请求拦截器里统一带上token,响应拦截器里对HTTP状态码做统一处理,比如401时跳转登录页。这些看起来像老生常谈,但实际项目里因为拦截器写得混乱导致登录态失效后还在疯狂发请求的问题,我见的绝不算少。
6. 面试官最喜欢问的Vue3核心知识点(也帮你自查)
6.1 响应式原理:Proxy和defineProperty的本质差异
这题在热词vue3面试题里稳稳占据C位。Vue2的响应式是Object.defineProperty对属性做劫持,所以对象新增属性和数组下标赋值都监听不到。Vue3用Proxy代理整个对象,那么无论读取、赋值还是删除属性,任何操作都会经过代理层,所以连delete obj.xxx也可以是响应式的。
但这里有一个核心细节:ref和reactive的实现底层不一样。reactive直接基于Proxy,ref则是包装了一个RefImpl类,内部用一个_value存数据,读取时走getter,赋值时走setter。你写ref(0)获取时是count.value,模板里自动解包,所以不用写.value。理解这一点之后,ref解包失效的场景问题就迎刃而解了:数组里放ref对象,在模板中遍历数组时,元素不会被自动解包,你得单独写item.value。
6.2 ref、reactive和toRefs的选择逻辑
很多面试题会问“什么时候用ref什么时候用reactive”,我的答案是:能用ref就用ref,但当你要维护一个复杂的表单对象时,用reactive能省掉一长串.value。另外如果有把一个响应式对象拆解出来给模板或子组件使用的需求,就用toRefs:
const state = reactive({ name: '张三', age: 18 }) const { name, age } = toRefs(state)这俩解构出来依然是响应式引用,且对模板和TS都友好。
要特别留意一个面试高频追问:ref嵌套reactive到底会怎样。ref存的如果是对象,它内部会自动用reactive去代理这个对象,所以ref({})取到的内容依然深度响应。不要把这两者当作对立的,理解成“ref是一个盒子,盒子里放基本类型就直接包一层值,放对象就交给Proxy处理”,就彻底通了。
6.3 watch、watchEffect和生命周期钩子的现代版本
watch和watchEffect也是热门考点。简单区分:watch需要指定要监听的数据源,回调里能拿到新旧值,适合需要精确控制触发条件和拿到前后对比的场景;watchEffect是“哪些数据变化就自动执行”,适合收集依赖并同步副作用,比如把用户ID变化同步到请求里。
生命周期方面和Vue2的对应关系,我经常用这张表帮人记忆:
| Vue2 | Vue3 Options | Vue3 Composition |
|---|---|---|
| beforeCreate | beforeCreate | 由 setup 代替 |
| created | created | 由 setup 代替 |
| beforeMount | beforeMount | onBeforeMount |
| mounted | mounted | onMounted |
| beforeUpdate | beforeUpdate | onBeforeUpdate |
| updated | updated | onUpdated |
| beforeDestroy | beforeUnmount | onBeforeUnmount |
| destroyed | unmounted | onUnmounted |
组合式API里可以在setup里多次调用onMounted,对于复杂页面按功能分组注册钩子,比Options API只允许一个mounted要干净得多。
6.4 性能优化相关的面试点:动态组件、v-memo和keep-alive
Vue3在编译层做了很多优化,v-memo和v-once是树摇级别的性能优化,面试官很喜欢考v-memo到底解决什么问题。我用大白话说:v-memo可以让你手动指定一组依赖,当依赖不变时,这个子树完全不更新。它适合那种数据不变但组件树比较重的场景,比如大表格的某一列里嵌了复杂的组件,可以针对性地包一层v-memo="[row.id]",实测对复杂列表的更新性能提升明显。
keep-alive配合动态组件也是一个高频结合点,后台管理系统的多标签页实现基本就是keep-alive+router-view的组合,把已打开的页面实例缓存住,切换时不会丢失组件内部的状态。实现多标签页时要注意:keep-alive的include要跟路由name配合好,路由不写name的话缓存根本不生效,这一点我在好几个团队代码里发现过。
7. 多端场景和生态扩展:从后台到商城到嵌入式
7.1 后台管理系统、商城这类中后台项目的同构套路
热词里vue3后台管理系统和vue3商城高频出现。中后台项目的骨架其实高度相似:登录权限、动态菜单、内容区多标签页、基于表格的表单增删改查、权限指令。我搭过不止一个这类项目,核心建议是先把基础布局和权限模型定清楚,再来谈页面功能。权限模型一般分为路由级权限和按钮级权限,路由权限用动态addRoute实现,按钮权限用自定义指令v-permission封装,指令内部从Pinia的store里读取用户权限集合,没有就移除元素:
app.directive('permission', { mounted(el, binding) { const { value } = binding const userStore = useUserStore() if (!userStore.permissions.includes(value)) { el.parentNode?.removeChild(el) } } })商城项目则多一个维度:商品模型、购物车状态、订单状态、支付流程。购物车这类跨页面共享的数据,最适合放Pinia,因为刷新不丢靠持久化插件,页面切换能即时同步,同时组件间不用再层层emit。
7.2 uni-app里用Vue3写跨端应用的注意点
热词里uni-app vue3 ref万能对象和uniapp vue3 echarts 图片说明不少人已经在uni-app里用Vue3了。uni-app对Vue3的支持现在已经比较成熟,但有几个特殊约束:某些平台不支持Proxy的完整特性,所以响应式在低版本小程序端会有一定差异,遇到数据更新不触发视图的诡异问题,优先排查是不是用了老式写法去操作对象。ref在模板里可以做“万能对象”是因为编译器对ref做了特殊的自动解包处理,但在v-for里遍历数组内部的ref,以及在某些需要在setup里传递整个响应式对象的场景,还是得留神v-for循环中的ref数组问题。
H5端使用ECharts通常直接在onMounted里初始化就行,但如果要把图表生成图片保存到相册,小程序端就得用canvas的toTempFilePath这类平台API,不能直接拿H5的canvas.toDataURL,这些差异在跨端项目里几乎每天都能遇到,遇到就记下来,慢慢会攒成团队的避坑手册。
7.3 Vue3 + Three.js + TypeScript的3D应用搭建
热词里还有基于 vue3 + three.js + typescript 机房这种偏可视化的项目。技术栈组合很清晰:Vue3做应用框架,Three.js做WebGL渲染,TypeScript做类型安全和业务约束。搭建时关键在工程层面:Three.js按需引入、场景资源加载时机、组件销毁时释放渲染器和几何体内存。用Vue3封装Three.js场景的通用套路是,在onMounted里初始化WebGLRenderer、Scene、Camera,在onBeforeUnmount里调用renderer.dispose()和scene.traverse去dispose几何体和材质,否则单页应用切换路由几次后,GPU内存会大幅飙升。用ref获取canvas元素时,模板里写ref="canvasRef",在script setup里通过const canvasRef = ref<HTMLCanvasElement | null>(null)取到真实DOM再交给Three.js。
8. 个人经验的最后补充
我在实际项目里踩过最多次的坑,总结下来就三条:第一,Node环境不一致导致的莫名其妙的依赖报错,团队里一定要统一nvm和.nvmrc;第二,不要在脚手架阶段图省事跳过TS配置,后面补的成本高十倍不止;第三,遇到性能问题先从网络请求和渲染列表数量下手,不要一上来就怀疑框架本身。
最后再分享一个小习惯:每次搭建完新项目,我都会抽出十分钟做一次“骨架体检”,大致包括检查依赖是否有废弃版本、路由是否全部懒加载、全局组件是否真的有必要注册、公共样式里有没有无用代码等等。这种体检看着不起眼,但能让项目从一开始就保持干净,等团队规模变大、业务迭代变快时,你会感谢最初那个愿意多花十分钟的自己。Vue3项目搭建这件事,说到底是把一堆看似琐碎的事情安排明白,脚手架只是起点,真正的差距在后续的配置、规范和排坑能力上。