Vue 3实战全攻略:从环境配置到路由守卫与Token处理
2026/9/16 4:30:18 网站建设 项目流程

“Vue”这个词在简历和招聘市场上出现频率高到什么程度呢?过去几年我筛过的前端简历里,十份有八份写着“熟练使用 Vue”。但“熟练”和“能干活”之间,往往差着一个从零搭项目、被各种环境报错折磨、最后成功上线全流程的距离。这篇东西不想复述官方文档,而是把我带新人时走过的完整路径拆给你:从 Node 安装、Vite 创建项目、Volar 配置,到路由守卫、Token 处理、打包部署、m3u8 播放这类实战需求,一次捋顺。适合刚接触 Vue 的入门者,也适合后端同学想快速接手一个前后端分离项目时当速查手册用。

1. 环境配置:先让项目在你电脑上跑起来

1.1 Node 版本与 npm 镜像:绕不开的第一步

Vue 源码和构建工具链都跑在 Node 环境上,所以第一步是装 Node。这里我只有一个建议:装 LTS 版本,别追最新版。很多依赖包对 Node 的最高版本有兼容限制,用最新版容易踩“当前 Node 版本不受支持”的提示。装完之后命令行里验证一下:

node -v npm -v

两个命令都能输出版本号,才算环境就绪。接下来强烈建议先把 npm 镜像切到国内源,否则npm install卡在 node_modules 下载上是新手劝退第一杀手:

npm config set registry https://registry.npmmirror.com

有人喜欢用 cnpm,我的实际体验是:cnpm 装包快,但偶尔会出现依赖目录结构和原生模块不一致的问题,排查起来比多用两分钟等安装还折腾。所以常规项目里只配 registry 就够了。

如果你需要同时维护多个 Node 版本(比如老项目用 14,新项目用 18),建议装一个 nvm 管理版本切换,别手动卸载重装。开发机多版本并存,是工作中很常见的状态,nvm 能能省掉大量重复劳动。

1.2 用 Vite 创建第一个 Vue 项目

官方现在的脚手架已经全面切换到 Vite,命令行工具是create-vue。打开终端,执行:

npm create vue@latest

它会问你项目名、是否加 TypeScript、Vue Router、Pinia、ESLint 等。如果是纯上手练习,建议这些通通选 Yes:TypeScript 能逼你写出更严谨的代码,Router 和 Pinia 是后面必然要用的,ESLint 能帮你在写错的第一时间就发现。如果是公司存量项目要接 Vue,看团队已有规范再决定。

创建完成后,进入目录安装依赖并启动:

cd my-project npm install npm run dev

看到Local: http://localhost:5173/的输出,浏览器打开能出现默认欢迎页,项目就算跑起来了。注意旧教程里常见的vue create(Vue CLI 那条线)已经不再维护,新项目别再用它,阶段性的技术债没意义。Vite 启动速度极快,热更新也是秒级响应,这对开发体验的提升是革命性的,也是我在新项目里坚持用它的核心理由。

1.3 编辑器与 Volar:少踩一个省级大坑

写 Vue 3 的时候,VS Code 里一定要装 Volar 插件,它的全名是 Vue Language Features (Volar),千万不要再装旧时代的 Vetur。这两个插件同时启用会把代码提示搞混乱,类型推导也会失效。Vue 3 官方在创建项目时其实已经给了提示:推荐使用 Volar。这里再补充两个配套插件:

  • TypeScript Vue Plugin (Volar),用于支持*.vue文件中的 TS 服务。
  • Vue Devtools 浏览器扩展,调试组件状态、路由和 Pinia store 时非常关键。

遇到代码不报错但类型提示全红、defineProps不识别等情况,十有八九是插件装错或者没生效。所以环境配置这块,编辑器插件的重要性不亚于 Node 本身。我之前带过一个新人,代码复制过来没问题,就是提示一直报错,排查到最后发现装的是 Vetur,社区里这种案例实在太多。

1.4 初次安装依赖时的安全提示解析:ignored build scripts

执行npm install经常看到一大段黄色警告,比如:

ignored build scripts: cpu-features@0.0.10, esbuild@0.21.5, ssh2@1.17.0

很多新手看到这行就慌了,以为是安装失败了。其实这是 npm 为了防止恶意安装脚本而默认忽略某些原生模块的 postinstall 脚本。对 Vite 全家桶来说,esbuild 被忽略 build 脚本一般不影响正常使用,项目照常能启动。只有当某些依赖真正需要编译原生代码且报错时,再针对性处理,例如执行:

npm rebuild esbuild

