主题
创建问诊项目与请求工具(二)
本页对应 创建问诊项目与请求工具,共 7 段视频,约 1 小时 11 分。
学习目标
- 能够参考接口返回定义
User类型,使用defineStore创建包含user状态、setUser、deleteUser的用户仓库,并配置持久化插件,通过刷新页面验证用户信息仍保留。 - 能够将 Pinia 初始化与插件注册迁移到
stores/index.ts,使用export * from './modules/user'统一导出仓库函数,并验证组件通过短路径导入后登录、退出功能正常。 - 能够创建 axios 实例配置
baseURL、timeout,在请求拦截器中安全携带 token,并封装request<T>(url, method, submitData?),根据请求方式将数据放入params或data,通过浏览器验证请求头、查询参数和请求体正确。 - 能够实现响应拦截器:当
code !== 10000时使用 Toast 提示并返回失败 Promise,成功时返回res.data摘取核心响应数据;能够处理 401,清空用户仓库并跳转登录页且携带returnUrl当前完整路由。 - 能够为
request函数增加泛型并定义Data<T>类型别名,将泛型传入axios.request第二个类型参数,使登录接口响应获得code、message、data: User的准确类型提示,并能说明使用第二个泛型参数的原因。
知识点
先定用户类型:对照接口文档,而不是凭感觉写
为什么需要:仓库里存的是用户信息,如果不先定义类型,ref 里放什么、setUser 收什么、页面读什么都会变成 any,后面接接口时很容易字段对不上。
怎么想:类型必须以后台返回为准。转写稿里先看“密码登录”接口的成功响应,再决定字段;不要自己发明字段。
怎么做:新建 src/types/user.d.ts,导出 User 类型。按转写稿模拟用到的字段,至少包含 id、avatar、token、mobile、account:
ts
export interface User {
id: number
avatar: string
token: string
mobile: string
account: string
}易错点:接口文档能“生成代码”,但生成类型有时和项目实际需要不完全一致;转写稿建议直接采用笔记里整理好的类型。字段名、可选性要以接口为准,不能把 token 写成 taken。
用 defineStore 建用户仓库:状态、设置、删除三件事
为什么需要:登录成功要保存用户状态,退出要清空用户状态;如果散落在组件里,后续很多页面都要重复写。
怎么想:用户仓库只做三件事:定义 user 状态、提供 setUser、提供 delUser,最后把使用仓库的函数导出。
怎么做:在 src/stores/user.ts 中写:
ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { User } from '@/types/user'
export const useUserStore = defineStore('cp-user', () => {
const user = ref<User>()
const setUser = (u: User) => {
user.value = u
}
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
})易错点:ref<User>() 初始值可以是 undefined,删除时用 user.value = undefined;不要写成 onDef、ondef,正确是 undefined。仓库 id 要唯一,转写稿用的是 cp-user 这类标识。
在 App.vue 做最小验证:先证明仓库能存能清
为什么需要:业务页面还没正式接,先用两个按钮验证仓库,能把问题挡在早期。
怎么想:一个按钮模拟登录,调用 setUser 塞一条模拟数据;一个按钮模拟退出,调用 delUser;页面直接打印 store.user。
怎么做:
vue
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const login = () => {
userStore.setUser({
id: 1,
avatar: '1',
token: '1',
mobile: '1',
account: '1'
})
}
const logout = () => {
userStore.delUser()
}
</script>
<template>
<button @click="login">登录</button>
<button @click="logout">退出</button>
<p>{{ userStore.user }}</p>
</template>易错点:转写稿调试时先看“点击登录后页面是否打印出用户信息”,再点退出看是否清空。若一开始打印为空是正常的,因为初始值是 undefined。
持久化:安装 pinia-plugin-persistedstate 并开启 persist
为什么需要:用户信息通常要留在本地,刷新页面不能丢;手写 localStorage 读写很麻烦,插件可以自动完成。
怎么想:先安装插件,再在 main.ts 里让 Pinia 实例 use 插件,最后在需要持久化的仓库第三个参数里开启 persist: true。
怎么做:安装:
bash
pnpm install pinia-plugin-persistedstatemain.ts:
ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)仓库里加第三个参数:
ts
export const useUserStore = defineStore(
'cp-user',
() => {
const user = ref<User>()
const setUser = (u: User) => { user.value = u }
const delUser = () => { user.value = undefined }
return { user, setUser, delUser }
},
{
persist: true
}
)易错点:插件要在 createPinia() 之后 pinia.use(...);persist 写在 defineStore 第三个参数,不是写在 ref 上。验证时点登录后刷新页面,数据还在;打开应用本地存储能看到 cp-user 对应数据。
调试顺序与提交
转写稿的调试过程很明确:先点“登录”,确认页面打印出用户信息;再点“退出”,确认数据清空;开启持久化后,再点登录并刷新,确认数据仍在。最后提交代码,提交信息可用 feature: 用户仓库和持久化。这样这一节的目标就完成了:类型有出处、仓库能读写、页面能验证、刷新不丢数据。
统一管理要解决什么问题
到这一节,用户仓库和持久化已经能跑通,但代码组织上还有两个明显痛点。
第一,main.ts 里混着 Pinia 的初始化代码:创建 Pinia 实例、导入持久化插件、调用 pinia.use(persist)。这些都属于状态管理基础设施,却和应用的启动装配混在一起,导致 main.ts 职责不单一。第二,仓库的导出方式是“每个仓库模块各自导出 useXxxStore”,使用时要写很长的路径,例如从 stores/modules/user 导入。现在只有一个用户仓库还不明显,等后续咨询、订单等仓库多起来,路径会越来越分散,不利于维护。
所以“统一管理”要做的不是改业务逻辑,而是做两件事:一是把 Pinia 相关代码独立维护到 stores/index.ts;二是让仓库的使用函数统一由 stores/index.ts 导出。这样 main.ts 只负责装配,业务组件统一从 @/store 拿仓库函数。
易错点:stores/index.ts 只做基础设施和统一导出,不要把用户状态、登录逻辑塞进去,否则只是把混乱换了个位置。
把 Pinia 初始化迁移到 stores/index.ts
先在 stores 下新建 index.ts,把原来 main.ts 中创建 Pinia、使用持久化插件的代码挪过来:
ts
// stores/index.ts
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persist)
export default pinia然后修改 main.ts,只保留“导入 Pinia 实例并挂载”:
ts
// main.ts
import pinia from './stores'
app.use(pinia)这里的推导是:Pinia 实例是全局唯一的,创建实例和安装插件应该集中在一个模块里;main.ts 只消费这个实例。转写稿中调试时回到浏览器,先点退出,再点登录,功能仍然正常,说明迁移没有破坏原有逻辑。
易错点:不要在 main.ts 里再次 createPinia(),否则可能出现两个实例;插件要在创建实例后、挂载应用前 use;导入路径要确认是 ./stores 而不是旧文件。
用 modules 目录收纳仓库
接下来把 user.ts 放到 stores/modules/user.ts。这样每个仓库就是一个模块,后续新增仓库也放进 modules,目录语义更清晰。
ts
// stores/modules/user.ts
export const useUserStore = defineStore('cp-user', () => {
// 用户状态、setUser、delUser 等逻辑仍在这里
})移动文件后,原来 App.vue 里的导入路径会变长。转写稿中先观察到路径变成 modules/user,再用统一导出解决。易错点:移动文件后要检查相对路径和别名路径;不要只改一处导入,仓库内部若有互相引用也要同步检查。
在 stores/index.ts 统一导出仓库
在 stores/index.ts 中把用户仓库导出。可以先写成显式导入导出:
ts
import { useUserStore } from './modules/user'
export { useUserStore }更简洁的写法是:
ts
// stores/index.ts
export * from './modules/user'这样业务组件统一从 store 入口导入:
ts
// App.vue
import { useUserStore } from '@/store'为什么可行:export * 会把 modules/user.ts 中的命名导出再次导出,使用方不再关心仓库放在哪个子目录。易错点:export * 要求被导出模块使用命名导出;如果多个模块有同名导出会冲突,需要显式重命名;路径不要写成 ./module/user。
统一管理后的验证与边界
改完后回到浏览器刷新,点击退出看用户信息是否清空,点击登录看用户信息是否写入,同时确认本地持久化仍然存在。转写稿中的验证结果就是退出、登录都正常,说明 Pinia 独立维护和统一导出都生效。
最后明确边界:stores/index.ts 负责 Pinia 实例、插件安装和仓库汇总导出;main.ts 负责应用装配;modules/user.ts 负责用户仓库业务。这样后续仓库增多时,使用方始终从统一入口导入,维护成本更低。
先明确请求工具要解决什么问题
这一节要做的不是“随便装一个 axios”,而是把项目里所有接口请求的公共逻辑收口到 request.ts:统一基准地址、统一超时时间、统一带 token、统一处理业务失败、统一摘取核心响应数据、统一处理 401,并在此之上封装一个更简单的 request 函数。这样做的原因是:如果每个页面都直接写 axios.get、axios.post,token 拼接、错误提示、401 跳转、返回数据解包都会重复,后期改一处要改很多地方。怎么想:先配置 axios 实例,再写拦截器,最后封装函数和类型。怎么做:安装 axios,在 src/utils/request.ts 中维护实例与拦截器。易错点:不要一开始就把所有功能混在一起,转写稿的调试顺序是“配一点、测一点”,先验证基准地址、超时和 token,再验证业务失败与 401。
创建 axios 实例:baseURL 与 timeout
为什么需要实例:项目接口通常有统一域名,如果每个请求都写完整地址,既长又容易写错。怎么想:从接口文档里找“域名 + 接口路径”的结构,前面公共部分就是基准地址;超时时间按项目约定设置。怎么做:安装依赖后创建实例,核心代码形态如下,具体基准地址从接口文档复制,不要手写猜:
ts
import axios from 'axios'
const instance = axios.create({
baseURL: '从接口文档复制的基准地址',
timeout: 10000
})
export default instance易错点:baseURL 后面不要再重复拼接口文档里的公共域名;timeout 单位是毫秒,转写稿中设置的是 10 秒。调试时先发一个请求,看请求地址是否自动带上了基准地址。
请求拦截器:携带 token
为什么需要:登录后后台通过 token 识别用户,后续接口要在请求头里带 Authorization。怎么想:token 存在用户仓库里,请求发出前从仓库取;有 token 才拼 Bearer 并写入请求头。怎么做:在请求拦截器里取 userStore 的 token,并判断 config.headers 是否存在:
ts
instance.interceptors.request.use((config) => {
const userStore = useUserStore()
const token = userStore.user?.token
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})易错点:config.headers 在类型上可能是 undefined,不能只用可选链去“赋值”,否则 TS 会提示对象可能未定义;要先判断再设置。转写稿的调试方法是点“获取用户信息”,在浏览器 Network 的请求标头里看有没有 Authorization: Bearer 1,有就说明携带成功。
响应成功:处理业务失败并摘取核心数据
为什么需要:HTTP 状态码 200 只代表请求到达后台,不代表业务成功。转写稿约定后台 code === 10000 为正常,其他 code 是业务失败,例如登录密码错误。怎么想:响应成功回调里先判断业务 code;失败时轻提示后台 message,并返回失败的 Promise,让调用处走 catch;成功时直接返回 res.data,把 axios 外层对象摘掉。怎么做:
ts
instance.interceptors.response.use(
(res) => {
if (res.data.code !== 10000) {
showToast(res.data.message || '业务失败')
return Promise.reject(res.data)
}
return res.data
},
(error) => {
// 401 处理见下一段
return Promise.reject(error)
}
)易错点:业务失败要 Promise.reject(res.data),这样 catch 里拿到的是后台数据对象,可以继续取 code 做不同处理;成功时返回 res.data 后,调用处拿到的就不再是 axios 的完整响应对象。转写稿中先用错误密码测出轻提示和 catch,再把密码改对,验证成功时打印的是后台核心数据。
响应失败:401 清信息并带 returnURL 跳登录
为什么需要:token 失效时后台返回 401,如果还停留在原页面,用户会不断看到失败。怎么想:401 时清空用户仓库和本地持久化信息,再跳登录页;为了登录后能回到原页面,跳转时携带当前完整路由地址。怎么做:给错误参数加 AxiosError 类型,判断 error.response?.status === 401,调用 store.deleteUser(),再用 router.push:
ts
if (error.response?.status === 401) {
const store = useUserStore()
store.deleteUser()
router.push({
path: '/login',
query: {
returnURL: router.currentRoute.value.fullPath
}
})
}易错点:router.currentRoute 是 ref,要取 .value.fullPath;fullPath 才包含 query 和 hash,path 不够。转写稿中模拟访问带参数的档案页后触发 401,验证本地信息被清空、跳登录页且 returnURL 带上了原地址和参数。
封装 request 函数:简化传参
为什么需要:直接调 axios 要传配置对象,get 要写 params,post 要写 data,调用处不统一。怎么想:封装成 request(url, method, submitData) 三参,默认 GET;函数内部根据 method 决定放 params 还是 data。怎么做:
ts
import type { Method } from 'axios'
export const request = <T = any>(
url: string,
method: Method = 'GET',
submitData?: object
) => {
return instance.request<any, Data<T>>({
url,
method,
[method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
})
}易错点:method 可能大小写不一致,判断前统一 toUpperCase();GET 用 params,POST/PUT/PATCH 用 data;submitData 可选,因为有的接口不传提交数据。调试时分别用登录接口和获取用户信息接口验证 data 与 params 是否放对。
用泛型约束响应数据类型
为什么需要:不同接口返回的 data 结构不同,不设类型时 res.data 提示不准确。怎么想:让 request 支持泛型,把调用处传入的业务数据类型传给 axios 的第二个泛型参数。转写稿中定义类型别名:
ts
type Data<T> = {
code: number
message: string
data: T
}调用时写 request<User>('接口路径', 'POST', { mobile, password }),这样返回值里的 data 就是 User。易错点:axios 第一个泛型不是这里要设置的自定义响应数据;因为响应拦截器已经返回了 res.data,所以要用 instance.request<any, Data<T>> 的第二个泛型。类型设置好后,鼠标悬停能看到 code、message、data,再点 data 能看到 User 的字段,说明类型链路打通。
代码示例
一、用户仓库 + 持久化 + 统一导出(Pinia)
ts
// src/types/user.ts
// 参考接口文档「密码登录」返回的用户信息定义
export type User = {
id: string
account: string
avatar: string
token: string
mobile: string
}ts
// src/stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { User } from '@/types/user'
// 参数1:仓库唯一标识 参数2:setup 函数 参数3:配置对象(开启持久化)
export const useUserStore = defineStore(
'cp-user',
() => {
// 1. 用户信息状态
const user = ref<User>()
// 2. 设置用户信息(登录成功后调用)
const setUser = (u: User) => {
user.value = u
}
// 3. 删除用户信息(退出登录 / token 失效时调用)
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
},
{
persist: true // 开启本地持久化,自动写入 localStorage
}
)ts
// src/stores/index.ts
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
// 注意:必须在 createPinia 之后 use 插件
pinia.use(persist)
export default pinia
// 统一导出:将来所有仓库都从 '@/stores' 导入
export * from './modules/user'ts
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
createApp(App).use(pinia).use(router).mount('#app')关键行与易错点
defineStore的第三个参数{ persist: true }才是持久化开关,写在前两个参数里无效。pinia.use(persist)必须在createPinia()之后执行,且要在app.use(pinia)之前完成,否则插件不生效。- 仓库文件要放在
stores/modules/下,index.ts用export * from './modules/user'统一导出,业务里统一写import { useUserStore } from '@/stores',避免'@/stores/modules/user'这类长路径。 ref<User>()不传初值时类型是User | undefined,delUser里赋undefined才不报错;如果写成ref<User>({} as User)就清不干净。- 验证方式:模拟登录后刷新页面,用户信息仍在;本地存储里能看到键名
cp-user(即 store id)。
二、axios 配置 + 响应拦截 + request 函数封装(src/utils/request.ts)
ts
// src/utils/request.ts
import axios, { type Method } from 'axios'
import type { AxiosError } from 'axios'
import { showToast } from 'vant'
import router from '@/router'
import { useUserStore } from '@/stores'
// 1. 创建实例:基准地址 + 超时时间(基准地址以接口文档为准)
const instance = axios.create({
baseURL: 'https://consult-api.itheima.net/',
timeout: 10000
})
// 2. 请求拦截器:携带 token
instance.interceptors.request.use(
(config) => {
const store = useUserStore()
// 类型守卫:headers 可能为 undefined,不能直接赋值
if (store.user?.token && config.headers) {
config.headers.Authorization = `Bearer ${store.user.token}`
}
return config
},
(err) => Promise.reject(err)
)
// 3. 响应拦截器
instance.interceptors.response.use(
(res) => {
// 业务失败:HTTP 200 但 code !== 10000
if (res.data.code !== 10000) {
showToast(res.data.message || '业务失败')
// 返回失败的 promise,后续走 catch,并带上 code 供外部区分处理
return Promise.reject(res.data)
}
// 摘取核心响应数据
return res.data
},
(err: AxiosError) => {
// 401:token 失效
if (err.response?.status === 401) {
const store = useUserStore()
store.delUser() // 清仓库 = 同时清本地存储
router.push({
path: '/login',
// 携带当前页面完整地址(含参数),登录成功后可回跳
query: { returnUrl: router.currentRoute.value.fullPath }
})
}
return Promise.reject(err)
}
)
// 4. 响应数据统一结构
type Data<T> = {
code: number
message: string
data: T
}
// 5. 请求函数封装:url / method / 提交数据
export const request = <T>(url: string, method: Method = 'GET', submitData?: object) => {
return instance.request<any, Data<T>>({
url,
method,
// 动态属性名:GET 走 params,其余走 data
[method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData
})
}关键行与易错点
config.headers.Authorization = ...前必须判断config.headers存在,TS 里headers是可选类型,直接用可选链赋值会报错(可选链只能读不能写)。- 业务失败
return Promise.reject(res.data)才能让调用方走catch,并能在catch里拿到code做差异化处理;直接throw或只弹提示不 reject,外部会误以为成功。 - 成功分支
return res.data是「摘取核心数据」的关键:拦截器返回什么,await request()拿到的就是什么。 - 401 判断要用
err.response?.status(err需标注AxiosError才有提示),不是err.status。 returnUrl用router.currentRoute.value.fullPath(含 query/hash),不要用path,否则参数丢失,登录后回跳会丢条件。request的泛型必须写在instance.request的第二个泛型位(自定义响应数据类型),第一个位写any;写第一个位拿到的是 axios 包装后的完整响应对象。- 动态属性名
[条件 ? 'params' : 'data']要配合method.toUpperCase(),否则传小写get会误放进data。
三、在组件中使用(模拟登录 / 退出 / 调接口)
vue
<!-- src/App.vue -->
<script setup lang="ts">
import { request } from '@/utils/request'
import { useUserStore } from '@/stores'
import type { User } from '@/types/user'
const store = useUserStore()
// 模拟登录:写入仓库(会被持久化)
const onLogin = () => {
store.setUser({
id: '1',
account: '1',
avatar: '1',
token: '1',
mobile: '1'
})
}
// 模拟退出:清空仓库与本地存储
const onLogout = () => {
store.delUser()
}
// 真实调用接口:返回类型即 User
const getProfile = async () => {
const res = await request<User>('/patient/myUser', 'GET', { test: 1 })
console.log(res.code, res.message, res.data.token)
}
// 真实登录(密码故意写错,验证业务失败处理)
const onRealLogin = async () => {
try {
const res = await request<User>('/patient/login', 'POST', {
mobile: '13212345678',
password: 'abc12345'
})
console.log('登录成功', res.data)
} catch (err) {
console.log('登录失败', err) // 能拿到 { code, message }
}
}
</script>
<template>
<div>当前用户:{{ store.user }}</div>
<van-button type="primary" @click="onLogin">模拟登录</van-button>
<van-button type="danger" @click="onLogout">模拟退出</van-button>
<van-button @click="getProfile">获取用户信息</van-button>
<van-button @click="onRealLogin">接口登录</van-button>
</template>关键行与易错点
request<User>(...)传进去的User会经request的泛型T→Data<T>→instance.request第二泛型,最终res.data就是User类型,点.token、.mobile都有提示。- 调用 GET 时第三个参数是查询参数(进
params),POST 时是请求体(进data),不用再手写{ url, method, params }配置对象。 - 业务失败时不会进
try的成功分支,而是在catch里拿到{ code, message },可据此做不同错误提示。 - 验证 token 是否带上:打开浏览器 Network 面板,看请求头是否有
Authorization: Bearer xxx;验证持久化:登录后刷新页面,store.user仍在。
常见错误
刷新页面后用户信息丢失
- 现象:登录后状态正常,F5 刷新后
user变回undefined。 - 原因:只在 store 里定义了状态,没有开启持久化;或插件装了但没在 pinia 实例上
use。 - 修法:在创建 pinia 实例后
pinia.use(piniaPluginPersistedstate),并在defineStore第三个参数写{ persist: true };两处缺一不可。
- 现象:登录后状态正常,F5 刷新后
请求拦截器给
config.headers.Authorization赋值时报“对象可能为 undefined”- 现象:TS 红线,提示
headers可能为undefined;用config.headers?.Authorization = ...后报“左侧不能是可选属性”。 - 原因:
headers类型允许undefined,可选链只能读不能写。 - 修法:先做类型守卫
if (config.headers) { config.headers.Authorization = ... },再赋值。
- 现象:TS 红线,提示
业务失败判断写了但页面还是走
then- 现象:
code !== 10000时 toast 弹了,但调用处仍进入then。 - 原因:只弹提示没有
return Promise.reject(...),或 reject 写在return res.data之后成了死代码。 - 修法:业务失败分支内先 toast,再
return Promise.reject(res.data),成功分支最后才return res.data。
- 现象:
catch里拿不到后端返回的code- 现象:
catch(err)打印出来是字符串或undefined,无法按错误码分支处理。 - 原因:
Promise.reject传的是res.data.message而不是整个数据对象。 - 修法:
reject(res.data),调用处用err.code判断;同时约定好 reject 的数据形状。
- 现象:
401 跳登录后无法回到原页面
- 现象:登录成功一律回首页,用户之前所在页面和查询参数丢失。
- 原因:跳转时只写了
router.push('/login'),没带returnURL;或用了route.path而非fullPath,query 丢失。 - 修法:
router.push({ path: '/login', query: { returnURL: router.currentRoute.value.fullPath } }),登录成功后按该参数回跳。
封装的
request发 GET 时后端收不到参数- 现象:GET 请求参数为空,POST 正常。
- 原因:统一把提交数据放进了
data,GET 应放params。 - 修法:用动态键
[method.toUpperCase() === 'GET' ? 'params' : 'data']: submitData,并给method默认值'GET'。
泛型标了但
res.data没有类型提示- 现象:调用
request<User>后res仍是AxiosResponse结构,点不出业务字段。 - 原因:泛型传给了
axios.request的第一个类型参数(响应整体类型),而拦截器已返回res.data。 - 修法:第一个参数写
any,业务类型放第二个:instance.request<any, Data<T>>(config),其中Data<T>统一收敛code/message/data。
- 现象:调用
仓库多了以后 import 路径到处改
- 现象:模块挪进
modules目录后,各页面引用路径全部报错。 - 原因:每个页面直接引具体 store 文件,没有统一出口。
- 修法:
stores/index.ts里export * from './modules/user',页面统一从@/stores引入。
- 现象:模块挪进
练习
在
userstore 中补充一个token的 getter,并验证:登录 → 刷新页面 →token仍在;退出登录 → 刷新 → 本地存储中cp-user已被清除。改造请求拦截器:当仓库中
token为空时,不添加Authorization头。用浏览器 Network 面板分别验证“有 token”和“无 token”两种请求的请求头差异。故意用错误密码调用登录接口,验证:① 页面出现 toast 提示后端
message;② 调用处进入catch;③catch中能读到err.code并据其打印不同文案。手动把本地 token 改成无效值,在带 query 参数的页面触发 401,验证:本地用户信息被清空、跳转到
/login且returnURL为含参数的完整地址;再模拟登录成功后按returnURL回跳。给“获取用户信息”接口调用处加上泛型
User,验证res.data能点出id / mobile / token / account等字段;再把request中的响应结构抽成类型别名,确认调用处无需重复声明code/message。
课件图示






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 基础架构-用户仓库和持久化 | 13:17 |
| 2 | 基础架构-统一管理 | 09:16 |
| 3 | 请求工具-axios配置 | 09:35 |
| 4 | 请求工具-业务失败处理和摘取核心响应数据 | 10:21 |
| 5 | 请求工具-401处理 | 09:28 |
| 6 | 请求工具-工具函数 | 08:44 |
| 7 | 请求工具-类型设置 | 11:14 |
小结
- 用户仓库的三件套是“状态 + 设置函数 + 删除函数”,类型定义必须以接口文档返回结构为准,不要凭感觉写。
- 持久化是“插件注册 + store 配置”两步,任何一步漏掉都表现为“刷新即丢”,排查时先看这两处。
- 基础架构的收口原则:pinia 初始化归
stores/index.ts,store 归modules,对外只暴露统一出口,页面不感知内部目录。 - 请求拦截器负责“带凭证”,响应拦截器负责“判业务、摘数据、处理 401”,职责不要混在页面里。
- 业务失败必须
reject且携带结构化数据,否则调用方无法按code做差异化处理;HTTP 200 不等于业务成功。 - 401 的关键不是“跳登录”,而是“带着
fullPath跳登录”,这是登录后可回跳的前提。 - 请求函数封装的核心价值是“三参调用 + 按 method 自动分配 params/data”,让调用处不再拼配置对象。
- 泛型要落在
axios.request的第二个类型参数上,并把code/message/data抽成公共类型别名,业务侧只传data的类型。