Skip to content

项目介绍与 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 即可。

技术栈总览:先把笔记里的清单摆清楚 ​

本节笔记原文列出的技术栈是:

后台管理平台

  1. Vue2.6
  2. Vue-admin-template
  3. Vuex
  4. ElementUI
  5. js-cookie
  6. dayjs

前台可视化

  1. Vue3
  2. Echarts
  3. Spline
  4. 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 为假时仍继续请求。

为什么需要: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 需要手动取出放进请求头,面试对比时按「存储空间 / 是否允许后端操作 / 是否跟随接口发送」三个维度说。

常见错误 ​

  1. 不聚焦直接点登录,校验形同虚设

    • 现象:用户不点输入框直接提交,空表单照样发请求。
    • 原因:只挂了 blur/change 触发的基础校验,提交前没有兜底。
    • 修法:点击登录时先拿表单实例调 validate,回调第一个参数为 true 才进入后续登录逻辑。
  2. 跳转抢在 token 回来之前

    • 现象:登录成功提示正常,但首页某些依赖 token 的接口 401 或报错。
    • 原因:dispatch 返回 Promise,没 await 就执行了路由跳转和提示。
    • 修法:把跳转/提示写在 await dispatch(...) 之后(或 .then 里),保证 token 落地再跳。
  3. 控制台报 [vuex] unknown action type

    • 现象:点登录报 action 找不到,代码看着没毛病。
    • 原因:三种——namespaced 拼错、子模块没在 modules 里注册、dispatch 的 action 名拼错。
    • 修法:先看方括号前缀确认是 vuex 报错,再依次核对 namespaced 拼写 → modules 注册 → dispatch 名称,三处对齐即可。
  4. 报 ... is not a function(带三个点的对象)

    • 现象:调用接口函数时报 object(...) is not a function。
    • 原因:导入导出方式不匹配,接口是按需导出(export const),调用方写成了默认导入。
    • 修法:点错误定位到具体文件行,找到对应 import,改成 { loginAPI } 按需导入;凡见此错误一律先怀疑导入导出。
  5. 存 token 少了一层字段,值变成整个对象

    • 现象:vuex 里 token 存的是 { token: 'xxx' } 而不是字符串,后续带 token 请求异常。
    • 原因:没看接口返回结构,直接 res.data 而非 res.data.token。
    • 修法:先在 Network 面板确认返回体层级,再取对应字段;初始值类型也按后端返回类型定(字符串就初始化为 '')。
  6. 刷新后登录态丢失

    • 现象:F5 一下 vuex 里 token 变空,被踢回登录页。
    • 原因:vuex 是内存态,只存了一份。
    • 修法:存储时一式两份(vuex + cookie/localStorage),初始化 state 时优先从本地取,取不到再兜底空串。
  7. 把跳转、提示写进 store

    • 现象:功能能跑,但 store 里出现 router.push、Message.success。
    • 原因:职责边界没划清,只考虑"能实现"。
    • 修法:store 只管用户数据相关(token 存取),跳转与提示回归业务组件。
  8. 拦截器忘了 return

    • 现象:请求发出去但拿不到响应,或 config 丢失导致请求异常。
    • 原因:请求拦截器没 return config,响应拦截器没 return res.data。
    • 修法:两个拦截器函数体末尾务必补上 return,剥离 data 的逻辑放在响应拦截器里统一做。

练习 ​

  1. 登录兜底校验:给登录按钮加统一校验,要求校验不通过时按钮不发起任何请求,且控制台能看到校验结果。验收:空表单点击登录,Network 面板无请求。

  2. 异步时序验证:把 await dispatch 故意去掉,在首页加一个依赖 token 的接口,观察报错;再恢复 await 对比结果。验收:能用自己的话说清"为什么必须等"。

  3. 错误排查三连:分别制造 namespaced 拼错、模块未注册、action 名拼错三种情况,记录控制台报错文本的共同点与差异。验收:能在 30 秒内定位到是哪一处写错。

  4. 持久化闭环:完成 token 一式两份存取,然后做三次验证——登录后看 cookie 是否有值、刷新后看 vuex 是否还在、手动清 cookie 后刷新是否回到登录态。验收:三次结果都符合预期。

  5. 表单提交前自检:写一个含 4 个字段的表单,提交前先做两步检查——用 devtools 确认双向绑定收集到值、对照接口文档核对字段名与类型。验收:能列出你实际踩到的字段名/类型不一致问题。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1登录-点击登录统一校验05:04
2登录-使用vuex管理用户token15:23
3补充-action错误和模块化错误04:40
4登录-跳转到首页提示用户04:55
5登录-token持久化操作08:40
6补充-存取方式对比07:20
7重点内容总结19:12

小结 ​

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