这个知识点我专门列出来,是因为“安装失败”的恐慌感会浪费大量时间,而实际上根本没事。真正的安装失败会有 exit code 非 0,并且终端会明确告诉你缺了什么模块,那才需要去查。

2. 核心语法与组件化:Vue 到底在写什么

2.1 模板、响应式与 ref/reactive 的选择

Vue 3 的组合式 API 里,最核心的就是refreactive。一个简单落地示例:

<script setup> import { ref, computed } from 'vue' const count = ref(0) const double = computed(() => count.value * 2) function add() { count.value++ } </script> <template> <div> <p>{{ count }} 的两倍是 {{ double }}</p> <button @click="add">加一</button> </div> </template>

模板里{{ }}插值,: 属性是动态绑定,@click是事件绑定,v-model是双向绑定。这些语法占总使用量的一半以上,先把它们玩熟就够日常开发。

refreactive的区别,我用一句话总结:基本类型用ref,对象/数组如果整个替换就用reactive,但如果你需要给对象动态新增属性(这种情况在联调时特别多),那用ref包对象反而更顺手,因为ref内部本质也是reactive,赋值时用.value更统一。团队规范里如果统一用ref,也能减少大家纠结的成本。

2.2 组件通信:props、emit 与透传

组件化开发是 Vue 的灵魂。父组件向子组件传数据用props,子组件通知父组件用emit。但有一个很多人没搞懂的概念是“透传”:子组件根节点会自动继承父组件传下来但没有声明为 props/emits 的属性和事件。比如我封装一个基础输入框:

<script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" class="base-input" /> </template>

父组件这样用:

<BaseInput class="big-input" placeholder="请输入" @focus="onFocus" />

classplaceholder@focus这些东西会自动落到子组件的<input>根元素上,这就是透传。理解透传之后,你再看很多 UI 组件库为什么能“无脑传参”,逻辑就通了。

跨层级通信用provide/inject,适合隔代传数据;如果子组件想暴露方法给父组件调用,用defineExpose。通信方式很多,记住一个原则:能用 props 解决就别上全局状态,否则代码维护成本指数上升。

2.3 生命周期与 Composable:把逻辑抽出来复用

Vue 3 的生命周期钩子改了名字:created没了,beforeMount改成onBeforeMount,挂载后改成onMounted。在组合式 API 里,你可以把这些钩子放进一个独立函数,这个函数就叫 Composable(组合式函数),其实也就是自定义 hook。比如:

import { ref, onMounted, onBeforeUnmount } from 'vue' export function useWindowSize() { const width = ref(window.innerWidth) function update() { width.value = window.innerWidth } onMounted(() => window.addEventListener('resize', update)) onBeforeUnmount(() => window.removeEventListener('resize', update)) return { width } }

组件里只需要const { width } = useWindowSize(),就可以在任何组件复用这段逻辑。Composable 是 Vue 3 最值得学的设计模式,它让逻辑复用从“mixin 黑魔法”变成了普通函数调用,这也是 Vue 3 相比 Vue 2 的核心优势之一。

顺带说一句面试里总被问的 Angular 和 Vue 的区别:Angular 是一个完整的框架,强制 TypeScript,依赖注入体系庞大,项目复杂度高了之后约束力强但也更重;Vue 是渐进式框架,你可以只写一个组件、也可以搞一整个大型应用,学习曲线更平缓。团队小、节奏快、希望快速出活,选 Vue 很顺手;如果公司平台大、类型要求严苛,Angular 也有它的生存空间。这个差异直接回答“为什么选 Vue”这类面试题,给出清晰的技术取舍逻辑会更加分。

2.4 样式作用域与 UI 组件库选型

