主题
完成登陆与根据TOKEN获取个人信息(Lesson21 1/2)
本页对应 Lesson21,共 7 段视频,约 1 小时 28 分。
学习目标
- 能够完成登录表单的数据收集与校验,调用登录 API,并根据返回状态给出成功或失败提示及页面跳转。
- 能够解释 Token 认证流程,并在请求隐私接口时通过请求头携带 Token 获取个人信息。
- 能够封装 Token 的保存、获取、删除方法,并在登录成功后保存 Token、在获取用户信息时正确调用封装方法。
- 能够将登录后获取到的用户信息保存到 Vuex,并说明为什么不应将用户信息直接持久化到 localStorage。
- 能够使用
v-if替代不合适的v-show,避免头部组件在路由切换时重复触发分类数据请求。 - 能够根据 Vuex 中的用户信息在头部组件中条件渲染登录态内容,并定位刷新后个人信息丢失的原因并给出解决思路。
知识点
先明确:登录按钮点下去要发生什么
为什么需要:登录不是“点一下按钮”就结束,它要完成“收集手机号/密码 → 调登录接口 → 根据返回 code 判断成功失败 → 成功提示并跳首页,失败提示账号或密码错误”。转写稿里反复强调先顺业务流程:点击登录后先看表单数据有没有收集到,再看 action 有没有调用,再看 API 地址和请求方式对不对。
怎么想:按“页面 → store → api”三层拆。页面只负责收集参数和触发 action;store 负责组织登录成功/失败后的业务行为;api 只负责发请求。这样后续加 token、获取个人信息时不会把逻辑全堆在组件里。
怎么做:先写 src/api/user.js 的登录接口,再写 src/store/user.js 的 action,最后在 src/pages/Login/index.vue 里提交表单。
易错点:action 有命名空间时,dispatch 要写 "user/postLoginAsync";store 里用 router.push 必须引入 router;表单提交要阻止默认行为,否则页面会刷新。
接口层:把登录请求收敛到 api/user.js
为什么需要:接口地址、请求方式集中管理,页面和 store 不直接写 URL。笔记里登录是 post,参数放在请求体里,所以用 sphRequest.post("/user/passport/login", body)。
js
// 登陆 /api/user/passport/login post
export const postLogin = body =>sphRequest.post("/user/passport/login",body);怎么想:body 就是 { phone, password }。转写稿里说,文档没特别指定请求体格式时,按常见 JSON/对象方式传,第二个参数直接放对象。
怎么做:在 store 中 import { postLogin } from "@/api/user";,调用后解构 { code, data }。
易错点:不要把 phone、password 作为两个位置参数传,接口封装接收的是一个 body 对象。
store层:登录成功后跳转的两种写法
为什么需要:登录成功后的提示、跳转属于业务动作。放 store 里可以复用,放组件里则更直观。笔记给了两种方案。
js
import {postLogin} from "@/api/user";
import {Message} from "element-ui";
import router from "@/router";
.....
// 1
async postLoginAsync(content,body){
const {code,data} = await postLogin(body);
if(code === 200){
Message.success("恭喜您,登陆成功!");
router.push("/");// 跳转至首页
}else{
Message.error("账号或密码错误!");
}
}
// 2
// async postLoginAsync(content,body){
// const {code,data} = await postLogin(body);
// if(code === 200){
// Message.success("恭喜您,登陆成功!");
// return data;
// }
// Message.error("账号或密码错误!");
// return new Promise(()=>{});
// }怎么想:方案 1 在 store 内直接跳首页,组件只负责 dispatch,推荐初学者使用。方案 2 把成功数据返回给组件,由组件决定跳转;失败时返回一个永远 pending 的 Promise,阻止组件后续 push 执行。
怎么做:方案 1 中 router 必须从 @/router 引入;方案 2 中组件要 await this.$store.dispatch(...) 后再 this.$router.push("/")。
易错点:content 是 Vuex action 的上下文参数,当前没用到但位置不能省;body 才是 dispatch 传过来的第二个参数。方案 2 若不 await,登录请求还没成功就可能先跳转。
页面层:阻止默认提交并收集表单
为什么需要:form 默认提交会刷新页面,导致 JS 流程中断。笔记用 @submit.prevent="login" 阻止默认行为,再用 e.target.phone.value、e.target.password.value 收集输入框值。
vue
<template>
<!-- 登录 -->
<div class="login-wrap">
<div class="login">
<div class="loginform">
<div class="content">
<form @submit.prevent="login">
<div class="input-text clearFix">
<i></i>
<input type="text" name="phone" placeholder="手机号">
<span class="error-msg">错误提示信息</span>
</div>
<div class="input-text clearFix">
<i class="pwd"></i>
<input type="text" name="password" placeholder="请输入密码">
<span class="error-msg">错误提示信息</span>
</div>
<button class="btn">登 录</button>
</form>
<div class="call clearFix">
<router-link to="/register" class="register">立即注册</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
methods:{
// 1
async login(e){
const phone = e.target.phone.value.trim();
const password = e.target.password.value.trim();
if(!phone || !password) {
this.$message.error("手机号与密码不允许为省略");
return;
}
console.log(this.$router)
await this.$store.dispatch("user/postLoginAsync",{
phone,
password
})
}
// 2
// async login(e){
// const phone = e.target.phone.value.trim();
// const password = e.target.password.value.trim();
// if(!phone || !password) {
// this.$message.error("手机号与密码不允许为省略");
// return;
// }
// await this.$store.dispatch("user/postLoginAsync",{
// phone,
// password
// });
// this.$router.push("/")
// }
}
}
</script>怎么想:输入框必须有 name,否则 e.target.phone 拿不到。转写稿调试时就是先点登录,在控制台看 e.target 是否是表单、e.target.phone.value 是否有值。
怎么做:先 trim() 去空格,空值提示并 return;再 await dispatch。方案 1 的跳转已在 store 完成;方案 2 才在组件里 push("/")。
易错点:路由地址建议以 / 开头;提示文案“不允许为省略”按笔记保留,实际项目可改为“不能为空”;this.$message 来自 Element UI 原型挂载,store 中则用引入的 Message。
调试顺序与易错点清单
为什么需要:转写稿说“闭着眼睛排 bug”:点登录 → 表单收集 → action 调用 → api 地址 → 网络请求 → 返回 code。按链路查最快。
怎么做:打开 Network 看是否请求 /user/passport/login;看请求体是否有 phone/password;看返回 code 是否 200;成功看是否提示并跳 /,失败看是否提示“账号或密码错误”。
易错点:① 忘记 @submit.prevent,点击后刷新;② input 没写 name;③ dispatch 命名空间写错;④ store 没引入 router;⑤ 方案 2 忘记 await 就跳转;⑥ 登录成功后的“从哪来回哪去”、token 保存和获取个人信息属于后续优化,本节先把基本登录闭环跑通。
先理清:为什么要用 token 获取个人信息
为什么需要: 登录成功后,后端虽然返回了用户相关信息,但刷新页面或进入个人中心、交易页等隐私接口时,服务端并不知道“这次请求是谁发的”。HTTP 请求本身是无状态的,所以需要 token 作为用户标识。转写稿里反复提到的“投盔、偷给、图给、都K”都是同音转写,实际应统一理解为 token。
怎么想: token 的完整链路是:前端提交手机号和密码 → 服务端校验用户并生成 token → 前端保存 token 到 localStorage → 访问隐私接口时携带 token → 服务端验证 token 是否合法、是否过期 → 成功返回该用户信息,失败则前端清除 token 并重新登录。
怎么做: 登录成功后先把 data.token 存入 localStorage,再调用获取个人信息接口;获取个人信息时必须在请求头里带上 token。转写稿中调试时接口返回 208,被解释为“未登录/没有 token”,说明请求没有携带 token 或 token 无效。
易错点: 不要把 token 写死在某个页面里;也不要把用户信息长期只存在 localStorage 里当作登录状态。localStorage 会长期保存,token 过期后页面仍可能显示旧信息,造成“还登录着”的错觉。token 字段名和请求头字段名要以接口文档为准,不要自己猜。
流程拆解:从登录到携带 token 请求
为什么需要: 只写登录接口不够,登录后还要继续拿个人信息,才能渲染“欢迎谁谁谁”、头像、昵称等内容。
怎么想: 按转写稿的排错思路,登录按钮点下去后依次检查:有没有收集表单 → 有没有调用 Vuex action → action 有没有调用 API → API 地址和请求方式对不对 → 请求有没有带 token → 后端返回的 code 是什么。
怎么做: 本节接口层新增:
js
// 根据token获取用户信息 /api/user/passport/auth/getUserInfo GET
export const getUserInfo = ()=>sphRequest("/user/passport/auth/getUserInfo");Vuex 中登录成功后保存 token,并等待获取个人信息完成:
js
// 1
async postLoginAsync({dispatch},body){
const {code,data} = await postLogin(body);
if(code === 200){
Message.success("恭喜您,登陆成功!");
localStorage.setItem("token",data.token);
await dispatch("getUserInfoAsync");
router.push("/");// 跳转至首页
}else{
Message.error("账号或密码错误!");
}
},
// 获取个人信息
async getUserInfoAsync(){
const result = await getUserInfo();
console.log(result);
}易错点: await dispatch("getUserInfoAsync") 里的 await 不能丢。转写稿中特别强调:如果不加 await,登录请求还没完全处理完就执行跳转,容易出现“还没登录成功就跳走”的问题。
请求封装:把 token 放进请求头
为什么需要: 如果每个接口都手写一次请求头,重复且容易漏。转写稿建议把 token 处理集中到封装的 request 里。
怎么想: 先判断本地有没有 token,有值再放到请求头;没有值就不要硬塞。具体请求头字段名看接口文档,转写稿没有给出固定字段名,所以不要编造。
怎么做: 在 sphRequest 封装中读取 localStorage.getItem("token"),存在时加入请求头。也可以像转写稿提到的,把“保存 token、获取 token、删除 token”封装成独立方法,集中管理。
易错点: token 可能放在请求头,也可能按文档放在请求体,不能死记;字段名也不能随意改。转写稿里说“文档写 A 你就写 A,不要自己写 B”。
调试与易错点:208、异步和刷新丢失
为什么需要: 获取个人信息失败时,必须能快速定位是登录没成功、token 没保存、请求头没带,还是 token 过期。
怎么想: 先看登录接口返回里有没有 data.token;再看 localStorage 里有没有 token;再看 Network 里 getUserInfo 请求头有没有 token;最后看返回 code。转写稿中登录成功返回里有 token,获取信息接口却返回 208,原因就是请求没带 token。
怎么做: 本节先用 console.log(result) 验证能否拿到个人信息;确认成功后再把 data 保存到 Vuex 的 userInfo,供头部组件用 v-if 判断渲染。转写稿还提到 v-model、v-if 被同音写成“违杠mod、微钢衣服”,实际都是 Vue 指令。
易错点: 个人信息不要只存 localStorage;刷新后 Vuex 数据会丢失,后续需要处理持久化或重新获取。失败时清除 token 并重新登录,成功时再渲染页面内容。
一、为什么要把 token 操作单独封装
为什么需要:登录成功后,后端会把 token 放在响应数据的 data.token 里返回。之后访问“获取用户信息”等隐私接口时,请求头里必须带上这个 token,后端才能知道“是谁在请求”。如果每个组件都直接写 localStorage.getItem("token")、localStorage.setItem("token", token),代码会分散,字段名也容易写错。
怎么想:转写稿里提到,token 操作在不同地方都会用到:登录成功后要保存,请求拦截器里要读取,退出登录时要删除。既然都是对同一个 localStorage 键做操作,就应该集中到 src/utils/auth.js 里,对外只暴露 getToken、saveToken、removeToken 三个方法。
怎么做:本节笔记中的封装代码如下:
js
// 获取token
export const getToken = ()=> localStorage.getItem("token");
// 保存token
export const saveToken = token =>localStorage.setItem("token",token);
// 删除token
export const removeTOken = ()=>localStorage.removeItem("token");易错点:笔记里删除方法写成了 removeTOken,其中 O 是大写。实际调用时必须和导出名完全一致;更建议统一写成 removeToken,避免调用时写成 removeToken 却导出了 removeTOken。另外 localStorage 只能存字符串,保存前要确认传入的是 token 字符串,而不是整个对象。
二、登录成功后保存 token,再获取用户信息
为什么需要:登录接口成功只代表账号密码正确,页面还需要拿到用户信息来渲染“欢迎你,某某”。而获取用户信息接口需要 token,所以必须先保存 token,再发获取用户信息的请求。转写稿里调试时发现:登录成功后调用 getUserInfo 返回 208 未登录,原因就是请求没有携带 token。
怎么想:登录成功的判断依据是响应里的 code === 200。成功后按顺序做三件事:保存 token、获取用户信息、跳转首页。顺序不能反,因为 dispatch("getUserInfoAsync") 内部发请求时要从本地取 token。
怎么做:src/store/user.js 中:
js
async postLoginAsync({dispatch},body){
const {code,data} = await postLogin(body);
if(code === 200){
Message.success("恭喜您,登陆成功!");
// 1
saveToken(data.token);
await dispatch("getUserInfoAsync");
router.push("/");// 跳转至首页
}else{
Message.error("账号或密码错误!");
}
},易错点:saveToken(data.token) 要在 dispatch 之前;dispatch 前加了 await,是为了等用户信息获取完再跳首页。转写稿里也强调:如果不加 await,登录请求还没处理完就可能继续往下走,流程会乱。还要注意从响应中取的是 data.token,不是 code 或整个 data。
三、在请求拦截器里统一携带 token
为什么需要:获取用户信息、购物车、订单等接口都需要 token。如果每个接口都手写 headers.token,既重复又容易漏。请求拦截器可以在请求发出前统一读取 token 并放进请求头。
怎么想:转写稿里说,token 一般放在请求头里,字段名要看接口文档;本项目字段名是 token。同时项目里还有 userTempId,所以拦截器里先设置临时用户标识,再判断 token 是否存在,存在才加到 config.headers.token。
怎么做:src/request/sphRequest.js 中:
js
sphRequest.interceptors.request.use(config => {
nprogress.start();// 开启进度条
config.headers.userTempId = getUserTempId()
const token = getToken();
if(token)
config.headers.token = token;
return config;
});易错点:拦截器最后必须 return config,否则请求发不出去;if(token) 不能省略,没有 token 时不要硬塞空值;请求头字段名必须和后端文档一致,不能自己改成别的名字。转写稿里调试时就是通过浏览器网络面板查看 getUserInfo 的请求头,确认 token 是否带上。
四、调试顺序与易错点
为什么需要:token 流程横跨登录接口、Vuex、本地存储、请求拦截器,任何一环出错都会表现为“获取用户信息失败”或“未登录”。
怎么想:转写稿给了一个很实用的排查顺序:点登录后,先看表单内容有没有收集到;再看登录接口有没有调用、参数对不对;再看响应里有没有 data.token;再看 saveToken 有没有执行;最后看 getUserInfo 请求头里有没有 token。
怎么做:打开网络面板,登录成功后检查登录响应中的 data.token;再检查 getUserInfo 请求头是否出现 token。如果返回 208 未登录,就回到 getToken() 和请求拦截器检查。退出登录时调用删除方法清除 token,并清理用户信息。
易错点:removeTOken 的拼写要统一;token 过期后不能继续当作已登录,应清除并重新登录;不要把 token 只放在组件里,否则刷新或换页面就取不到。
先明确目标:登录成功后保存个人信息
为什么需要:登录成功后,头部要显示“欢迎谁谁谁”,交易页、个人中心也要知道当前用户是谁。只有 token 还不够,页面渲染需要用户信息。
怎么想:登录接口成功后,后端会返回 token;前端保存 token 后,再调用“获取个人信息”接口,把返回的 data 存到 Vuex 的 user 模块里。这样头部组件就能从 store 读取 userInfo 做条件渲染。
怎么做:在 src/store/user.js 中准备 state.userInfo、mutations.SAVE_USER_INFO、actions.getUserInfoAsync。登录成功后调用 action,action 内请求接口并 commit 保存。
易错点:获取个人信息是异步请求,登录成功后如果没 await,可能在用户信息还没回来时就跳转或渲染,导致头部短暂不显示。转写稿里也强调:异步函数没执行完就往下走,会出问题。
为什么不把个人信息放 localStorage
为什么需要:有人会想,既然刷新会丢,那直接把个人信息放 localStorage 不就行了?看起来最省事。
怎么想:localStorage 的特点是永久保存,除非用户手动删除或硬盘损坏。问题在于 token 会过期,但 localStorage 里的个人信息不会自动消失。token 过期后页面仍然能读到旧个人信息,用户会误以为自己还处于登录状态,这是不合理的。笔记里明确说:“如果 token 过期了,localStorage 也存在,这种情况不合理。”
怎么做:个人信息这种“会话态”数据不直接放 localStorage;放到 store 的内存里。token 按项目方案保存,但个人信息不靠 localStorage 长期撑页面。
易错点:不要把“token 可以持久化”和“个人信息可以永久保存”混为一谈。token 过期后要清理,个人信息也不能一直显示旧值。
放进 store:刷新丢失是已知问题
为什么需要:store 是内存状态,适合保存当前会话的用户信息,组件读取方便,修改也集中。
怎么想:保存到 store 后,刷新页面数据会丢失。这不是不能接受,而是需要后续解决:刷新时根据 token 重新获取个人信息。笔记里说“可以将个人信息保存至 store 当中,但刷新数据会丢失。我们可以解决丢失的问题。”
怎么做:在 user 模块里用 state.userInfo 保存,用 mutation 修改,用 action 请求接口。
易错点:模块开启了 namespaced: true,外部调用 action 要带模块名,例如 user/getUserInfoAsync;模块内部 commit("SAVE_USER_INFO") 可以提交本地 mutation。mutation 名要和代码一致,不能拼错。
代码落地:user 模块
本节笔记代码保留如下:
js
const state = {
userInfo:null
}
const mutations = {
SAVE_USER_INFO(state,userInfo){
state.userInfo = userInfo;
}
}
const actions = {
// 获取个人信息
async getUserInfoAsync({commit}){
const result = await getUserInfo();
commit("SAVE_USER_INFO",result.data);
}
}
export default {
namespaced:true,
actions,
state,
mutations
}这段代码的关键点:getUserInfoAsync 里先 await getUserInfo(),拿到 result 后把 result.data 提交给 SAVE_USER_INFO。实际项目里 getUserInfo 需要从 API 文件引入;如果接口要求带 token,要在请求封装里加请求头。转写稿里调试时就是先看网络请求有没有调用 getUserInfo,再确认请求头是否带 token,最后看返回的 data 是否提交进 store。
调试过程:登录后看 state,刷新后变 null
为什么需要:保存个人信息不是写完就完,要确认 action 有没有被调用、mutation 有没有生效、state 有没有变化。
怎么想:按业务流程排查——点击登录收集表单 → 调登录接口 → 保存 token → 调获取个人信息 action → action 调 API → commit 保存 → 组件读取。
怎么做:登录后打开 Vue Devtools 查看 user/userInfo,能看到昵称、头像等字段;刷新页面后 userInfo 变回 null,说明 store 内存态被重置。网络面板里确认 getUserInfo 是否发出、请求头是否带 token、返回结构是否为 result.data。
易错点:mutation 没在模块里注册、action 没被调用、commit 名字写错、接口返回层级不是 result.data、namespaced 下外部调用没带 user/,都会导致“看起来登录了但 store 没数据”。
易错点与后续解决思路
个人信息放 store 后,头部组件可以用 userInfo 是否存在做 v-if:有值显示欢迎信息和退出登录,无值显示登录/注册。刷新丢失的解决思路是:刷新后读取 token,再调用获取个人信息接口重新写入 store;token 失效则清理并引导重新登录。不要为了省事把个人信息直接塞进 localStorage,否则 token 过期后仍显示旧用户信息,体验和逻辑都不合理。
本节目标与前置状态
为什么需要:登录成功后,页面头部不能还显示“请登录 / 免费注册”,而应展示当前用户信息,例如昵称,并提供“退出登录”入口。头部是全局组件,登录态变化后它要能跟着变。
怎么想:头部组件不负责登录,也不负责请求用户信息;它只负责“根据 Vuex 中是否已有用户信息”切换两套 UI。前置流程应是:登录成功 → 调用获取个人信息接口 → 把返回的用户信息保存到 Vuex 的 user 模块 → Header 通过映射拿到 userInfo 后渲染。
怎么做:在 src/components/Header/index.vue 中,用 v-if="userInfo" 和 v-else 分成登录后、未登录两套结构。本节笔记代码如下,需原样保留:
vue
<template>
<div class="loginList">
<p v-if="userInfo">
<span>尚品汇欢迎{{userInfo.nickName}}!</span>
<a href="" class="register">退出登陆</a>
</p>
<p v-else>
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
</p>
</div>
</template>易错点:不要用 token 直接渲染昵称。token 只能作为用户标识,昵称、头像等要从用户信息接口得到;而且 token 可能过期,不能凭它判断“当前一定已登录并显示用户资料”。
为什么用 userInfo 判断登录态
为什么需要:转写稿里调试时发现,登录后调用获取个人信息接口返回 208,表示后端认为“未登录”。原因是 HTTP 请求是无状态的,后端不知道这次请求是谁发的,必须携带 token。这也说明:头部要显示用户信息,前提是前端已经拿到并保存了用户信息。
怎么想:登录成功后,后端返回 token,前端把 token 保存到 localStorage,并在请求拦截器或请求头中携带;随后调用获取个人信息接口,把返回的 data 保存到 Vuex 的 user 模块。头部只读 Vuex 的 userInfo。
怎么做:在 Header 的 script 中把 user 模块的 userInfo 映射为组件可用的计算属性(转写稿中使用 mapState 的方式),模板里就能直接写 userInfo。若未映射,模板会找不到变量。
易错点:userInfo 的初始值要保证未登录时能被 v-if 判为假。若写成空对象 {},v-if="userInfo" 会误判为真,头部会错误显示“欢迎”。实际实现中应使用 null,或改成判断 userInfo.nickName 等有效字段。
模板两套分支怎么写
为什么需要:登录与未登录的头部内容完全不同,用两套分支最直观,避免在一个结构里堆很多三元表达式。
怎么想:登录后显示“尚品汇欢迎昵称!”和“退出登录”;未登录显示“请 / 登录 / 免费注册”。登录、注册用 router-link,不要用普通 a 标签做站内跳转。
怎么做:按笔记代码写 v-if="userInfo" 和 v-else。昵称字段用 userInfo.nickName。退出登录当前先用 <a href="" class="register">退出登陆</a> 占位,后续再绑定退出逻辑。
易错点:router-link 的 to 地址要以 / 开头,例如 /login、/register。转写稿特别强调:若写成相对路径,从商品页等位置点击时可能跳到“当前路径下的 login”,而不是项目根路由下的登录页。退出登录的 a href="" 只是占位,直接点击可能刷新页面,后续应改为事件处理。
调试与验证过程
为什么需要:头部不显示用户信息时,要按业务流程排查,而不是盲改模板。
怎么想:按“登录按钮是否收集表单 → 是否调用 Vuex action → action 是否调 API → API 是否带 token → 后端是否返回用户信息 → Vuex 是否保存 → Header 是否映射”的顺序查。
怎么做:登录后打开 Vue Devtools,查看 Vuex 的 user 模块中 userInfo 是否有昵称等字段;再回首页看 Header 是否从“请登录”切换为“尚品汇欢迎xxx!”。转写稿中还提到:刷新后 Vuex 数据会丢失,这是后续要解决的问题;本节先把“登录后头部渲染”跑通。
易错点:如果登录成功但头部不变,先确认 userInfo 是否真的保存进 Vuex,再确认 Header 是否用 mapState 映射;如果 userInfo 有值但页面仍显示未登录分支,检查 v-if 的变量名和初始值。
问题现象:刷新后个人信息丢失
为什么需要:登录成功后,用户信息通常保存在 Vuex 的 store.state.user.userInfo 中,头部组件据此渲染“欢迎你,某某”。但刷新页面会重新创建 Vuex 状态,userInfo 变成空值,页面就退回“登录/注册”的展示。转写稿里已经明确:保存到 Vuex 刷新会丢失;保存到 localStorage 又不合适,因为 token 可能过期,但本地用户信息还在,会给用户“仍然登录”的错觉。
怎么想:不要靠“登录成功后顺手保存昵称”来解决刷新,而要在每次进入需要用户信息的页面时,先检查有没有 token、Vuex 里有没有 userInfo;缺了就重新请求个人信息。
怎么做:把这件事放到全局路由前置守卫里,在路由放行前补齐用户信息。
易错点:不能无条件请求用户信息,否则每次路由跳转都会发请求;必须先判断 getToken() 和 !store.state.user.userInfo。
全局前置守卫:先补信息再放行
为什么需要:刷新后第一次路由跳转时,Vuex 是空的,如果直接渲染页面,头部组件拿不到用户信息;如果等到组件内再请求,容易出现“先显示未登录、后闪现登录态”的抖动。
怎么想:路由守卫发生在页面进入前,适合做“进入前准备数据”。用 async/await 等 dispatch 完成后再 next(),保证进入页面时 userInfo 已经写好。
怎么做:在 src/router/index.js 中写全局前置守卫:
js
router.beforeEach(async function (to, from, next) {
if (getToken() && !store.state.user.userInfo)
await store.dispatch("user/getUserInfoAsync");
next();
});这段逻辑的含义是:有 token,并且 Vuex 里没有用户信息时,调用 user/getUserInfoAsync 获取并保存用户信息;完成后统一放行。转写稿中登录成功后也会调用获取个人信息的方法,因此正常登录流程下 userInfo 已有值,守卫不会重复请求;只有刷新导致 Vuex 重建时才会触发补请求。
易错点:next() 要放在 await 之后且只调用一次;不要写成 if (...) { dispatch(); } next();,否则请求还没回来就放行,刷新后仍可能丢信息。getUserInfoAsync 内部要使用保存好的 token 发请求,并把结果 commit 到 userInfo。
调试与验证:沿着业务流程排查
为什么需要:这类问题不是“页面不显示”这么简单,要确认 token 是否存在、请求是否发出、请求头是否带 token、返回后是否写入 Vuex。
怎么想:按转写稿里的排查顺序走:点击登录是否收集到表单 → 是否调用 action → action 是否调用 API → API 地址和请求头是否正确 → 获取个人信息后是否 commit。刷新场景则先看 getToken() 是否有值,再看 store.state.user.userInfo 是否为空,最后看守卫是否发出 getUserInfoAsync。
怎么做:登录后进入首页,刷新页面,观察网络面板是否请求获取个人信息接口;请求成功后检查 Vuex 中 userInfo 是否恢复,头部是否重新渲染。若刷新后仍不显示,优先检查守卫里是否漏了 await,以及 getUserInfoAsync 是否把返回数据 commit 到 userInfo。
易错点:token 过期时不要继续把旧用户信息当作有效登录态;退出登录要同时清理 token 和 userInfo。另外,保存 token 一般放 localStorage,用户信息放 Vuex,再由守卫在刷新时补齐,避免把个人信息长期缓存在本地造成状态不一致。
课堂画面






