1. 从零上手 Vue3:一个后端转前端的人踩过的所有坑
Vue3 出来已经好几年了,但真正让我下决心系统学一遍,是因为接手了一个后台管理系统的维护工作。项目用的是 Vue3 + TypeScript + Vite,而我之前只写过 Vue2 的 Options API,连setup是啥都不知道。打开代码一看,满屏的ref、reactive、computed,还有各种defineProps、defineEmits,整个人是懵的。后来花了大概两周时间,从环境搭建到路由配置再到组合式 API 的实战,总算把整个链路跑通了。这篇文章就是把我这段时间的学习路径、踩过的坑、以及那些“当时要是有人告诉我就好了”的经验,完整地整理出来。
如果你也是刚接触 Vue3 的新手,或者从 Vue2 迁移过来还在适应组合式 API 的写法,又或者你正在搭建一个 Vue3 的后台管理系统,那这篇内容应该能帮你省下不少查文档和试错的时间。我会从最基础的环境配置讲起,一直讲到路由、组件通信、常见报错排查,尽量做到“照着做就能跑起来”。
1.1 为什么现在学 Vue3 而不是继续用 Vue2
这个问题我一开始也纠结过。Vue2 我用了两年多,写起来很顺手,为什么要换?直到我真正在项目里用了一段时间 Vue3,才发现几个关键差异直接影响了开发体验。
首先是响应式系统的底层重构。Vue2 用的是Object.defineProperty,这个 API 有个天然缺陷:它没法检测对象属性的新增和删除,也没法直接监听数组索引和长度的变化。所以 Vue2 里才会有Vue.set、Vue.delete这些补丁方法。Vue3 换成了Proxy,直接代理整个对象,上面那些限制全没了。你在setup里随便给reactive对象加属性,视图都能自动更新,不需要额外调 API。
其次是组合式 API 带来的逻辑复用方式。Vue2 的mixin用过的都知道,命名冲突、来源不清晰、多个 mixin 混在一起根本看不出数据从哪来。Vue3 的组合式 API 把相关逻辑写在一个函数里,哪个组件用就 import 哪个,清晰得多。我后来把项目里的权限判断、表格分页、表单校验都抽成了独立的组合函数,复用起来非常舒服。
再就是性能层面的提升。Vue3 的 diff 算法做了优化,编译时会对静态节点做标记,更新时直接跳过。加上 Tree-shaking 的支持,打包体积也比 Vue2 小不少。对于一个后台管理系统来说,首屏加载速度的提升是能明显感知到的。
还有一个很现实的原因:生态已经全面转向 Vue3。Element Plus、Ant Design Vue、Naive UI 这些主流组件库,新版本都是 Vue3 优先。Vue2 已经进入维护模式,新项目再用 Vue2 确实不太合适了。
1.2 这篇文章适合谁看,能帮你解决什么
我写这篇内容的定位很明确:面向刚接触 Vue3 的新手,以及从 Vue2 转过来需要快速上手的开发者。不管你是前端科班出身,还是像我一样从后端转过来的,只要你会基本的 HTML、CSS、JavaScript,就能跟着走下来。
具体来说,这篇文章会覆盖以下几个核心问题:
- 怎么在 Windows 上搭建一个完整的 Vue3 开发环境,包括 Node.js、包管理器、编辑器的配置
- 用
create-vue和 Vite 创建项目的完整流程,以及每种配置选项的含义 - 组合式 API 的核心概念:
ref、reactive、computed、watch、生命周期钩子 - Vue Router 的配置和跳转,包括动态路由、路由守卫、参数传递
- 组件之间的通信方式,从 props 到 emit 再到 provide/inject
- 实际开发中常见的报错和排查思路
我不会只贴代码不说原因。每个配置项、每个 API 的选择,我都会解释为什么这么用,什么场景下该用哪个。这样你遇到类似问题时,能自己判断该怎么处理,而不是只会复制粘贴。
2. 开发环境搭建:Windows 下的完整配置流程
环境搭建是很多新手的第一道坎。我见过不少人卡在 Node.js 版本不对、npm 安装慢、编辑器没装对插件这些问题上,还没开始写代码就放弃了。这一章我把 Windows 下搭建 Vue3 开发环境的完整流程拆开讲,每个步骤都说明为什么这么做。
2.1 Node.js 版本选择与安装细节
Vue3 和 Vite 对 Node.js 版本有要求。Vite 4.x 需要 Node.js 14.18+ 或 16+,Vite 5.x 要求 Node.js 18+。我建议直接装Node.js 18 LTS 或 20 LTS,这两个版本都是长期支持版,稳定性和兼容性都经过验证。
安装方式有两种:官网下载安装包,或者用版本管理工具。我推荐用nvm-windows(Node Version Manager for Windows),原因是不同项目可能依赖不同的 Node 版本,用 nvm 可以随时切换,不用反复卸载重装。
安装 nvm-windows 的步骤:
- 去 GitHub 搜
nvm-windows,下载nvm-setup.exe - 安装时注意两个路径:nvm 的安装目录和 Node.js 的 symlink 目录,后者不要有空格
- 安装完成后打开新的命令行窗口,输入
nvm version确认安装成功 - 输入
nvm install 18.20.0安装指定版本 - 输入
nvm use 18.20.0切换到该版本
装完之后用node -v和npm -v检查版本。如果node -v没输出或者报错,大概率是环境变量没配好,检查一下 nvm 的 symlink 目录是否加到了 PATH 里。
注意:安装 nvm-windows 之前,如果系统里已经装了 Node.js,建议先卸载,否则可能出现版本冲突。另外 nvm 切换版本时需要管理员权限,普通命令行窗口可能报错,用管理员身份打开终端即可。
2.2 包管理器选型:npm、yarn 还是 pnpm
Node.js 自带 npm,但 npm 在安装依赖时速度较慢,而且多个项目会重复下载相同的包,占用大量磁盘空间。我现在的选择是pnpm,原因有三个:
- 速度快:pnpm 用硬链接的方式共享依赖,安装速度比 npm 快很多
- 省空间:同一个包在磁盘上只存一份,多个项目共用
- 依赖严格:pnpm 的 node_modules 结构更严格,不会出现“幽灵依赖”问题(即没有在 package.json 里声明的包也能被引用)
安装 pnpm 很简单,一行命令:
npm install -g pnpm装完之后用pnpm -v检查版本。如果你之前用的是 npm,项目里的package-lock.json可以删掉,用pnpm install重新生成pnpm-lock.yaml。
当然,如果你只是学习阶段,用 npm 也完全没问题。只是当项目依赖多起来之后,pnpm 的优势会越来越明显。我自己的后台管理系统项目,用 npm 装依赖要两分多钟,换成 pnpm 之后大概三十秒就搞定了。
2.3 编辑器配置:VS Code 插件与设置
VS Code 是目前前端开发的主流编辑器,Vue3 开发需要装几个关键插件:
| 插件名称 | 作用 | 是否必装 |
|---|---|---|
| Vue - Official | Vue3 语法高亮、智能提示、类型检查 | 必装 |
| TypeScript Vue Plugin | 让 TS 文件识别 .vue 文件 | 用 TS 时必装 |
| ESLint | 代码规范检查 | 推荐 |
| Prettier | 代码格式化 | 推荐 |
| Auto Rename Tag | 自动重命名配对标签 | 推荐 |
| Path Intellisense | 路径自动补全 | 推荐 |
注意:以前很多人装的是Vetur插件,那是 Vue2 时代的产物,Vue3 项目里要禁用 Vetur,改用Vue - Official(以前叫 Volar)。两个插件同时开启会冲突,导致语法高亮异常。
VS Code 的settings.json里建议加几项配置,让开发体验更好:
{ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "files.autoSave": "onFocusChange", "vue.updateImportsOnFileMove.enabled": true }vue.updateImportsOnFileMove.enabled这个配置很实用,当你移动或重命名组件文件时,VS Code 会自动更新所有引用该文件的 import 路径,省去手动修改的麻烦。
3. 创建 Vue3 项目:create-vue 与 Vite 的配合
环境配好之后,下一步就是创建项目。Vue3 官方推荐的方式是用create-vue,它底层基于 Vite,提供了交互式的配置选项。这一章我把创建项目的完整流程和每个选项的含义讲清楚。
3.1 create-vue 的交互式配置逐项解读
在终端里执行:
npm create vue@latest或者用 pnpm:
pnpm create vue@latest然后会进入交互式问答,我逐项说明每个选项的含义和选择建议:
Project name:项目名称,只能用英文小写和连字符,比如my-vue-app。这个名字会作为文件夹名和 package.json 里的 name 字段。
Add TypeScript?:是否使用 TypeScript。如果你之前没接触过 TS,建议先选 No,用纯 JavaScript 把 Vue3 的核心概念跑通,之后再学 TS。但如果你有后端经验或者已经会 TS,直接选 Yes,Vue3 对 TS 的支持非常好。
Add JSX Support?:是否支持 JSX 语法。Vue3 默认用模板语法,JSX 是可选的。除非你有 React 背景或者需要写渲染函数,否则选 No 就行。
Add Vue Router for Single Page Application development?:是否添加 Vue Router。做单页应用基本都要用路由,选 Yes。后面我会专门讲路由的配置和使用。
Add Pinia for state management?:是否添加 Pinia 状态管理。Pinia 是 Vue3 官方推荐的状态管理库,替代了 Vue2 时代的 Vuex。如果你做的是中小型项目,可能用不上;但后台管理系统这类需要跨组件共享状态的场景,建议选 Yes。
Add Vitest for Unit Testing?:是否添加单元测试。学习阶段可以先选 No,等需要写测试的时候再手动加。
Add an End-to-End Testing Solution?:端到端测试方案,学习阶段选 No。
Add ESLint for code quality?:是否添加 ESLint 代码检查。建议选 Yes,能帮你养成好的代码习惯。
Add Prettier for code formatting?:是否添加 Prettier 格式化。建议选 Yes,和 ESLint 配合使用。
选完之后,create-vue会自动生成项目结构并安装依赖。整个过程大概一两分钟。
3.2 项目目录结构详解
创建完成后,用编辑器打开项目,你会看到这样的目录结构:
my-vue-app/ ├── public/ # 静态资源,不会被 Vite 处理 ├── src/ │ ├── assets/ # 需要被 Vite 处理的静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面级组件 │ ├── App.vue # 根组件 │ └── main.js # 入口文件 ├── index.html # HTML 模板 ├── package.json # 项目依赖和脚本 ├── vite.config.js # Vite 配置 └── jsconfig.json # JS 项目配置(TS 项目是 tsconfig.json)几个关键点说明:
index.html是 Vite 项目的入口 HTML,它会被 Vite 处理,<script type="module" src="/src/main.js">这行引入了入口 JS。和 Vue CLI 不同,Vite 的 index.html 在项目根目录,而不是 public 目录。
src/main.js是应用的入口,它创建 Vue 应用实例并挂载到 DOM:
import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')vite.config.js是 Vite 的配置文件,可以配置别名、代理、插件等。比如配置@指向src目录:
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })配好别名之后,import 组件就可以写成import HelloWorld from '@/components/HelloWorld.vue',不用写一长串相对路径。
3.3 启动项目与热更新验证
在项目根目录执行:
npm run dev或者:
pnpm dev终端会输出本地访问地址,通常是http://localhost:5173。打开浏览器访问,看到 Vue 的欢迎页面就说明项目跑起来了。
Vite 的热更新(HMR)非常快,你修改组件里的内容,浏览器几乎瞬间就能看到变化,不需要手动刷新。这是 Vite 相比 Webpack 的一大优势,开发体验提升明显。
提示:如果
npm run dev报错,先检查 Node.js 版本是否符合要求。Vite 5.x 需要 Node.js 18+,版本太低会直接报错。另外,如果端口 5173 被占用,Vite 会自动切换到下一个可用端口,注意看终端输出的实际地址。
4. 组合式 API 核心概念:从 ref 到生命周期
组合式 API 是 Vue3 最核心的变化,也是新手最容易懵的地方。这一章我把最常用的几个 API 讲透,每个都配上实际场景和代码示例。
4.1 ref 与 reactive 的选择逻辑
ref和reactive都是用来创建响应式数据的,但使用场景不同。
ref可以包裹任何类型的值:基本类型(字符串、数字、布尔值)、对象、数组。在<script setup>里访问ref的值需要加.value,但在模板里会自动解包,不需要加。
import { ref } from 'vue' const count = ref(0) const user = ref({ name: '张三', age: 25 }) // 修改值 count.value++ user.value.name = '李四'reactive只能包裹对象类型(包括数组和 Map、Set),访问时不需要.value:
import { reactive } from 'vue' const state = reactive({ count: 0, user: { name: '张三', age: 25 } }) // 直接修改 state.count++ state.user.name = '李四'那到底该用哪个?我的经验是:优先用ref。原因是ref更通用,不会丢失响应性。reactive有个坑:如果你把它的属性解构出来,响应性会丢失。比如:
const state = reactive({ count: 0 }) let { count } = state // count 失去了响应性而ref解构后仍然是响应式的(在 Vue 3.2+ 中,解构ref需要配合toRefs或直接访问.value)。
当然,如果你要管理一组相关的状态,比如一个表单的多个字段,用reactive写起来更简洁:
const form = reactive({ username: '', password: '', remember: false })模板里直接form.username就行,不用写一堆.value。
4.2 computed 与 watch 的实战用法
computed用来定义计算属性,它会根据依赖自动缓存,只有依赖变化时才重新计算。
import { ref, computed } from 'vue' const firstName = ref('张') const lastName = ref('三') const fullName = computed(() => firstName.value + lastName.value)computed默认是只读的,如果你想修改计算属性的值,需要传一个包含get和set的对象:
const fullName = computed({ get: () => firstName.value + lastName.value, set: (val) => { firstName.value = val[0] lastName.value = val.slice(1) } })watch用来监听数据变化并执行副作用,比如发请求、操作 DOM、写日志。
import { ref, watch } from 'vue' const keyword = ref('') watch(keyword, (newVal, oldVal) => { console.log(`搜索关键词从 ${oldVal} 变成了 ${newVal}`) // 这里可以发请求搜索 })watch的几个常用配置:
immediate: true:立即执行一次回调deep: true:深度监听对象内部的变化- 监听多个源:第一个参数传数组
watch([keyword, page], ([newKeyword, newPage], [oldKeyword, oldPage]) => { // 两个值任意一个变化都会触发 })还有一个watchEffect,它会自动追踪回调里用到的响应式数据,不需要手动指定监听源:
import { watchEffect } from 'vue' watchEffect(() => { console.log(`当前关键词:${keyword.value},当前页码:${page.value}`) })watchEffect适合那种“依赖关系明确、不需要旧值”的场景。如果需要旧值或者要精确控制监听源,用watch。
4.3 生命周期钩子在组合式 API 中的写法
Vue3 的生命周期钩子在组合式 API 里变成了onXxx的形式,在setup里调用:
| Options API | 组合式 API | 调用时机 |
|---|---|---|
| beforeCreate | 不需要 | setup 本身替代 |
| created | 不需要 | setup 本身替代 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载后 |
| beforeUpdate | onBeforeUpdate | 更新前 |
| updated | onUpdated | 更新后 |
| beforeUnmount | onBeforeUnmount | 卸载前 |
| unmounted | onUnmounted | 卸载后 |
最常用的是onMounted,用来发初始请求、初始化第三方库:
import { onMounted } from 'vue' onMounted(() => { console.log('组件挂载完成,DOM 已经可用') fetchData() })onUnmounted用来清理定时器、取消事件监听,防止内存泄漏:
import { onMounted, onUnmounted } from 'vue' let timer = null onMounted(() => { timer = setInterval(() => { console.log('定时任务') }, 1000) }) onUnmounted(() => { clearInterval(timer) })注意:
setup的执行时机在beforeCreate之前,所以你不能在setup里访问this。所有需要this的操作,都要用组合式 API 的方式替代。
5. Vue Router 路由配置:从基础到动态路由
路由是单页应用的核心,Vue3 对应的路由版本是 Vue Router 4。这一章我讲清楚路由的配置、跳转、传参和守卫。
5.1 路由配置文件的结构与写法
用create-vue创建项目时如果选了 Router,会自动生成src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router' import HomeView from '@/views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', component: () => import('@/views/AboutView.vue') } ] }) export default router几个关键点:
createWebHistory是 HTML5 History 模式,URL 里没有#,看起来更干净。但需要服务器配置支持,否则刷新页面会 404。另一种是createWebHashHistory,URL 里带#,不需要服务器配置,但不太美观。
component: () => import(...)是路由懒加载,只有访问该路由时才会加载对应的组件文件。对于大型项目,这能显著减少首屏加载体积。
name是路由名称,用于编程式导航时指定目标,比写路径更安全(路径改了名称不用改)。
5.2 声明式导航与编程式导航
声明式导航用<router-link>组件:
<router-link to="/">首页</router-link> <router-link :to="{ name: 'about' }">关于</router-link>编程式导航用useRouter:
import { useRouter } from 'vue-router' const router = useRouter() // 跳转到指定路径 router.push('/about') // 用名称跳转 router.push({ name: 'about' }) // 带参数 router.push({ name: 'user', params: { id: 123 } }) // 带查询参数 router.push({ path: '/search', query: { keyword: 'vue3' } }) // 替换当前历史记录,不能后退 router.replace('/about') // 前进或后退 router.go(-1)push和replace的区别:push会往历史记录里添加一条,可以后退;replace是替换当前记录,不能后退。比如登录成功后跳转首页,用replace更合适,防止用户后退回到登录页。
5.3 动态路由与参数传递
动态路由用:参数名定义:
{ path: '/user/:id', name: 'user', component: () => import('@/views/UserView.vue') }组件里用useRoute获取参数:
import { useRoute } from 'vue-router' const route = useRoute() console.log(route.params.id) // 获取路径参数 console.log(route.query.keyword) // 获取查询参数params和query的区别:
params是路径的一部分,比如/user/123里的123,刷新页面参数还在query是 URL 问号后面的部分,比如/search?keyword=vue3,刷新页面也在- 用
params传参时,如果用name跳转,参数不会显示在 URL 里(除非路由配置了:id);用path跳转则必须配置动态段
我个人的习惯是:需要分享链接或刷新后保留的参数用query,页面内部的状态传递用params。
5.4 路由守卫的实际应用场景
路由守卫用来在跳转前后执行逻辑,最常用的是全局前置守卫beforeEach,用来做登录验证:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ name: 'login', query: { redirect: to.fullPath } }) } else { next() } })路由配置里给需要登录的页面加meta:
{ path: '/dashboard', name: 'dashboard', component: () => import('@/views/DashboardView.vue'), meta: { requiresAuth: true } }next函数的几种用法:
next():放行next(false):中断当前导航next('/login'):跳转到指定路径next({ name: 'login' }):用名称跳转
注意:
next必须被调用一次,否则导航会一直挂起。另外,Vue Router 4 里next是可选的,你也可以直接return一个路由地址或return true/false,效果一样。
6. 组件通信与常见报错排查
组件通信是实际开发中绕不开的话题,这一章我讲几种常用的通信方式,以及新手最容易遇到的报错和解决方法。
6.1 props、emit 与 v-model 的双向绑定
父组件传数据给子组件用props:
// 子组件 const props = defineProps({ title: String, count: { type: Number, default: 0, required: false } })子组件传数据给父组件用emit:
// 子组件 const emit = defineEmits(['update', 'delete']) const handleClick = () => { emit('update', { id: 1, name: '新名称' }) }父组件监听:
<ChildComponent @update="handleUpdate" @delete="handleDelete" />v-model在 Vue3 里可以绑定多个,本质是props+emit的语法糖:
<!-- 父组件 --> <ChildComponent v-model:title="pageTitle" v-model:count="pageCount" />// 子组件 const props = defineProps(['title', 'count']) const emit = defineEmits(['update:title', 'update:count'])6.2 provide/inject 跨层级传递
当组件层级很深,用 props 一层层传很麻烦时,用provide和inject:
// 祖先组件 import { provide, ref } from 'vue' const theme = ref('dark') provide('theme', theme)// 后代组件 import { inject } from 'vue' const theme = inject('theme', 'light') // 第二个参数是默认值provide/inject适合传递全局配置、主题、用户信息这类数据。但要注意:inject的数据默认不是响应式的,如果传的是ref,后代组件修改时需要.value。
6.3 常见报错速查与解决思路
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
Failed to resolve component | 组件没注册或路径写错 | 检查 import 路径和组件名 |
Property "xxx" was accessed during render but is not defined | 模板里用了未定义的变量 | 检查 setup 里是否声明 |
Cannot read properties of undefined | 访问了未初始化的对象属性 | 用可选链?.或给默认值 |
Component is missing template or render function | 组件文件没有 template 或 script | 检查 .vue 文件内容 |
Vue Router: No match found for location | 路由路径没配置 | 检查 routes 数组 |
Module not found: Error: Can't resolve '@/xxx' | 别名没配置 | 检查 vite.config.js 的 alias |
我遇到最多的一个报错是Failed to resolve component,通常是因为组件名大小写不一致。Vue3 的<script setup>里,组件名推荐用 PascalCase(大驼峰),模板里可以用 PascalCase 或 kebab-case。但如果用 kebab-case,要确保和 import 的名字对应上。
另一个高频问题是响应式丢失。比如从reactive对象里解构属性,或者把ref的值赋给普通变量,都会导致视图不更新。解决方法是:解构reactive用toRefs,访问ref记得加.value。
6.4 开发环境与浏览器兼容性注意事项
Vue3 默认支持现代浏览器,不支持 IE11。如果你需要兼容旧浏览器,要额外配置@vitejs/plugin-legacy,但会增大打包体积。
在 Edge 浏览器里,我遇到过一个问题:Vue3 项目有时候无法关闭浏览器右上角的最小化按钮。这其实是浏览器本身的行为,和 Vue3 无关。如果你在做 Electron 或类似桌面应用,需要自己处理窗口控制。
另外,pxtorem对 ECharts 不生效的问题,是因为 ECharts 的 canvas 渲染不受 CSS 的 rem 影响。解决方法是在 ECharts 的配置里手动根据屏幕宽度计算字体大小,或者用resize事件重新渲染。
7. 从学习到实战:搭建一个后台管理系统的思路
学完基础之后,最好的巩固方式就是做一个完整的项目。后台管理系统是很好的练手选择,因为它涵盖了路由、权限、表格、表单、状态管理等核心场景。
7.1 项目结构规划与模块拆分
一个典型的 Vue3 后台管理系统可以这样拆分:
src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── composables/ # 组合式函数 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── utils/ # 工具函数 ├── views/ # 页面组件 ├── App.vue └── main.jscomposables目录放可复用的组合式函数,比如useTable、useForm、usePermission。这样不同页面可以共享逻辑,代码量能减少很多。
7.2 登录鉴权与路由拦截的完整实现
登录鉴权的流程:
- 用户输入账号密码,调用登录接口
- 接口返回 token,存到 Pinia 和 localStorage
- 路由守卫检查 token,没有就跳转登录页
- 请求拦截器自动在 header 里带上 token
- token 过期时,响应拦截器统一处理,跳转登录页
Pinia 里的用户状态:
import { defineStore } from 'pinia' import { ref } from 'vue' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') const userInfo = ref(null) const setToken = (newToken) => { token.value = newToken localStorage.setItem('token', newToken) } const logout = () => { token.value = '' userInfo.value = null localStorage.removeItem('token') } return { token, userInfo, setToken, logout } })7.3 动态路由与权限控制的实现思路
不同角色的用户看到的菜单不一样,这就需要动态路由。思路是:
- 前端定义好所有路由(包括静态路由和动态路由)
- 登录后根据用户角色,从后端获取可访问的路由列表
- 用
router.addRoute()动态添加路由 - 侧边栏菜单根据最终的路由表渲染
const addDynamicRoutes = (routes) => { routes.forEach(route => { router.addRoute(route) }) }注意:动态添加的路由在刷新页面后会丢失,所以需要在路由守卫里判断,如果路由表为空,重新获取并添加。另外,404 页面要放在动态路由添加之后再匹配,否则会先匹配到 404。
8. 我踩过的坑与独家避坑经验
最后这部分,我整理一些文档里不会写、但实际开发中一定会遇到的问题。
8.1 响应式数据的常见陷阱
陷阱一:直接替换 reactive 对象。reactive返回的是 Proxy,如果你直接给它赋一个新对象,响应性会丢失。正确做法是修改属性,或者用Object.assign。
陷阱二:ref 在模板里自动解包,但在 setup 里不会。模板里写{{ count }}没问题,但 setup 里必须写count.value。这个新手很容易搞混。
陷阱三:watch 监听 reactive 对象的属性。如果直接watch(state.count, ...),需要写成函数形式watch(() => state.count, ...),否则监听不到。
8.2 路由跳转后组件不渲染的排查
我遇到过一个问题:路由跳转了,URL 变了,但页面内容没变。排查下来有几个原因:
- 路由配置里
component写成了components(多了个 s) - 父路由没有写
<router-view /> - 路由路径匹配到了但组件加载失败,检查控制台有没有报错
- 用了
router.push但目标路由和当前路由相同,Vue Router 会忽略这次跳转
解决方法:在<router-view>上加:key="$route.fullPath",强制重新渲染。
8.3 打包部署时的路径与配置问题
npm run build之后,dist目录部署到服务器,常见问题:
- 白屏:通常是
base路径配置不对。如果部署在子目录,要在vite.config.js里设置base: '/子目录名/' - 刷新 404:History 模式需要服务器配置 fallback 到
index.html。Nginx 的配置是try_files $uri $uri/ /index.html; - 接口跨域:开发环境用 Vite 的
server.proxy配置代理,生产环境由后端或 Nginx 处理
Vite 代理配置示例:
server: { proxy: { '/api': { target: 'http://后端地址', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }8.4 性能优化的几个实用技巧
- 路由懒加载:所有页面级组件都用
() => import(...) - 组件按需引入:Element Plus 等组件库用按需引入,不要全量引入
- v-if 和 v-show 的选择:频繁切换用
v-show,条件很少变用v-if - v-for 加 key:key 要用唯一值,不要用 index
- 大列表用虚拟滚动:数据量超过几百条时,用
vue-virtual-scroller之类的库 - 图片懒加载:用
loading="lazy"或 Intersection Observer
8.5 调试工具与开发效率提升
Vue DevTools 是必备的调试工具,浏览器装插件后,可以在开发者工具里看到组件树、Pinia 状态、路由信息。Vue3 需要装 Vue DevTools 6 以上版本。
另外几个提升效率的习惯:
- 用
<script setup>语法,代码更简洁 - 把重复逻辑抽成 composable
- 配置 ESLint + Prettier,保存时自动格式化
- 用 VS Code 的代码片段(snippet)快速生成模板
我个人在实际操作中的体会是,Vue3 的学习曲线在前一周会比较陡,尤其是组合式 API 的思维转变。但一旦理解了ref、reactive、computed这几个核心概念,后面写起来会越来越顺。关键是不要只看文档,一定要动手写,哪怕是把官方示例敲一遍,也比光看强得多。遇到报错不要慌,先看控制台,再查文档,大部分问题都能自己解决。