<style scoped>是 Vue 单文件组件的标配。它的原理是编译时给元素加一个>:deep(.inner-box) { background-color: #f5f5f5; }

UI 库选型上,新项目我一般推荐 Element Plus 或 Naive UI。有人会问,老牌的 layui 能不能配合 Vue 用。我的结论是:不推荐。layui 的设计理念是直接操作 DOM,Vue 是基于虚拟 DOM 的数据驱动框架,两者的思维模式天然冲突,硬凑在一起会出现大量不可预期的渲染问题。懂 jQuery 时代的老开发可能对 layui 有感情,但技术选型不是讲情怀,Vue 生态内已经有足够成熟的组件库,没必要为了一个熟悉的名字增加技术债。

3. 路由与状态管理:让页面真正“动”起来

3.1 Vue Router 基础与路由参数的两种写法

单页面应用必须有路由。创建项目时选上 Vue Router 之后,项目里会生成router/index.ts文件。核心 API 就几个:createRoutercreateWebHistoryrouterouter

路由参数最让人混淆的是paramsquery,我拆开细说:

  • params对应path风格,比如路由定义为/user/:id,跳转时用router.push({ name: 'User', params: { id: 1 } }),页面上用route.params.id取。
  • query对应查询串,跳转时用router.push({ path: '/user', query: { id: 1 } }),页面上用route.query.id取。

我的经验是:查询条件、可选的筛选信息用query,路径上必须存在的身份标识用params。两者混用没问题,但别把重要参数放query,因为用户刷新页面后query还在,但如果你把对象整个塞进query,URL 会变得巨丑且可能丢失类型。

嵌套路由使用children数组,配合<router-view />展示子页面。它和 Tab 页签、侧边栏布局的配合是后台管理系统最常见的需求:一个布局组件,下面挂多个子页面路由。

3.2 路由拦截器与登录权限控制

后台系统几乎都要做登录校验,最简单也最常用的方案就是全局前置守卫:

import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })

这个模式能解决 90% 的登录拦截需求。再往上就是基于角色的动态路由:用户登录后,后端返回角色和权限列表,前端用router.addRoute动态挂载可访问的路由,同时配合自定义指令做按钮级权限控制。

如果你拿到的是若依这类开源项目,你会在代码里看到完整的一套动态路由 + 权限指令实现,把它的权限部分读一遍,相当于免费上了一次企业级权限设计课。

3.3 Pinia vs Vuex:新项目怎么选

Vuex 统治了 Vue 2 时代,但 Vue 3 时代官方已经明确推荐 Pinia。我在新项目里也全部用 Pinia,主要对比看这张表:

维度PiniaVuex
官方定位Vue 3 默认推荐维护模式,适配旧项目
API 风格灵活,Composition + Options 均可严格 Options 风格,模板化
TypeScript 支持类型推导强,几乎零心智负担类型定义繁琐,容易写出一堆重复类型
模块化每个 store 独立定义,天然按需需要 modules 配置,集中管理
调试工具Devtools 支持全面Devtools 支持全面

Pinia 创建一个 store 非常简单:

// stores/counter.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { double: (state) => state.count * 2 }, actions: { increment() { this.count++ } } })

组件里直接const counter = useCounterStore(),再用storeToRefs解构出响应式数据就不会丢响应性。持久化配合pinia-plugin-persistedstate插件,把 token、用户信息这些状态自动存到 localStorage,刷新不丢登录态,体验很好。

核心原则还是那句:普通组件通信、页面间传参能自己搞定的,就别上 Pinia。全局状态是最后手段,用多了项目就会变得难以维护。

3.4 复杂业务组件的集成套路:地图、图表、自定义组件

有搜索热词提到“用在 vue 里的腾讯地图”“矩阵树图 vue”“sql-viewer”“多周选择组件”,这类需求本质相同:把第三方 JS 库整合到 Vue 响应式系统里。

以腾讯地图为例,最省事的做法是直接在index.html引入地图 JS SDK,然后在组件里这样初始化:

<script setup> import { onMounted, onBeforeUnmount, ref } from 'vue' const mapContainer = ref(null) let mapInstance = null onMounted(() => { mapInstance = new window.TMap.Map(mapContainer.value, { center: new window.TMap.LatLng(39.9, 116.4), zoom: 12 }) }) onBeforeUnmount(() => { mapInstance = null }) </script> <template> <div ref="mapContainer" style="height: 400px" /> </template>

这个ref模板引用拿到的是真实 DOM 节点,第三方库要求在真实 DOM 上初始化,所以必须等onMounted之后再操作。ECharts 做图表、AntV G6 做关系图,套路完全一样:引入资源、onMounted初始化、数据变化时调用实例的setOptionupdateData,卸载时销毁实例。摸透这一套,任何第三方库在 Vue 里都能顺手集成。

4. 前后端联调实战:接口、Token 与构建部署

4.1 封装 axios:拦截器统一处理 Token 与错误

前后端分离项目里,axios 是事实标准的 HTTP 客户端。但我建议每个人都封装一层,不要每个页面直接axios.get。核心代码就一段:

// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use((config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.response?.data?.message || '请求失败') return Promise.reject(error) } ) export default request

请求拦截器负责给每个请求自动带 token,响应拦截器负责统一处理 401 跳登录、弹错误提示。这样业务代码里就不需要到处判断if (res.code !== 200)了。token 过期自动踢回登录页,是“前后端分离请求 token 处理”最常见的硬需求,这个封装直接解决。

