资讯详情

Vue3 入门到能用:响应式、组件化、路由与登录后台实战

📅 2026/9/30 8:06:26 | 华诺云谱 👁 阅读
Vue3 入门到能用:响应式、组件化、路由与登录后台实战
前端开发这几年变化挺快的Vue3 从 2020 年发布到现在生态基本稳了招聘 JD 里写 Vue3 的岗位占比也早就超过 Vue2。但我在带新人的过程中发现一个挺尴尬的现象很多人搜Vue3入门教程收藏了一堆视频和文档真正动手把项目跑起来、把响应式搞明白的没几个。问题不在于资料少而在于大多数教程要么一上来就讲编译原理和源码要么把官方文档念一遍缺少一条从小白到能上手做项目的完整路径。这篇东西我想按自己带人的思路写把 Vue3 的学习拆成能落地的几步环境怎么搭、响应式怎么理解、组件怎么拆、路由和状态管理怎么接、最后手把手做一个带登录的后台雏形。内容偏向实操会讲清楚每个选择背后的原因也会把我和同事踩过的坑摊开说。不管你是完全零基础还是 Vue2 转过来想补 Vue3 的跟着走一遍应该能把入门到能用这段路走通。1. 学 Vue3 之前先把这几个问题想明白我在教人学 Vue3 时第一步不是打开编辑器而是先聊清楚方向。方向错了后面花的时间基本都是浪费。这一节把几个最容易被忽略但又最重要的问题讲透帮你少走弯路。1.1 Vue2 和 Vue3 到底差在哪值不值得现在入坑先说结论如果你现在准备入行或者换项目直接学 Vue3不用纠结。Vue2 在 2023 年底已经停止维护新项目基本不会再选它。但这不代表 Vue2 的东西全废了两者在模板语法指令组件化这些表层概念上是一脉相承的你学会 Vue3回头看 Vue2 的代码也能看懂七八成。真正的差异集中在三块。第一块是响应式实现。Vue2 用的是Object.defineProperty它在初始化时就得遍历对象的所有属性去劫持所以有两个经典毛病新增属性不会触发更新得用$set、数组下标赋值和长度修改监听不到。Vue3 换成了Proxy直接代理整个对象动态新增的属性能被感知数组操作也不用特殊处理。我用一个生活类比帮你记Vue2 像是在房子每个房间门口装传感器你后加的隔断房间没装就监控不到Vue3 是给整栋楼装了一个全景摄像头里面怎么改都看得见。第二块是代码组织方式。Vue2 用选项式 APIOptions API一个组件的 data、methods、computed、生命周期都写在各自的格子里。逻辑一多同一个功能的代码被拆到四五个地方改一个需求要上下翻。Vue3 引入组合式 APIComposition API允许你按功能把相关代码放在一起甚至可以抽成独立的函数复用。这一点在第 1.2 节会展开讲。第三块是性能与工程能力。Vue3 的编译优化静态提升、补丁标记、更好的 TypeScript 支持、Tree-shaking 带来的更小打包体积这些都是实打实的提升。不过对初学者来说前三块里前两块才是影响你写代码体验的关键性能那部分知道有这回事就行不用现在就深挖。1.2 组合式 API 不是语法糖是思维方式的变化很多刚转过来的朋友会有个误区以为组合式 API 就是把data()里的东西挪到setup()里换汤不换药。我一开始也这么想直到重写了一个几百行的复杂表单组件才反应过来——它改变的是代码按什么维度组织。选项式 API 是按类型组织的数据归数据、方法归方法、计算属性归计算属性。组合式 API 是按业务逻辑组织的搜索相关的状态、方法、监听放一起分页相关的放一起。好处在组件变大以后特别明显你想改搜索这个功能眼睛只需要盯着一块区域不用在文件里跳来跳去。更实际的价值是逻辑复用。Vue2 时代复用逻辑主要靠 mixin但 mixin 有命名冲突、来源不清晰的老问题出了 bug 很难查。Vue3 可以把一段逻辑写成普通的函数社区里叫 composable习惯以use开头命名比如useUser()、usePagination()谁用谁引入来源清清楚楚。我自己的项目里凡是超过两个组件用到的逻辑基本都会抽成 composable。还有个心态上的建议刚上手别急着追最佳实践。先把setup语法糖script setup用熟把ref、reactive、computed、watch这几个用明白等有感觉了再研究provide/inject、自定义 composable 这些进阶玩法。顺序搞反了容易劝退自己。1.3 小白的学习路径排布别一上来就啃源码我见过不少人在入门阶段就开始研究 Vue3 的 diff 算法、编译器的 patchFlag 是怎么生成的。这些东西有价值但不该在入门阶段碰。原因是你不知道它解决什么问题看的时候只能死记硬背过两天全忘。我给带的新人排的路径是这样的你可以参考第一阶段1 到 2 周环境搭建、模板语法、响应式基础、指令、计算属性与侦听器。目标是能独立写出一个带增删改查的列表页。第二阶段2 到 3 周组件拆分、props 与 emit、插槽、生命周期。目标是能把一个页面拆成 5 到 8 个职责清晰的组件。第三阶段2 周Vue Router、Pinia、Axios 封装。目标是能做出多页面切换、带登录态的项目。第四阶段持续读官方文档进阶章节、看优秀开源项目的源码比如后台管理模板、刷面试题补漏。提醒一句每个阶段都要有产出物哪怕是个丑丑的待办清单。光看不动手学三个月和学三天没区别。关于看官方文档这件事我强烈建议把 vuejs.org 的中文文档加到书签遇到问题第一反应应该是查官方文档而不是直接搜博客。网上很多文章是几年前写的API 早变了照抄容易踩坑。2. 环境搭建从零到跑起第一个页面环境这块是劝退重灾区。我见过太多人卡在 node 版本不对、npm 装依赖超时、vite 起不来这些问题上。这一节把从装 Node 到项目跑起来的完整流程走一遍顺带把常见坑标出来。2.1 Node.js 与包管理器怎么选、怎么装Vue3 的开发环境依赖 Node.js因为构建工具Vite跑在 Node 上。版本上我建议直接装Node 20 LTS或更高Vue3 的新版本对 Node 版本有要求太老的版本比如 14、16会遇到各种奇怪的报错。判断标准很简单如果安装或启动时报engine相关的错误八成就是 Node 版本太低。安装方式按系统分Windows去 Node 官网下载 LTS 安装包一路下一步即可注意勾选Add to PATH。macOS可以用官网安装包也可以装nvm方便切版本。Linux建议用nvm切换版本灵活。装完在终端敲node -v和npm -v能打印出版本号才算成功。如果提示不是内部或外部命令说明 PATH 没配上重装并确认勾选选项。包管理器这块npm是随 Node 自带的够用。但如果你经常遇到装依赖慢的问题可以考虑换pnpm。它的优势是硬链接复用磁盘上的包装得快、省空间在公司项目里用得很多。安装就一行npm install -g pnpm至于淘宝镜像那套现在 npm 官方源在国内的速度已经改善不少遇到慢的时候临时用--registry参数指定就行不必永久改配置永久改有时候会踩到私有包拉不下来的坑。2.2 用 Vite 创建 Vue3 项目的完整流程现在创建 Vue3 项目标准做法是 Vite官方文档也是这么推荐的。Vue CLI 那套基于 webpack 的方案在 Vue3 项目里已经逐渐退场启动慢、配置繁琐新项目没必要再用。完整命令如下# 用 pnpmnpm 同理把 pnpm create 换成 npm create pnpm create vuelatest my-vue-app敲完回车脚手架会交互式问你几个问题我的建议是这样选选项建议原因TypeScript新手可先选 No进阶选 YesTS 能提前发现类型错误但会增加入门门槛JSXNo除非你有 React 背景模板语法更直观Vue RouterYes做多页面项目必备PiniaYes状态管理早期不用但先装上Vitest可选想学测试就选ESLintYes统一代码风格团队协作必备PrettierYes自动格式化省心选完依赖会自动安装。然后cd my-vue-app pnpm install # 如果创建时没自动装 pnpm dev # 启动开发服务器启动后终端会打印一个本地地址通常是http://localhost:5173浏览器打开就能看到默认页面。到这一步环境就算通了。项目结构我简单说明一下几个重点目录src/main.js入口文件创建应用实例并挂载。src/App.vue根组件。src/components/公共组件放这里。src/views/或src/pages/页面级组件。vite.config.js构建配置配别名、代理都在这。2.3 VSCode 必装插件与几个实用配置编辑器我默认你用 VSCode。装插件不用贪多这几个够用Vue - Official原 VolarVue3 的官方语言支持插件模板里的类型提示、跳转全靠它。注意如果你之前装过 Vetur一定要禁用两个插件会打架。ESLint和Prettier配合项目里的配置自动检查、格式化代码。Auto Rename Tag改标签名自动同步闭合标签。Path Intellisense路径自动补全。配置上我建议在项目根目录建一个.vscode/settings.json把保存自动格式化打开{ editor.formatOnSave: true, editor.codeActionsOnSave: { source.fixAll.eslint: explicit } }关于你可能会搜到的在 VSCode 里怎么看网页的代码构成直接在浏览器里按 F12 打开开发者工具Elements 面板就是最终渲染出来的 DOM 结构。注意它和你在 VSCode 里写的.vue文件不是一一对应的——Vue 组件会在浏览器里被渲染成真实 DOM调试样式时以浏览器面板为准找逻辑问题时才回到源码。一个容易被忽略的点项目里配了路径别名指向src后VSCode 可能还是无法跳转。这时候需要在项目根目录加jsconfig.json或tsconfig.json里配置paths编辑器才知道是什么。3. 核心语法拆解响应式是一切的地基Vue 最核心的能力就是数据变了页面自动更新。这套机制叫响应式。把响应式搞明白后面学组件、学路由都会顺很多。这一节把响应式的几个关键 API 和常见坑讲清楚。3.1 ref 与 reactive 的取舍以及我踩过的坑Vue3 里声明响应式数据主要用两个函数ref和reactive。ref用来包裹基本类型字符串、数字、布尔和任意对象使用时在 JS 里要加.value在模板里会自动解包不用加。reactive只能包裹对象或数组不需要.value。看代码import { ref, reactive } from vue // ref 包裹基本类型 const count ref(0) count.value // JS 里改要用 .value console.log(count.value) // 0 - 1 // reactive 包裹对象 const user reactive({ name: 张三, age: 18 }) user.age 20 // 不需要 .value那到底用哪个我的实战建议是统一用 ref。原因有三点。一是团队协作时不用纠结这个变量是对象还是基本类型全都.value心智负担统一。二是reactive有解构丢失响应性的坑你写const { name } user之后name就变成普通字符串了不再是响应式。三是ref可以整个替换user.value newObjreactive不行。踩坑记录我曾经在一个表单组件里用reactive定义表单对象然后在模板里用v-modelform.name结果某个动态新增的字段死活不更新。排查半天发现是那个字段初始化时没在对象里声明。虽然 Vue3 的 Proxy 能感知新增属性但模板渲染时如果依赖收集发生在新增之前就会出现看起来没反应的现象。后来统一用ref加完整初始值问题就没了。还有个toRefs用来把reactive对象转成一组ref解决解构丢失响应性的问题。不过既然我建议统一用ref这个用到的场景就不多了知道有这么个东西即可。3.2 模板语法和常用指令的实际用法模板语法是 Vue 的门面写页面全靠它。基础的东西插值{{ }}、属性绑定v-bind、事件v-on官方文档讲得很清楚我挑几个实际开发中容易用错的说。v-if和v-show的选择。这是面试常问的。v-if是真正的条件渲染false 时元素根本不会创建v-show是切换display: none元素始终在 DOM 里。判断标准是切换频率频繁切换用v-show比如标签页、折叠面板条件基本不变用v-if比如权限控制的按钮。我见过有人在长列表的每一项上写v-if控制显隐频繁切换时性能掉得厉害换成v-show就好了。v-for的 key。key 必须是唯一且稳定的值不要用数组下标。因为下标在列表增删时会错位导致 Vue 复用错误的 DOM 节点出现删了第一项输入框里的内容却跑到第二项去了这种诡异现象。正确做法是用后端返回的id。v-model的本质。它其实是:value加input的语法糖。在自定义组件上Vue3 的默认是v-model:modelValue加update:modelValue这点和第 4.2 节讲的组件通信有关。指令还能自己封。比如做一个点击外部关闭弹窗的指令把监听逻辑抽出来// 点击元素外部时触发 const clickOutside { mounted(el, binding) { el._handler (e) { if (!el.contains(e.target)) binding.value() } document.addEventListener(click, el._handler) }, unmounted(el) { document.removeEventListener(click, el._handler) } }用的时候v-click-outsideclose就行。自定义指令在封装通用交互时特别好用值得花点时间掌握。3.3 计算属性与侦听器什么时候用哪个computed和watch都能响应数据变化但用途完全不同搞混会写出难维护的代码。computed用于派生数据它的值由其他响应式数据算出来有缓存依赖不变就不会重算。典型场景是全名由姓和名拼出来、购物车总价由商品列表算出来。const firstName ref(张) const lastName ref(三) const fullName computed(() firstName.value lastName.value)watch用于执行副作用数据变了之后要做点别的事比如发请求、存本地、打印日志。它没有缓存概念每次变化都执行。watch(keyword, async (newVal) { // 搜索关键词变了去请求接口 const res await searchApi(newVal) list.value res.data })选择标准很清晰要一个由别的数据算出来的新值用 computed数据变化后要做别的事情用 watch。我见过有人用 watch 监听 a 和 b然后手动更新 c这完全可以用 computed 替代代码还更短。关于 watch 还有两个参数值得说。immediate: true让监听在初始化时立刻执行一次deep: true让深度监听对象内部变化但要注意深度监听大对象会有性能开销能不用就不用优先监听具体的属性。另外记得在组件卸载时清理不必要的监听虽然script setup里的 watch 会自动清理但手动用addEventListener加的要自己清。3.4 生命周期钩子的执行顺序与真实用途模板写多了就知道什么时候执行什么代码很关键。Vue3 的生命周期钩子在组合式 API 里都是以on开头比如onMounted、onUnmounted。常用的几个和作用onBeforeMount/onMounted挂载前 / 挂载后。DOM 操作、发初始请求、初始化第三方库比如地图、图表都要放在onMounted因为这时候 DOM 才真实存在。onBeforeUpdate/onUpdated数据更新前后。一般很少用需要根据更新后的 DOM 做操作时才用。onBeforeUnmount/onUnmounted卸载前后。清理定时器、取消事件监听、断开 WebSocket 连接都放这里不然会内存泄漏。一个父子组件的挂载顺序父beforeMount→ 子beforeMount→ 子mounted→ 父mounted。卸载顺序反过来。这个顺序面试也爱问记一下有好处。实战提醒如果你在onMounted里操作某个子组件的 DOM 拿不到多半是因为子组件还没渲染完可以用nextTick等一个微任务。另外script setup里没有created原来的created逻辑直接写在 setup 顶层就行。4. 组件化开发把页面拆成能维护的积木单文件组件写多了你会发现真正难的不是语法而是怎么拆。拆得太碎props 传一层又一层拆得太粗一个文件上千行没法维护。这一节聊聊拆分的思路和组件通信的正确姿势。4.1 组件拆分的原则与目录组织我的拆分原则是一个组件只干一件事且这件事能用一个短语描述清楚。比如用户搜索框订单列表项统计卡片名字起得出来、描述得清楚就值得拆。如果拆出来的组件名字叫通用内容区业务容器 A这种说明拆得不对。目录组织上我一般这么分components/base/无业务的基础组件比如按钮、输入框、弹窗。这些组件理论上可以拿去任何项目复用。components/business/带业务语义的组件比如用户卡片、订单项。换个项目就用不了了。views/页面级组件一般和路由一一对应。composables/抽出来的逻辑函数。这样分的好处是你一眼能看出哪些能复用、哪些不能。基础组件可以放心大胆改业务组件改动前要确认调用方。再说一个反面的经验。我见过一个项目把一个简单的表单拆成了七八个组件父子之间靠 props 和 emit 传了三层。结果改一个校验规则得同时改四五处。这种情况下组件拆得太碎反而增加了维护成本。判断标准是如果 props 传递超过两层就该考虑把状态提到 Pinia 或者用provide/inject了。4.2 props、emit 和 v-model 双向绑定的正确姿势组件通信是组件化的核心。父子通信主要靠 props 往下传、emit 往上报。props 要声明类型和默认值这既是文档也是校验// 子组件 const props defineProps({ title: { type: String, default: }, count: { type: Number, default: 0 }, list: { type: Array, default: () [] } })注意对象和数组的默认值必须用函数返回() []直接写[]会导致所有实例共享同一个对象这是经典的坑。emit 要显式声明Vue3 里虽然不声明也能触发但声明了编辑器有提示const emit defineEmits([update, delete]) emit(update, newData)v-model的双向绑定在 Vue3 里升级了一个组件可以绑定多个v-model!-- 父组件 -- UserForm v-model:nameform.name v-model:ageform.age / !-- 子组件 -- script setup const props defineProps([name, age]) const emit defineEmits([update:name, update:age]) /script template input :valuename inputemit(update:name, $event.target.value) / /template这个模式在封装表单组件时特别常用。如果嫌每次写update:xxx麻烦社区有defineModel宏较新版本已稳定可以更省事不过老项目里不一定支持用之前确认版本。有个禁忌要记住子组件绝不能直接修改 props。props 是单向数据流你改了 Vue 会警告而且逻辑会乱。要改就在子组件里 emit 事件让父组件去改。4.3 插槽的三种形态与实战场景插槽是让组件可定制的机制。分三种默认插槽、具名插槽、作用域插槽。默认插槽最简单父组件写的内容会填到子组件的slot /位置。具名插槽让内容填到不同位置!-- 子组件 Card.vue -- div classcard div classheaderslot nameheader //div div classbodyslot //div /div !-- 父组件 -- Card template #header标题区/template 这里是默认内容 /Card作用域插槽是进阶里最有用的。它允许子组件把数据传回给插槽内容父组件可以基于这些数据自定义渲染。典型场景是表格组件表格负责渲染框架和分页每一行怎么显示交给使用方决定。!-- 子组件 Table -- tr v-forrow in rows :keyrow.id tdslot :rowrow{{ row.name }}/slot/td /tr !-- 父组件 -- Table :rowslist template #default{ row } span :style{ color: row.active ? green : gray } {{ row.name }} /span /template /Table我强烈建议你把作用域插槽练熟几乎所有优秀的 UI 组件库表格、下拉、树形控件都靠它实现高度定制。4.4 组件通信方案对比与选型建议组件多了之后通信方式也变多我把常见的方案整理成一张表方便你按场景选场景方案备注父传子props单向最常用子传父emit 事件配合 v-model 可做双向跨多层传递provide / inject避免 props 层层透传全局共享Pinia登录用户信息、主题等兄弟组件状态提升到父组件 / Pinia优先提升简单直接任意组件事件总线Vue3 已移除 $on用 mitt 之类的库替代provide/inject要单独说下用法因为它容易用错// 祖先组件 provide(themeConfig, { primary: #409eff, mode: dark }) // 后代组件任意层级 const theme inject(themeConfig)注意默认情况下provide的值不是响应式的如果你想让它响应变化要 provide 一个ref或者reactive对象。还有provide 的 key 建议用 Symbol 避免命名冲突尤其是写组件库的时候。5. 工程化进阶路由、状态管理与请求封装单页应用光有组件不够还得有页面跳转、全局状态和接口管理。这三块搞定一个完整项目的骨架就搭起来了。5.1 Vue Router 4 的配置与路由守卫Vue Router 是官方路由Vue3 对应的是 4.x 版本。基本配置在创建项目时选上 Router 就自动生成了核心是router/index.jsimport { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /, redirect: /home }, { path: /home, component: () import(/views/Home.vue) }, { path: /user/:id, component: () import(/views/User.vue), props: true } ] })几个实际会用到的点。第一路由懒加载() import(...)能让首屏只加载当前页面用到的代码后面访问再加载对打包体积和首屏速度帮助很大页面多了一定要用。第二动态路由参数用:id开props: true后可以在组件里直接用 props 接收比route.params.id干净。第三createWebHistory用的是 HTML5 History 模式URL 好看但需要服务器配合做 fallback把找不到的路径都指回 index.html不然刷新页面会 404。如果没法改服务器配置就用createWebHashHistoryURL 带#但省事。路由守卫是做权限的核心。全局前置守卫最常用router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) // 没登录踢到登录页 } else { next() // 放行 } })在路由配置里给需要登录的页面加meta: { requiresAuth: true }即可。这个逻辑第 6.3 节做登录时会用到。注意守卫里的next有坑一定要保证每个分支只调用一次重复调用会报 next() was called multiple times。用 async/await 写法时更要小心。5.2 Pinia 取代 Vuex 的理由与落地写法状态管理现在基本都推荐 Pinia它是 Vue 官方推荐的新一代方案取代 Vuex。相比 VuexPinia 少了 mutation 这一层改状态直接写方法TypeScript 支持更好模块化也更自然。一个 store 长这样// stores/user.js import { defineStore } from pinia import { ref, computed } from vue export const useUserStore defineStore(user, () { // state const userInfo ref(null) const token ref(localStorage.getItem(token) || ) // getters const isLogin computed(() !!token.value) // actions function setToken(t) { token.value t localStorage.setItem(token, t) } function logout() { token.value userInfo.value null localStorage.removeItem(token) } return { userInfo, token, isLogin, setToken, logout } })这种写法叫 setup store和组件里的组合式 API 风格一致我比较推荐。组件里用的时候import { useUserStore } from /stores/user const userStore useUserStore() console.log(userStore.isLogin) userStore.setToken(xxx)什么时候该用 Pinia我的标准是多个页面/组件需要共享且会变化的数据。比如登录用户信息、购物车、全局主题。如果只有一个组件用到就别往 store 里塞本地ref就够了。见过把所有状态都往 Pinia 塞的项目结果 store 文件比组件还复杂得不偿失。5.3 Axios 封装与接口层的组织方式实际项目里不会在每个组件里直接axios.get那样接口地址散落各处改起来要命。标准做法是封装一层。// utils/request.js import axios from axios import { useUserStore } from /stores/user const request axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) // 请求拦截带上 token request.interceptors.request.use((config) { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) // 响应拦截统一处理错误 request.interceptors.response.use( (res) res.data, (err) { const status err.response?.status if (status 401) { useUserStore().logout() window.location.href /login } return Promise.reject(err) } ) export default request然后在api/目录按业务拆分// api/user.js import request from /utils/request export const login (data) request.post(/login, data) export const getUserInfo () request.get(/user/info)组件里直接import { login } from /api/user用即可。这样接口地址、错误处理、鉴权都集中在一处改后端地址只改一个地方。环境变量通过.env文件管理开发、测试、生产的地址分开用import.meta.env.VITE_XXX读取Vite 要求环境变量以VITE_开头才会暴露给前端代码这点容易忘。6. 手把手做一个登录页加后台雏形前面都是零件这一节把它们拼起来做一个带登录和路由拦截的最小后台。做完这个你基本能把 Vue3 的常用能力串一遍。6.1 需求拆解与页面结构设计目标很简单一个登录页登录成功后跳到后台首页首页有侧边栏和顶栏未登录访问后台会被拦回登录页刷新后登录态保持。技术上涉及表单组件、路由守卫、Pinia 存 token、Axios 发请求。目录先规划好src/ ├── api/user.js # 接口 ├── stores/user.js # 用户状态 ├── router/index.js # 路由与守卫 ├── views/ │ ├── Login.vue # 登录页 │ └── layout/ │ ├── Index.vue # 后台整体布局 │ ├── Sidebar.vue # 侧边栏 │ └── Header.vue # 顶栏 └── utils/request.js # 请求封装布局组件layout/Index.vue用嵌套路由router-view /承载内容区侧边栏写死几个菜单项练手即可。6.2 表单校验与登录逻辑实现登录页的核心是表单和提交逻辑。模板部分template div classlogin-page form submit.preventhandleLogin input v-model.trimform.username placeholder用户名 / p v-iferrors.username classerr{{ errors.username }}/p input v-modelform.password typepassword placeholder密码 / p v-iferrors.password classerr{{ errors.password }}/p button typesubmit :disabledloading {{ loading ? 登录中... : 登录 }} /button /form /div /template逻辑部分script setup import { ref, reactive } from vue import { useRouter } from vue-router import { useUserStore } from /stores/user import { login } from /api/user const router useRouter() const userStore useUserStore() const form reactive({ username: , password: }) const errors reactive({ username: , password: }) const loading ref(false) function validate() { errors.username form.username ? : 请输入用户名 errors.password form.password.length 6 ? : 密码至少 6 位 return !errors.username !errors.password } async function handleLogin() { if (!validate()) return loading.value true try { const res await login(form) userStore.setToken(res.token) userStore.userInfo res.user router.push(/) } catch (e) { alert(登录失败请检查账号密码) } finally { loading.value false } } /script几个细节值得说。v-model.trim自动去掉首尾空格避免用户手滑输空格导致登录失败。校验放在提交前而不是用watch实时校验因为实时校验在输入过程中频繁报错体验不好。loading状态防止用户重复点击提交按钮这个在弱网环境下能救命。try/finally保证无论成功失败loading 都会复位。实际项目中登录还涉及验证码、记住密码、回车提交等。回车提交可以直接监听 input 的keyup.enter触发登录比要求用户点按钮友好。6.3 登录态持久化与路由拦截登录态保持靠两样东西token 存 localStorage 和路由守卫。store 里读 token 时从 localStorage 初始化见 5.2 的代码这样刷新页面 token 不会丢。退出登录时清空 localStorage 并跳回登录页。路由守卫在 5.1 已经给了框架这里补充完整router.beforeEach((to) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { return { path: /login, query: { redirect: to.fullPath } } } if (to.path /login userStore.token) { return / // 已登录就别去登录页了 } })注意这里带上redirect参数登录成功后可以跳回用户原本想访问的页面体验更好router.push(route.query.redirect || /)一个安全提醒前端存 token 和路由拦截只是体验层的防护真正的权限校验必须在后端做。任何人都能通过改前端代码绕过路由守卫所以接口一定要验证 token 的有效性和权限别把安全全押在前端。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