主题
项目介绍与 vue-admin 认知(三)
本页对应 项目介绍与 vue-admin 认知,共 7 段视频,约 1 小时 5 分。
学习目标
- 能够为登录表单实现点击登录时的统一校验,并通过
validate回调的布尔值控制是否继续执行登录逻辑。 - 能够使用 Vuex 模块化定义
state、mutations、actions管理用户 token,并在组件中通过dispatch传入正确参数,验证 token 已存入 Vuex。 - 能够识别并修复两类常见错误:action 名称找不到(拼写、命名空间、模块未注册)以及模块导入导出方式不匹配导致的
is not a function错误。 - 能够在登录成功后先等待 token 返回再跳转首页并提示用户,并能说明为什么必须保证异步顺序。
- 能够实现 token 持久化:存入 Vuex 的同时写入 Cookie,初始化时优先从本地读取,并通过刷新页面验证 token 不丢失。
- 能够从存储空间、是否允许后端操作、是否自动随接口发送三个维度对比 Cookie 与 localStorage,并解释 Vuex 与持久化存储配合使用的原因。
知识点
项目周期与登录模块的定位
本节笔记给出的项目周期是:后台部分 8 天 + 可视化前台部分 3 天 = 11 天。
为什么需要:先明确周期,才能判断哪些是后台基础能力、哪些是前台展示能力,避免把登录、权限、请求封装这类基础模块拖到后面。
怎么想:后台 8 天优先完成登录、Vuex 管理 token、路由跳转、请求封装等;可视化前台 3 天再处理展示与图表。登录是后台入口,也是后续接口依赖 token 的前提。
怎么做:按“基础模块→业务模块→可视化”的顺序推进;登录模块内部再拆成表单校验、接口登录、token 存储、跳转提示、持久化。
易错点:只盯着页面效果,忽略 token、请求封装等基础链路;后面接口依赖 token 时才回头补,容易返工。
点击登录时的统一校验
为什么需要:失焦校验只在用户聚焦后再失焦时触发。如果用户一进来不聚焦,直接点“登录”,基础校验可能根本没生效。
怎么想:点击登录时,先拿到表单实例,调用统一校验方法,把所有需要校验的表单项校验一遍;全部通过后再调登录接口。
怎么做:
js
const loginFormRef = ref(null)
async function handleLogin() {
const valid = await loginFormRef.value.validate()
if (!valid) return
// 校验通过后再执行登录
await store.dispatch('user/login', form)
}调试时先什么都不填点登录,valid 应为 false;填写正确后再点,valid 应为 true,满足预期后才继续。
易错点:只做失焦校验,不做提交兜底校验;不判断 validate() 的返回值;校验没通过就调用接口。
用 Vuex 管理 token
为什么需要:登录目的是拿到 token,而 token 会在多个模块、多个接口中使用,属于需要共享的数据,适合放到 Vuex 统一管理。
怎么想:Vuex 里定义 state 存 token,mutation 同步修改,action 里调登录接口并提交 mutation;组件只负责触发 action,不把异步接口逻辑堆在组件里。
怎么做:
js
// store/modules/user.js
const state = () => ({
token: getToken() || ''
})
const mutations = {
setToken(state, newToken) {
state.token = newToken
}
}
const actions = {
async login(context, { username, password }) {
const res = await loginAPI({ username, password })
const token = res.data.token
context.commit('setToken', token)
setToken(token) // 持久化,见下一节
}
}组件中:
js
await store.dispatch('user/login', form)调试时先打开 Network 面板看登录接口参数和返回值,确认返回结构里有 data.token;再到 Vue 调试工具里看 Vuex 中 token 是否存进去。转写稿里就出现过只写了 res.data、漏掉 .token 的问题,检查返回格式后才发现。
易错点:token 初始值写成空对象而不是空串;action 里忘记先调接口再 commit;提交时漏取 .token;namespaced 拼写错误;模块写了但入口没有注册。
登录成功后的跳转与提示
为什么需要:登录成功后要进入首页,并提示用户登录成功。但跳转和提示属于业务代码,不应堆进 Vuex;Vuex 只处理跟用户数据相关的内容。
怎么想:跳转和提示放在业务组件里做;同时必须保证 token 已经返回后再跳转,否则首页若有依赖 token 的接口,可能请求失败。
怎么做:
js
await store.dispatch('user/login', form)
router.push('/')
message.success('登录成功')关键是 await:要等 dispatch 内部的异步接口完成、token 返回后再跳转。
易错点:不 await,接口还没回来就跳首页;把跳转、提示写进 Vuex,职责混乱。
token 持久化与刷新不丢
为什么需要:Vuex 存在内存里,刷新就丢;而 token 通常一段时间内有效,重复请求没有意义,所以需要持久化。
怎么想:固定套路是“一式两份”:存 Vuex 时也存一份到 cookie 或 localStorage;初始化 token 时优先从本地取,取不到再给空串。
怎么做:
js
// 存:Vuex 与 cookie 各一份
context.commit('setToken', token)
setToken(token)
// 初始化:优先从本地取
const state = () => ({
token: getToken() || ''
})调试时先看 Vuex 有值,再看 cookie 面板当前域名下是否有 token;刷新后 Vuex 仍有值,说明初始化时成功取到了本地缓存。
易错点:只存 Vuex 不存本地;初始化时忘记调用 getToken();cookie 插件按需导入却写成默认导入,常见报错是 object (...) is not a function,要顺着报错定位导入导出是否匹配。
调试顺序与常见错误
为什么需要:登录链路包含表单、接口、Vuex、路由、缓存,混在一起测容易不知道错在哪。
怎么想:分段验证:先测接口通不通,再看参数和返回值;再测 Vuex 是否存对;最后测刷新后 cookie 是否还在。
怎么做:先开 Network 看登录请求参数和 data.token;再开 Vue 调试工具看 Vuex;最后看 cookie。常见 Vuex 报错如 action type 找不到,优先查 namespaced 拼写、模块是否注册、action 名是否写错。
易错点:一上来乱点,报错后不知道是哪一层;看到 ... is not a function 不检查导入导出;忽略异步,token 未回来就跳转。
另外,cookie 与 localStorage 的对比可按维度记:localStorage 空间约 5MB、纯前端操作、不自动跟随请求;cookie 空间几 KB、前后端都可操作、可自动跟随接口发送。当前场景二者都能做持久化,选一个配合 Vuex 即可。
技术栈总览:先把笔记里的清单摆清楚
本节笔记原文列出的技术栈是:
后台管理平台
- Vue2.6
- Vue-admin-template
- Vuex
- ElementUI
- js-cookie
- dayjs
前台可视化
- Vue3
- Echarts
- Spline
- VScaleScreen
微前端接入 - qiankun
为什么需要:一个项目不是“会用 Vue 就行”,而是要知道每项技术解决哪一层问题。后台管理偏表单、权限、状态共享;前台可视化偏图表、视觉展示;微前端偏多应用接入。
怎么想:先按“工程骨架—状态管理—UI 组件—工具库—可视化—微前端”分层理解,不要把技术栈当成名词堆砌。
怎么做:后台管理以 Vue2.6 和 Vue-admin-template 为骨架,Vuex 管共享状态,ElementUI 提供表单与后台组件,js-cookie 做 token 持久化,dayjs 处理时间。
易错点:转写稿里的“VS/VBS/AVX”应理解为 Vuex,“偷分/头梗”应理解为 token,“C S 杠 cooking”应理解为 js-cookie;不要被同音字带偏。
Vue-admin-template 与后台管理骨架
为什么需要:企业项目多数是二次迭代,直接使用成熟后台模板能快速获得目录结构、路由、请求封装和基础布局。转写稿总结也强调:实际开发中从零初始化项目的机会较少,后台管理里 Vue-admin-template 这类架子很常见。
怎么想:先看目录职责:package.json 管命令和依赖,main.js 做全局初始化,App.vue 是根组件,router 配路由,Vuex 管状态,request/api 管请求与接口。
怎么做:新增页面时按现有目录放组件、配路由;入口文件只做全局初始化,不堆业务代码。
易错点:执行 npm 命令要在项目根目录;入口文件里不要写大量业务逻辑;依赖要分清生产依赖和开发依赖。
Vuex 管理 token:组件只负责触发 action
为什么需要:登录目的是拿 token,而 token 会在多个模块共享,放进组件局部状态不好维护。
怎么想:按 Vuex 固定模式走:state 定义数据,mutations 同步修改,actions 调接口并提交 mutation,组件只 dispatch action。
怎么做:转写稿中的关键代码可概括为:
js
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.$store.dispatch('user/login', this.form)
}
})js
state: { token: '' },
mutations: {
setToken(state, newToken) {
state.token = newToken
}
},
actions: {
async login({ commit }, form) {
const data = await loginApi(form)
commit('setToken', data.token)
}
}调试时先看 Network:确认请求参数、返回值格式;转写稿中曾发现返回是 data.token,但存的时候少写了一层 .token,修正后才存入 Vuex。
易错点:namespaced 拼写错误、模块未在 modules 注册、action 名拼写不一致,都会报“action type 找不到”;导入导出不匹配会报“... is not a function”,要按报错定位到具体文件和行。
ElementUI 表单校验:基础校验之外还要统一兜底
为什么需要:ElementUI 的表单项通常在失焦时校验;如果用户一进来不聚焦、直接点登录,基础校验可能不触发。
怎么想:用户输入时做实时校验,点击登录时再做一次统一校验,只有全部通过才调登录接口。
怎么做:通过 ref 拿表单实例,调用 validate,回调里的 valid 为 true 才继续登录。
易错点:只做失焦校验不做提交校验;拿到表单实例后忘记调用 validate;valid 为假时仍继续请求。
js-cookie 持久化:一式两份,刷新回填
为什么需要:Vuex 存在内存里,刷新会丢;token 在一段时间内有效,重复请求没有意义,所以要持久化。
怎么想:存 token 时同时写 Vuex 和 Cookie;初始化 state 时优先从 Cookie 取,取不到再给空串。
怎么做:
js
Cookies.set('token', newToken)
state: {
token: Cookies.get('token') || ''
}转写稿中用 js-cookie 封装了 setToken/getToken,调试时先刷新看 Vuex 是否丢失,再打开 Cookie 面板确认是否写入。
易错点:只存 Vuex 不存本地;初始化时忘记调用 getToken();Cookie 和 localStorage 面板看错位置。二者对比可从存储空间、后端是否可操作、是否自动随请求发送三个层面说,当前场景二者都能做持久化。
登录跳转与前台可视化、微前端边界
为什么需要:登录 action 是异步的,token 未返回就跳首页,依赖 token 的接口可能失败。
怎么想:跳转和提示属于业务代码,放组件里;Vuex 只处理用户数据相关逻辑。
怎么做:等待 dispatch 完成后再 router.push 和提示登录成功。
易错点:不等待异步 action 直接跳转。前台可视化中的 Vue3、Echarts、Spline、VScaleScreen,以及微前端 qiankun,笔记只列出技术选型,本文不编造未讲到的实现细节。
代码示例
示例 1:登录组件 src/views/login/index.vue(基础校验 + 统一校验 + 登录跳转)
vue
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<el-form-item prop="username">
<el-input v-model="loginForm.username" placeholder="用户名" />
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
show-password
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
loading: false,
// 表单收集到的数据,最终要交给 vuex 的 action
loginForm: { username: '', password: '' },
// 基础校验:失去焦点时触发
loginRules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
}
},
methods: {
handleLogin() {
// 第一步:拿到表单实例对象,调用 validate 做统一(兜底)校验
this.$refs.loginForm.validate(async (valid) => {
// valid 为 true 才代表所有表单项都通过校验
if (!valid) return
this.loading = true
try {
// 第二步:触发 action,await 保证 token 已经存好之后才往下走
await this.$store.dispatch('user/login', this.loginForm)
// 第三步:跳转首页 + 提示
this.$router.push('/')
this.$message.success('登录成功')
} catch (err) {
this.$message.error(err.message || '登录失败')
} finally {
this.loading = false
}
})
}
}
}
</script>关键行与易错点
ref="loginForm"是拿到表单实例的唯一入口,方法里必须写成this.$refs.loginForm,ref 名字写错就是undefined.validate报错。validate(callback)的回调参数valid才表示「所有表单项都通过」,必须if (!valid) return,否则校验形同虚设。- 基础校验靠
trigger: 'blur',用户不聚焦直接点登录就不会触发,所以必须再加一次点击时的统一校验——两步都要有。 - 回调必须声明为
async,并用await dispatch(...):dispatch内部有接口请求,不await就可能 token 还没回来就跳到首页,首页依赖 token 的接口会失败。 - 跳转和提示属于业务代码,写在组件里,不要堆到 vuex 里。
示例 2:用户模块 src/store/modules/user.js(vuex 管理 token)
js
import { login } from '@/api/user'
import { getToken, setToken } from '@/utils/auth'
const state = {
// 初始化时优先从本地取,取不到再兜底成空串
token: getToken() || ''
}
const mutations = {
// 同步修改,第二个参数即传下来的新 token
SET_TOKEN(state, token) {
state.token = token
}
}
const actions = {
// 解构赋值约束了调用方必须传 { username, password }
async login({ commit }, { username, password }) {
// 第一步:调登录接口
const res = await login({ username, password })
// 第二步:取 token(注意层级:res.data.token)
const token = res.data.token
// 第三步:一式两份,一份进 vuex,一份进 cookie
commit('SET_TOKEN', token)
setToken(token)
return token
}
}
export default {
// 拼写必须准确:namespaced
namespaced: true,
state,
mutations,
actions
}关键行与易错点
state.token初值写成getToken() || '':token 是字符串类型,不要初始化成空对象;||是刷新不丢失的关键兜底。namespaced: true少写一个字母(如name space/namespce)会报unknown action type,组件里dispatch('user/login')就找不到。- action 里只做「调接口 + commit」两件事,异步请求必须写在 action 里,不要在组件里调接口再 commit mutation。
res.data.token少写.token会把整个对象存进去,接口返回结构一定要先在 Network 面板确认。- 解构
{ username, password }是最优写法:函数体一看就知道该传什么参数,比传整个 form 对象更规范。
示例 3:src/utils/auth.js + src/api/user.js + src/store/index.js(持久化与注册)
js
// ---------- src/utils/auth.js ----------
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}js
// ---------- src/api/user.js ----------
import request from '@/utils/request'
// 按需导出:使用时必须 import { login }
export function login(data) {
return request({
url: '/api/user/login',
method: 'post',
data
})
}js
// ---------- src/store/index.js ----------
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
// 忘了注册子模块,同样会报 unknown action type
user
}
})关键行与易错点
- 持久化的固定套路就两步:存 token 时「一式两份」(vuex + cookie),刷新时「初始化优先从本地取」,用 cookie 还是 localStorage 在当前场景没区别,都是保证刷新不丢失。
getToken是函数,必须getToken()调用,写成getToken就把函数体本身当成了初始值。- 导入导出必须匹配:
api/user.js是export function login(按需导出),组件里若写成import login from '@/api/user'(默认导入)就会报... is not a function,看到这个报错直接去对应文件检查导出方式。 store/index.js的modules里一定要把user放进去,模块写了不注册等于没写。- cookie 存的是纯前端操作,且会自动跟随请求发送;localStorage 需要手动取出放进请求头,面试对比时按「存储空间 / 是否允许后端操作 / 是否跟随接口发送」三个维度说。
常见错误
不聚焦直接点登录,校验形同虚设
- 现象:用户不点输入框直接提交,空表单照样发请求。
- 原因:只挂了 blur/change 触发的基础校验,提交前没有兜底。
- 修法:点击登录时先拿表单实例调
validate,回调第一个参数为true才进入后续登录逻辑。
跳转抢在 token 回来之前
- 现象:登录成功提示正常,但首页某些依赖 token 的接口 401 或报错。
- 原因:
dispatch返回 Promise,没await就执行了路由跳转和提示。 - 修法:把跳转/提示写在
await dispatch(...)之后(或.then里),保证 token 落地再跳。
控制台报
[vuex] unknown action type- 现象:点登录报 action 找不到,代码看着没毛病。
- 原因:三种——
namespaced拼错、子模块没在modules里注册、dispatch的 action 名拼错。 - 修法:先看方括号前缀确认是 vuex 报错,再依次核对
namespaced拼写 →modules注册 → dispatch 名称,三处对齐即可。
报
... is not a function(带三个点的对象)- 现象:调用接口函数时报
object(...) is not a function。 - 原因:导入导出方式不匹配,接口是按需导出(
export const),调用方写成了默认导入。 - 修法:点错误定位到具体文件行,找到对应 import,改成
{ loginAPI }按需导入;凡见此错误一律先怀疑导入导出。
- 现象:调用接口函数时报
存 token 少了一层字段,值变成整个对象
- 现象:vuex 里 token 存的是
{ token: 'xxx' }而不是字符串,后续带 token 请求异常。 - 原因:没看接口返回结构,直接
res.data而非res.data.token。 - 修法:先在 Network 面板确认返回体层级,再取对应字段;初始值类型也按后端返回类型定(字符串就初始化为
'')。
- 现象:vuex 里 token 存的是
刷新后登录态丢失
- 现象:F5 一下 vuex 里 token 变空,被踢回登录页。
- 原因:vuex 是内存态,只存了一份。
- 修法:存储时一式两份(vuex + cookie/localStorage),初始化 state 时优先从本地取,取不到再兜底空串。
把跳转、提示写进 store
- 现象:功能能跑,但 store 里出现
router.push、Message.success。 - 原因:职责边界没划清,只考虑"能实现"。
- 修法:store 只管用户数据相关(token 存取),跳转与提示回归业务组件。
- 现象:功能能跑,但 store 里出现
拦截器忘了 return
- 现象:请求发出去但拿不到响应,或 config 丢失导致请求异常。
- 原因:请求拦截器没
return config,响应拦截器没return res.data。 - 修法:两个拦截器函数体末尾务必补上 return,剥离 data 的逻辑放在响应拦截器里统一做。
练习
登录兜底校验:给登录按钮加统一校验,要求校验不通过时按钮不发起任何请求,且控制台能看到校验结果。验收:空表单点击登录,Network 面板无请求。
异步时序验证:把
await dispatch故意去掉,在首页加一个依赖 token 的接口,观察报错;再恢复await对比结果。验收:能用自己的话说清"为什么必须等"。错误排查三连:分别制造
namespaced拼错、模块未注册、action 名拼错三种情况,记录控制台报错文本的共同点与差异。验收:能在 30 秒内定位到是哪一处写错。持久化闭环:完成 token 一式两份存取,然后做三次验证——登录后看 cookie 是否有值、刷新后看 vuex 是否还在、手动清 cookie 后刷新是否回到登录态。验收:三次结果都符合预期。
表单提交前自检:写一个含 4 个字段的表单,提交前先做两步检查——用 devtools 确认双向绑定收集到值、对照接口文档核对字段名与类型。验收:能列出你实际踩到的字段名/类型不一致问题。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 登录-点击登录统一校验 | 05:04 |
| 2 | 登录-使用vuex管理用户token | 15:23 |
| 3 | 补充-action错误和模块化错误 | 04:40 |
| 4 | 登录-跳转到首页提示用户 | 04:55 |
| 5 | 登录-token持久化操作 | 08:40 |
| 6 | 补充-存取方式对比 | 07:20 |
| 7 | 重点内容总结 | 19:12 |
小结
- 校验是两段式的:实时校验负责体验,兜底校验负责安全,缺一段都不算完整。
- 凡是"先拿数据、再做事"的链路,都必须让异步串起来,跳转与提示永远排在 token 落地之后。
- 状态管理的边界是数据,不是业务;store 里出现路由和弹窗,就是越界信号。
- 持久化的套路是固定的:写入时一式两份,初始化时本地优先、取不到再兜底。
- 排错第一步看错误来源前缀(vuex / 模块化 / 原生),前缀决定了排查方向,能省掉大半时间。
- 导入导出必须成对:按需导出配按需导入,默认导出配默认导入,错配的报错形态高度一致。
- 测试要分段:先验接口通不通,再验数据存没存对,最后验页面表现,一段一段确认比一把梭高效得多。
- 面试表述要有层次:存储方案对比按空间、操作方、是否随请求发送等维度分点说,比想到哪说到哪更可信。