代码示例
1. 登录功能完整链路(api → store → 组件)
js
// ===== src/api/user.js =====
import sphRequest from "@/request/sphRequest";
// 登陆 /api/user/passport/login post
export const postLogin = body => sphRequest.post("/user/passport/login", body);
// 根据token获取用户信息 /api/user/passport/auth/getUserInfo GET
export const getUserInfo = () => sphRequest("/user/passport/auth/getUserInfo");js
// ===== src/store/user.js =====
import { postLogin, getUserInfo } from "@/api/user";
import { Message } from "element-ui";
import router from "@/router";
import { saveToken } from "@/utils/auth";
const state = {
userInfo: null
};
const mutations = {
SAVE_USER_INFO(state, userInfo) {
state.userInfo = userInfo;
}
};
const actions = {
// 登录:成功后在 action 内部完成保存 token + 取用户信息 + 跳转
async postLoginAsync({ dispatch }, body) {
const { code, data } = await postLogin(body);
if (code === 200) {
Message.success("恭喜您,登陆成功!");
saveToken(data.token); // 保存 token,供后续隐私接口使用
await dispatch("getUserInfoAsync"); // 等个人信息拿到再跳
router.push("/"); // 跳转至首页
} else {
Message.error("账号或密码错误!");
}
},
// 获取个人信息
async getUserInfoAsync({ commit }) {
const result = await getUserInfo();
commit("SAVE_USER_INFO", result.data);
}
};
export default {
namespaced: true,
state,
mutations,
actions
};vue
<!-- ===== src/pages/Login/index.vue ===== -->
<template>
<!-- 登录 -->
<div class="login-wrap">
<div class="login">
<div class="loginform">
<div class="content">
<form @submit.prevent="login">
<div class="input-text clearFix">
<i></i>
<input type="text" name="phone" placeholder="手机号" />
<span class="error-msg">错误提示信息</span>
</div>
<div class="input-text clearFix">
<i class="pwd"></i>
<input type="text" name="password" placeholder="请输入密码" />
<span class="error-msg">错误提示信息</span>
</div>
<button class="btn">登 录</button>
</form>
<div class="call clearFix">
<router-link to="/register" class="register">立即注册</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
methods: {
async login(e) {
const phone = e.target.phone.value.trim();
const password = e.target.password.value.trim();
if (!phone || !password) {
this.$message.error("手机号与密码不允许为空");
return;
}
await this.$store.dispatch("user/postLoginAsync", {
phone,
password
});
}
}
};
</script>关键行与易错点
@submit.prevent="login":表单默认提交会刷新页面,必须.prevent;<button class="btn">不写type时默认就是submit,点按钮等同提交表单,所以事件绑在form上。e.target.phone.value:监听在form上,e.target是 form 本身,靠input的name属性取值——name忘了写就取不到值(会报Cannot read property 'value' of undefined)。- 判空后
return,避免带着空手机号发请求。 dispatch("user/postLoginAsync", ...)必须带命名空间前缀,因为 store 里写了namespaced: true。await不能省:不写await,router.push会在登录响应回来之前执行(转写稿里第二种方案踩的就是这个坑)。- 跳转两种方案:①在 action 里
router.push("/")(推荐,行为集中在 store);②在组件里await dispatch(...)之后再this.$router.push("/"),此时await更不能漏。 store/user.js里import router from "@/router"才能用router.push,忘了引入会直接报错。
2. token 操作封装 + 请求拦截器统一携带 token
js
// ===== src/utils/auth.js =====
// 获取token
export const getToken = () => localStorage.getItem("token");
// 保存token
export const saveToken = token => localStorage.setItem("token", token);
// 删除token
export const removeToken = () => localStorage.removeItem("token");js
// ===== src/request/sphRequest.js =====
import axios from "axios";
import nprogress from "nprogress";
import { getToken } from "@/utils/auth";
import { getUserTempId } from "@/utils/userTempId";
const sphRequest = axios.create({
baseURL: "/api",
timeout: 5000
});
sphRequest.interceptors.request.use(config => {
nprogress.start(); // 开启进度条
config.headers.userTempId = getUserTempId();
const token = getToken();
if (token) config.headers.token = token; // 有 token 才携带
return config;
});
sphRequest.interceptors.response.use(
response => {
nprogress.done();
return response.data; // 业务层直接拿到 { code, data, message }
},
error => {
nprogress.done();
return Promise.reject(error);
}
);
export default sphRequest;关键行与易错点
- 把
localStorage的三个操作集中到utils/auth.js,避免各处手写"token"键名写错、写漏;store 里只需saveToken(data.token)。 - token 加在请求拦截器里而不是某个接口的
headers里,这样所有请求自动携带,不用每个接口写一遍。 if (token)判断不能少:没登录时直接拼token: undefined会把脏头发给后端。- 请求头的 key(这里是
token)必须按后端文档写,不能自己起名字。 - 响应拦截器
return response.data是 store 里const { code, data } = await postLogin(body)能成立的前提;如果项目里已有响应拦截器,不要重复添加。 nprogress.done()在成功和失败回调里都要调,只在成功里调会导致请求报错时进度条一直转。
3. 个人信息渲染 + 刷新后信息丢失的解决
vue
<!-- ===== src/components/Header/index.vue ===== -->
<template>
<div class="loginList">
<p v-if="userInfo">
<span>尚品汇欢迎{{ userInfo.nickName }}!</span>
<a href="" class="register">退出登陆</a>
</p>
<p v-else>
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
</p>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
name: "Header",
computed: {
...mapState("user", ["userInfo"])
}
};
</script>js
// ===== src/router/index.js =====
import Vue from "vue";
import VueRouter from "vue-router";
import store from "@/store";
import { getToken } from "@/utils/auth";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
Vue.use(VueRouter);
const router = new VueRouter({
routes: [
{ path: "/", component: Home },
{ path: "/login", component: Login },
{ path: "/register", component: Register }
]
});
router.beforeEach(async (to, from, next) => {
if (getToken() && !store.state.user.userInfo) {
await store.dispatch("user/getUserInfoAsync");
}
next();
});
export default router;关键行与易错点
...mapState("user", ["userInfo"])要带模块名,否则取到的是根 state 上的userInfo(undefined),v-if永远走 else 分支。userInfo初始值必须是null而不是{}:空对象是 truthy,会让v-if="userInfo"在未登录时也显示"欢迎 xxx"。- 个人信息不要存 localStorage:localStorage 永久保存,token 过期后它还在,用户会误以为自己仍是登录态;存 store 刷新即丢,反而与 token 状态一致。
- 刷新丢数据的解法就是路由守卫:
getToken()有值但store.state.user.userInfo为空时,重新dispatch拉一次个人信息,然后再next()。 - 守卫里的
!store.state.user.userInfo判断是必要的优化,否则每次路由跳转(包括进注册页再回首页)都会重复请求一次个人信息。 - 注意依赖方向:
store/user.js里import router from "@/router",而router/index.js又import store from "@/store",属于循环依赖,若出现router.push is not a function,先检查这里。
常见错误
现象:点击“登录”后页面刷新,接口偶发重复请求。
原因:表单提交未阻止默认行为,或只给按钮绑定事件但未处理form submit。
修法:统一使用<form @submit.prevent="login">,登录按钮保持type="submit",避免按钮和表单重复绑定。现象:登录成功后偶尔直接跳首页,但用户信息尚未拿到,头部仍显示“请登录”。
原因:组件里调用dispatch("user/postLoginAsync")后未await,或 action 内部未await dispatch("getUserInfoAsync")就执行跳转。
修法:把“登录→保存 token→获取用户信息→跳转”放在同一异步链路中,逐段await;失败分支直接return或抛出中断。现象:在
store/user.js中调用Message、router.push报未定义或行为异常。
原因:Vuex 模块中不能直接使用组件实例的this.$message、this.$router,且未显式引入。
修法:按项目约定显式引入Message和router;或让 action 只返回结果,由组件层负责提示与跳转,保持职责清晰。现象:登录后请求用户信息接口返回 208/未登录,或请求头中没有 token。
原因:token 未保存、保存字段名与后端文档不一致,或请求拦截器未在 token 存在时挂到请求头。
修法:统一通过saveToken/getToken/removeToken操作 token;请求拦截器先取值再判断,有值才写入后端约定的 header 字段。现象:刷新页面后头部登录态丢失,但 localStorage 里仍有 token。
原因:用户信息只存在 Vuex 内存中,刷新后 state 重置,未在路由守卫中根据 token 恢复用户信息。
修法:在全局前置守卫中判断“有 token 且无 userInfo”时await dispatch("user/getUserInfoAsync"),失败则清理 token 并跳登录。现象:把用户信息直接长期存入 localStorage,token 过期后页面仍显示旧用户信息。
原因:混淆了“身份凭证持久化”和“用户资料缓存”的生命周期,localStorage 不会随 token 过期自动失效。
修法:token 可持久化;用户信息优先放 Vuex,通过 token 拉取恢复;退出登录或接口返回未授权时同时清理 token 与用户状态。
练习
- 将登录成功后的固定跳转首页改为“从哪来回哪去”:进入登录页时携带
redirect,登录成功后优先跳回来源页,无来源时回首页。 - 对比两种登录实现:A 在 Vuex action 内完成提示、保存 token、获取用户信息并跳转;B 由组件
await dispatch后自行跳转。分别写出优缺点,并说明失败分支如何中断。 - 封装
getToken/saveToken/removeToken后,补齐“退出登录”流程:清理 token、清理用户信息、重置头部显示、跳转登录页或首页。 - 在路由守卫中实现刷新恢复登录态,并补充 token 失效处理:获取用户信息失败时清理状态并跳转登录页。
- 检查 Header 组件中
v-if与v-show的使用场景,说明为什么分类请求不应因频繁切换显示而重复触发,并给出修改方案。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 完成登陆 | 35:17 |
| 2 | token的流程 | 22:35 |
| 3 | 根据TOKEN获取个人信息 | 07:24 |
| 4 | token操作封装 | 06:38 |
| 5 | 小优化 | 02:19 |
| 6 | 保存个人信息 | 07:38 |
| 7 | 头部组件实现对登陆以后的个人信息的渲染 | 06:25 |
小结
- 登录功能的交付标准不是“接口通了”,而是“身份凭证保存、用户信息恢复、UI 状态同步、失败可回退”形成闭环。
- token 是跨请求的身份凭证,必须统一存取与携带;字段名、放置位置以接口文档为准,不能凭习惯硬写。
- 用户信息适合作为内存状态管理,token 适合持久化;不要把长期用户资料当作登录态的唯一来源。
- 登录链路中所有依赖前一步结果的异步操作都必须
await,失败分支要显式中断,否则会出现“未登录先跳转”的竞态。 - 刷新丢失是状态生命周期问题,恢复逻辑应放在路由守卫等全局入口,而不是散落在各组件。
- 头部登录态应由状态驱动渲染,避免直接操作 DOM 或依赖临时变量;同时注意
v-if/v-show对请求副作用的影响。 - 接口返回未授权时,前端要能清理登录态并引导重新登录;这比单纯提示错误更符合真实会话管理。