实际项目中如果后端要求刷新 token,就在响应拦截器里加一个“用 refreshToken 换新 token 后重发队列”的逻辑,前期可以不加,但设计上要留好扩展点。

4.2 代理与跨域:开发环境和生产环境分别怎么配

开发环境最常见的模式是 Vite 代理。前端跑在5173,后端跑在8080,浏览器直接请求8080会跨域,但通过 Vite 代理把/api开头的请求转发给后端,就不存在跨域了:

// vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

changeOrigin: true的作用是让后端看到的请求来源变成目标地址,避免某些后端框架对 Host 做校验时误判。后端那边,Spring Boot 项目可以用@CrossOrigin注解或全局 CorsFilter 配置允许来源;FastAPI 项目则是在中间件里配置 CORSMiddleware 的allow_origins。生产环境通常由 Nginx 把/转发到前端静态文件、把/api反向代理到后端服务,前端代码里不需要任何类似http://localhost的硬编码。

4.3 WebSocket 集成:心跳、断线重连与组件卸载

Vue 项目里加 WebSocket,核心不是 WebSocket 本身,而是它的生命周期管理。我常用一个简单的 composable 来封装:

// composables/useWebSocket.js import { onMounted, onBeforeUnmount, ref } from 'vue' export function useWebSocket(url) { const isConnected = ref(false) let ws = null let heartbeatTimer = null let retryTimer = null function connect() { ws = new WebSocket(url) ws.onopen = () => { isConnected.value = true heartbeatTimer = setInterval(() => ws.send('ping'), 30000) } ws.onmessage = (event) => { // 这里交给页面处理具体业务消息 } ws.onclose = () => { isConnected.value = false clearInterval(heartbeatTimer) retryTimer = setTimeout(connect, 3000) } } onMounted(connect) onBeforeUnmount(() => { clearTimeout(retryTimer) ws?.close() }) return { isConnected } }

心跳每 30 秒发一次,保证连接不被服务端主动断开;断线后 3 秒自动重连;组件卸载时清理定时器和连接,避免内存泄漏。这套代码几乎是所有即时通讯、实时推送页面的通用骨架。

4.4 打包发布:从npm run build到 Nginx

开发完成后,打包命令是:

npm run build

产物在dist/目录里,里面是纯静态文件。发布方式根据团队基础设施不同而不同,但核心思路一致。我在实际项目里最常遇到两类情况:

第一种,前后端一起部署,用 Nginx 托管前端并把 API 反向代理到后端。这类项目里最容易踩的坑是 history 模式路由刷新 404,解决办法是 Nginx 配置里加:

location / { try_files $uri $uri/ /index.html; }

第二种,整包打到一个服务里,比如若依这类 Spring Boot + Vue 项目,前端dist目录直接复制到后端src/main/resources/static下,随 Spring Boot 一起启动。注意改前端静态资源路径时,要设置base: './'或对应路径前缀,否则打包后 CSS/JS 路径不对,页面直接白屏。

“vue 打包后布局异常”这个热搜词我多说一句:先看浏览器控制台有没有静态资源 404,再看是不是 history 路由刷新导致,再看是不是某些按需引入的组件库样式没有完整引进来。90% 的情况都能在这三样里找到答案。

5. 新手绕不开的坑:报错排查与实用技巧

5.1 依赖安装阶段:esbuild、node-gyp 与缓存问题

依赖报错是新手最大的劝退点。整理一个高频速查表:

报错关键词常见原因处理办法
ignored build scripts: esbuildnpm 安全策略跳过了 postinstall通常无影响;若构建报错则npm rebuild esbuild
node-gyp相关错误需要本地编译原生模块安装对应系统编译工具,或换用预编译版本
EACCES权限错误全局安装没有权限不要用 sudo 硬来,用 nvm 管理 Node 再重装
npm install卡住网络问题或缓存脏了切换镜像源;npm cache verify;删除 node_modules 后重装

其中最常见的还是网络问题。切换 registry 后依然慢,可以考虑用npm install --registry=https://registry.npmmirror.com临时指定,不必全局改配置。

5.2 开发运行与打包阶段:白屏、样式错乱、路由 404

运行阶段的白屏,先打开 Vue Devtools 看根组件是否挂载成功,如果是 mounted 之后依然全白,再看控制台具体报错。常见情况是某个 API 在组件渲染时返回了 undefined,模板里直接访问user.avatar导致报错中断渲染。

