Skip to content

完成登陆与根据TOKEN获取个人信息(Lesson21 1/2) ​

本页对应 Lesson21,共 7 段视频,约 1 小时 28 分。

学习目标 ​

  1. 能够完成登录表单的数据收集与校验,调用登录 API,并根据返回状态给出成功或失败提示及页面跳转。
  2. 能够解释 Token 认证流程,并在请求隐私接口时通过请求头携带 Token 获取个人信息。
  3. 能够封装 Token 的保存、获取、删除方法,并在登录成功后保存 Token、在获取用户信息时正确调用封装方法。
  4. 能够将登录后获取到的用户信息保存到 Vuex,并说明为什么不应将用户信息直接持久化到 localStorage。
  5. 能够使用 v-if 替代不合适的 v-show,避免头部组件在路由切换时重复触发分类数据请求。
  6. 能够根据 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">登&nbsp;&nbsp;录</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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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">登&nbsp;&nbsp;录</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,先检查这里。

常见错误 ​

  1. 现象:点击“登录”后页面刷新,接口偶发重复请求。
    原因:表单提交未阻止默认行为,或只给按钮绑定事件但未处理 form submit。
    修法:统一使用 <form @submit.prevent="login">,登录按钮保持 type="submit",避免按钮和表单重复绑定。

  2. 现象:登录成功后偶尔直接跳首页,但用户信息尚未拿到,头部仍显示“请登录”。
    原因:组件里调用 dispatch("user/postLoginAsync") 后未 await,或 action 内部未 await dispatch("getUserInfoAsync") 就执行跳转。
    修法:把“登录→保存 token→获取用户信息→跳转”放在同一异步链路中,逐段 await;失败分支直接 return 或抛出中断。

  3. 现象:在 store/user.js 中调用 Message、router.push 报未定义或行为异常。
    原因:Vuex 模块中不能直接使用组件实例的 this.$message、this.$router,且未显式引入。
    修法:按项目约定显式引入 Message 和 router;或让 action 只返回结果,由组件层负责提示与跳转,保持职责清晰。

  4. 现象:登录后请求用户信息接口返回 208/未登录,或请求头中没有 token。
    原因:token 未保存、保存字段名与后端文档不一致,或请求拦截器未在 token 存在时挂到请求头。
    修法:统一通过 saveToken/getToken/removeToken 操作 token;请求拦截器先取值再判断,有值才写入后端约定的 header 字段。

  5. 现象:刷新页面后头部登录态丢失,但 localStorage 里仍有 token。
    原因:用户信息只存在 Vuex 内存中,刷新后 state 重置,未在路由守卫中根据 token 恢复用户信息。
    修法:在全局前置守卫中判断“有 token 且无 userInfo”时 await dispatch("user/getUserInfoAsync"),失败则清理 token 并跳登录。

  6. 现象:把用户信息直接长期存入 localStorage,token 过期后页面仍显示旧用户信息。
    原因:混淆了“身份凭证持久化”和“用户资料缓存”的生命周期,localStorage 不会随 token 过期自动失效。
    修法:token 可持久化;用户信息优先放 Vuex,通过 token 拉取恢复;退出登录或接口返回未授权时同时清理 token 与用户状态。

练习 ​

  1. 将登录成功后的固定跳转首页改为“从哪来回哪去”:进入登录页时携带 redirect,登录成功后优先跳回来源页,无来源时回首页。
  2. 对比两种登录实现:A 在 Vuex action 内完成提示、保存 token、获取用户信息并跳转;B 由组件 await dispatch 后自行跳转。分别写出优缺点,并说明失败分支如何中断。
  3. 封装 getToken/saveToken/removeToken 后,补齐“退出登录”流程:清理 token、清理用户信息、重置头部显示、跳转登录页或首页。
  4. 在路由守卫中实现刷新恢复登录态,并补充 token 失效处理:获取用户信息失败时清理状态并跳转登录页。
  5. 检查 Header 组件中 v-if 与 v-show 的使用场景,说明为什么分类请求不应因频繁切换显示而重复触发,并给出修改方案。

原视频对照 ​

#视频时长
1完成登陆35:17
2token的流程22:35
3根据TOKEN获取个人信息07:24
4token操作封装06:38
5小优化02:19
6保存个人信息07:38
7头部组件实现对登陆以后的个人信息的渲染06:25

小结 ​

  1. 登录功能的交付标准不是“接口通了”,而是“身份凭证保存、用户信息恢复、UI 状态同步、失败可回退”形成闭环。
  2. token 是跨请求的身份凭证,必须统一存取与携带;字段名、放置位置以接口文档为准,不能凭习惯硬写。
  3. 用户信息适合作为内存状态管理,token 适合持久化;不要把长期用户资料当作登录态的唯一来源。
  4. 登录链路中所有依赖前一步结果的异步操作都必须 await,失败分支要显式中断,否则会出现“未登录先跳转”的竞态。
  5. 刷新丢失是状态生命周期问题,恢复逻辑应放在路由守卫等全局入口,而不是散落在各组件。
  6. 头部登录态应由状态驱动渲染,避免直接操作 DOM 或依赖临时变量;同时注意 v-if/v-show 对请求副作用的影响。
  7. 接口返回未授权时,前端要能清理登录态并引导重新登录;这比单纯提示错误更符合真实会话管理。