样式错乱问题,主题集中在 scoped 和第三方组件上。比如全屏弹窗组件因为默认挂载到body下,scoped 样式打不到它内部,解决方案是组件库一般提供了append-to-body或自定义 class 透传能力,需要用到:deep()穿透去覆盖样式。

还有一个隐藏很深的坑是动态 import 路径写错。Vue 路由里我喜欢用() => import('@/views/xxx.vue')懒加载,但如果你手滑把路径写错,开发环境不报错、打包时才报Could not resolve,排查思路是完整看构建日志,不要只看第一条红色文字。

5.3 业务场景三件套:m3u8 视频、Excel 导出、i18n 插 HTML

这三个是搜索热词里出现频率很高的具体场景。

m3u8 视频播放:浏览器原生不支持 HLS 流,所以需要借助 hls.js,逻辑极简单:

import Hls from 'hls.js' const video = videoRef.value if (Hls.isSupported()) { const hls = new Hls() hls.loadSource('http://example.com/playlist.m3u8') hls.attachMedia(video) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Safari 原生支持 video.src = 'http://example.com/playlist.m3u8' }

多个表格导出一个 Excel:用xlsx库,创建 workbook 后循环添加 sheet:

import * as XLSX from 'xlsx' const wb = XLSX.utils.book_new() tables.forEach((table, index) => { const ws = XLSX.utils.json_to_sheet(table.data) XLSX.utils.book_append_sheet(wb, ws, `Sheet${index + 1}`) }) XLSX.writeFile(wb, '多表导出.xlsx')

vue-i18n 里插入 HTML 标签:直接写在翻译文案里的<a>标签会被当作纯文本转义,不会真正渲染成链接。正确做法是把标签片段作为插值变量传入,再用v-html渲染:

import { computed } from 'vue' import { useI18n } from 'vue-i18n' const { t } = useI18n() const tipHtml = computed(() => t('tip', { link: '<a href="/detail">这里</a>' }) )
<span v-html="tipHtml"></span>

强调一点:v-html里绝不能直接塞用户输入的内容,如果内容里包含用户填写的 name、comment 这类字段,一定要先做 HTML 转义再拼接,否则 XSS 漏洞在所难免。团队里如果安全审计严格,更稳的方案是用组件插槽替代v-html

5.4 面试速览:Vue 2 vs Vue 3、响应式原理、key 的作用

很多人上手 Vue 时正好赶上找工作,面试题总绕不开这几个维度:

  • Vue 2 和 Vue 3 的核心区别:响应式系统从Object.defineProperty换成Proxy,解决了数组下标更新、动态新增属性不响应的问题;组合式 API 替代了部分 Options API 的职责;虚拟 DOM 重写,diff 算法更快;更友好的 Tree-Shaking,项目体积更小。
  • 响应式原理:Vue 3 用Proxy代理对象,拦截getset,在get时收集依赖,在set时触发更新。这个“依赖收集 + 派发更新”的模型,就是响应式系统的核心。
  • v-for为什么需要key:key 是虚拟 DOM diff 时的唯一标识。没有 key 时,列表更新只能按顺序比较,导致状态错乱;有了 key,Vue 可以精确复用和移动节点。模板里提供了 index 作为 key,虽然能跑但删除中间项时性能会明显下降,一般用业务 id 最合适。
  • computed 与 watch 的区别:computed 是派生状态,有缓存,依赖不变就不重新计算;watch 是副作用监听,适合执行异步操作、请求接口这类场景。

其实掌握这些知识点不需要死记硬背,把这些机制当过手代码里实际遇到的性能问题、响应丢失问题去理解,面试时反而能讲得更有说服力。


说实话,Vue 这套东西上手真不难,难的是把环境问题、工程化细节、联调流程挨个踩完。我见过很多新人第一周就卡在npm install、卡在路由跳转 404、卡在 token 失效不知道怎么处理,最后就开始怀疑自己适不适合写代码。我的建议是:遇到问题先读一遍英文报错原文,再去搜报错里的关键片段,少用中文整段复述去搜索。前端社区非常活跃,你遇到的几乎每一个报错都已经有人贴过完整解决方案。把本文推荐的环境、路由拦截、axios 封装、WebSocket 封装这几个模板工程搭起来,剩下的业务代码无非就是参照同样的套路往里填内容。最后再分享一个小技巧:刻意给自己布置一个“从 0 到 1 搭一个带登录、路由守卫、接口联调的后台管理页面”的小项目,不依赖任何脚手架模板手写一遍,等你写完之后,Vue 的入门关卡基本就全过了。

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

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

立即咨询