主题
Git 分支与远程仓库(二)
本页对应 Git 分支与远程仓库,共 6 段视频,约 49 分钟。
学习目标
- 能够抽取并调用
checkToken校验函数,判断本地存储中是否存在 token;不存在时提示“请先登录”并跳转登录页,存在时允许访问首页。 - 能够登录成功后把 token 与用户名写入
localStorage,并调用公共渲染函数完成用户名回显;能够实现退出登录:清除用户缓存、提示退出成功并跳转登录页。 - 能够使用 axios 发送携带
Authorization请求头的 GET 请求获取首页统计数据,并通过async/await与try/catch正确获取响应数据。 - 能够封装
getData与renderOverview函数,在获取数据后按“数据键与页面类名一致”的方式渲染概览区域,并通过页面效果验证渲染结果。 - 能够识别接口返回
401的 token 失效场景,在catch中判断err.response.status === 401,提示登录过期、清除本地存储并延迟跳转登录页。 - 能够使用 axios 请求拦截器统一添加 token,使用响应拦截器统一处理
401失效并剥离响应数据,使业务代码无需重复拼接请求头和处理响应层级。
知识点
先看需求与前置条件
首页调用统计数据接口时,需要把本地存储里的 token 放进请求头。token 有效就正常返回数据;token 无效时,接口会返回 401,页面不能继续装作已登录,而要提示用户、清除缓存并回到登录页。

前置逻辑是:登录成功后本地存储里已有 username 和 token;首页的 getData 通过 headers.Authorization 携带 token。如果完全不携带,接口会直接报错。转写稿中反复提到“头ken/偷看”,这里统一纠正为 token;“exos”纠正为 axios;“hader/auotherization”纠正为 header/Authorization。
知识点一:用401判断token失效
为什么需要:token 不是永久有效,转写稿说明本案例默认有效时间约 2 小时;它也可能被篡改。前端不能只看“本地有没有 token”,还要处理接口返回的失效结果。
怎么想:接口文档中身份验证失败会返回状态码 401。因此请求失败后,先看错误对象里的 error.response.status 是否等于 401。注意 401 是数字,不要写成字符串。
怎么做:把 await axios(...) 放进 try,失败走 catch。转写稿中调试时用 console.dir(error) 展开错误对象,看到 error.response.status 为 401,说明判断入口正确。代码中保留:
javascript
// 首页-统计数据
async function getData() {
const token = localStorage.getItem('token')
try {
// 调用接口(登录成功之后才可以调用)
const res = await axios({
url: '/dashboard',
// 请求头中携带token
// 不携带token,直接报错
headers: {
Authorization: token
}
})
const overview = res.data.data.overview
// 渲染数据
Object.keys(overview).forEach(key => {
document.querySelector(`.${key}`).innerText = overview[key]
})
} catch (error) {
// 首页-登录状态过期
// 判断token失效(状态码401):token过期,token被篡改
if (error.response.status === 401) {
// 删除缓存并提示用户
localStorage.removeItem('username')
localStorage.removeItem('token')
// 使用普通用户可以理解的方式提示他们
showToast('请重新登录')
// 返回登录页
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
}
}
getData()易错点:await 的错误必须用 try/catch 接住;不要只打印 error 不判断 response;不要把 401 写成 '401';不要直接给用户提示“token验证失败”,用户不理解,提示“请重新登录”即可。
知识点二:删除缓存并提示用户
为什么需要:失效的 token 和旧用户名如果还留在本地,用户刷新页面会反复触发无效请求,状态也不干净。
怎么想:先清除登录痕迹,再提示用户。转写稿中强调“清空本地存储”,笔记代码明确删除 username 和 token 两项。
怎么做:使用 localStorage.removeItem('username') 和 localStorage.removeItem('token'),再调用项目已有的 showToast 提示。
易错点:只删 token 不删 username,页面可能还显示旧用户名;删除的键名要和登录页保存时一致,转写稿里也提醒“忘了就去 login.js 看当时怎么存的”。
知识点三:返回登录页与延时
为什么需要:提示刚出现就立刻跳转,用户看不清;不跳转又会停留在失效页面。
怎么想:先提示,给一个短暂停留,再跳转。笔记使用 setTimeout,延时 1500 毫秒后执行 location.href = 'login.html'。
怎么做:在 401 分支里先清缓存、提示,再延时跳转。转写稿调试时出现“提示没出来”,原因是忘记加延时,补上 setTimeout 后恢复正常。
易错点:不要清完缓存后忘记跳转;跳转地址要写当前项目中的 login.html;setTimeout 里要用函数包住跳转语句。
知识点四:模拟token失效并调试
为什么需要:真实等 2 小时不现实,需要主动制造失效场景验证代码。
怎么想:转写稿给出两种原因:过期或被篡改。模拟篡改最直接:在浏览器应用面板里把 localStorage 中的 token 删掉一部分,刷新首页,getData 会再次请求并返回 401。
怎么做:打开控制台查看请求与错误对象,确认进入 catch 且 error.response.status === 401;观察是否提示、缓存是否清除、1.5 秒后是否回到登录页。调试完可提交:
bash
git add .
git commit -m"首页-登录状态失效"易错点:模拟后要刷新页面才会重新触发 getData;不要把 token 改成一个仍然合法的值;测试时确认首页确实调用了 getData(),否则整个失效处理不会执行。
拦截器解决的是“重复的公共动作”
这一节的核心不是背 API,而是先理解拦截器为什么存在。笔记给它的作用是:请求发送之前、响应回来之后执行一些公共的逻辑。结合案例看就很清楚:登录成功后,本地存了 token;后面访问首页、获取统计数据时,每个请求都要把 token 放进请求头 Authorization;而 token 只有两小时有效,失效后接口返回 401,每个请求又都要判断“登录过期、清缓存、提示、回登录页”。如果每个接口都手写一遍,代码会又长又容易漏。
所以思路是:把“请求前统一加 token”和“响应后统一判断失效”从业务代码里抽出来,放到公共层。注册之后,再调用接口,执行顺序就是:
- 注册拦截器之后,调用接口
- 请求发送时 → 执行请求拦截器 → 服务器
- 服务器响应内容 → 执行响应拦截器 → 接收数据

请求前:统一把 token 放进请求头
转写稿里先做了登录验证:登录成功后保存用户信息和 token,首页再校验 token 是否存在。真正发请求时,代码是这样取 token 的:
js
const data = JSON.parse(localStorage.getItem('USER_MSG') || '{}')
const { token } = data
axios.get(url, {
headers: {
Authorization: token
}
})这里要注意几个易错点。第一,本地存储的键名是 USER_MSG,不是 USERNAME,转写稿里多次口误成“user message”,写代码时要统一。第二,JSON.parse 遇到空值会报错,所以用 || '{}' 兜底。第三,请求头字段是 headers,里面键名是 Authorization,不是 auotherization。第四,登录接口本身通常不应该带旧 token,实际项目里要放行;这一点转写稿没有展开,不能想当然把所有请求一刀切。
如果每个业务请求都写一遍 headers: { Authorization: token },就是拦截器要解决的重复。请求拦截器就是在请求发出前做这件事:取本地 token,判断存在再塞进请求头。调试时看 Network 的请求标头,能看到 authorization,就说明加进去了;没带时接口会报“没有携带 token”。
响应后:统一判断 401 并清理登录态
转写稿后半段模拟了 token 失效:token 可能过期,也可能被篡改。接口返回 401,控制台错误对象里能看到 err.response.status === 401。业务层当时的写法是:
js
try {
const res = await getData()
// 渲染数据
} catch (err) {
if (err.response && err.response.status === 401) {
Toast('登录过期,请重新登录')
localStorage.removeItem('USER_MSG')
setTimeout(() => {
location.href = './login.html'
}, 1500)
}
}这段代码本身能跑,但它是“每个请求都可能要写”的逻辑。响应拦截器就是把这套判断收口:响应回来先看状态,成功就放行数据;失败且是 401,就提示、清缓存、跳登录页。易错点有四个:await 不能直接接错误,必须 try/catch;err.response 可能不存在,要先判断;401 是数字不是字符串;提示语不要直接说“token 验证失败”,用户看不懂,应该说“登录过期,请重新登录”。转写稿里还出现过忘记加 setTimeout 延迟,导致提示一闪而过,这也是调试时要盯的细节。
注册位置与调用顺序
拦截器不是业务函数,注册一次即可,不要在每个页面重复注册。按 axios 文档,通常在创建实例后注册请求拦截器和响应拦截器;具体 API 以文档为准。注册后调用接口,才会按图里的顺序走:请求拦截器 → 服务器 → 响应拦截器 → 业务拿到数据。
易错点是响应拦截器里返回什么,业务层就要按什么接。如果拦截器返回了 response.data,业务层就不能再写 res.data.data;如果返回完整 response,就保持原样。团队里必须统一,否则会出现“明明有数据却取不到”的问题。
从调试过程看拦截器的价值
转写稿的调试路径很典型:登录后看 localStorage 有没有 token;首页校验 token;发请求时看 Network 请求头有没有 Authorization;删掉 token 或改坏 token 后刷新,观察是否返回 401;再看控制台错误对象里的 status。这些动作如果散落在每个接口里,排查成本很高。抽到拦截器后,请求头统一加、401 统一处理,业务函数只关心“拿数据、渲染页面”。
最后记住:拦截器适合做公共逻辑,不适合塞具体业务渲染。请求拦截器负责“发之前补公共信息”,响应拦截器负责“回来之后做公共判断”,业务层保留自己的 try/catch 兜底和页面渲染,这样结构才清楚。
需求:为什么用请求拦截器统一带 token
为什么需要:登录后很多接口都要携带 token。若在每个请求里手写 headers: { Authorization: token },代码重复,容易漏写;接口一多,维护成本高。
怎么想:axios 的请求拦截器会在每次请求发出前执行,可以在这里统一读取 token 并写入请求头。这样业务代码只关心接口地址和参数。
怎么做:在公共 commons.js 中添加请求拦截器,读取本地存储中的 token,存在才设置 config.headers['Authorization']。
易错点:不要无条件携带空 token;键名要与登录页保存时一致;拦截器必须 return config,否则请求配置会丢失。
请求拦截器基本写法
为什么需要:先掌握 axios 请求拦截器的固定结构,再填业务逻辑。
怎么想:axios.interceptors.request.use 接收成功和失败两个回调。成功回调拿到 config,可在发送前修改请求配置;失败回调返回 Promise.reject(error)。
怎么做:保留基本写法,把“统一设置 token”放在成功回调里。
易错点:失败回调不是处理 401 的地方;401 通常在后端返回响应后,由请求的 catch 处理。
javascript
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么,比如: 统一设置token
// 通过config可以获取请求的设置,比如headers可以获取(修改)请求头
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});核心步骤:统一设置 token 并移除首页单独逻辑
为什么需要:登录成功后 token 已存入本地存储,后续接口不应再逐个设置。
怎么想:在拦截器里读 token,存在就加到 Authorization 请求头;首页 getData 只保留 url,不再单独写 headers。
怎么做:在 commons.js 写入拦截器;在 index.js 删除首页请求中的单独携带逻辑。转写稿中调试时也验证过:手动在 axios 配置里写 headers 虽然能拿到数据,但每个接口都要写,后续图表等请求会重复,所以改为统一拦截器更合适。
易错点:config.headers 是对象,推荐 config.headers['Authorization'] = token;token 不存在时不要设置;修改后必须 return config。
javascript
// 添加请求拦截器
// 统一携带token
axios.interceptors.request.use(function (config) {
// 可以通过headers,查看+设置请求头
// config.headers['info'] = 'itheima666'
// 每次发送请求,都会执行这个回调函数
// console.log(config)
// 在发送请求之前做些什么,比如: 统一设置token
const token = localStorage.getItem('token')
// token存在,才携带
if (token) {
config.headers['Authorization'] = token
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});javascript
// 首页-统计数据
async function getData() {
try {
// 调用接口(登录成功之后才可以调用)
const res = await axios({
url: '/dashboard',
// 通过axios请求拦截器统一携带
})
const overview = res.data.data.overview
// 渲染数据
Object.keys(overview).forEach(key => {
document.querySelector(`.${key}`).innerText = overview[key]
})
} catch (error) {
// 首页-登录状态过期
// 判断token失效(状态码401):token过期,token被篡改
// console.dir(error)
if (error.response.status === 401) {
// 删除缓存并提示用户
localStorage.removeItem('username')
localStorage.removeItem('token')
// 使用普通用户可以理解的方式提示他们
showToast('请重新登录')
// 返回登录页
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
}
}调试与 401:token 过期或被篡改时怎么处理
为什么需要:token 不是永久有效。转写稿中说明案例里 token 有效时间为两小时,过期或被篡改后接口会返回 401,页面不能继续展示旧数据。
怎么想:请求失败会进入 catch。用 console.dir(error) 查看错误对象,展开 response 可看到 status 为 401。此时应提示用户重新登录,清除本地登录信息,并跳回登录页。
怎么做:在 catch 中判断 error.response.status === 401;删除 username 和 token;用 showToast('请重新登录') 提示;setTimeout 延迟 1500ms 后跳转 login.html。转写稿中模拟时删掉 token 的一部分字符串再刷新,控制台出现 401,说明校验生效。
易错点:401 判断要放在 catch 里,不要放进请求拦截器的 error 回调;error.response 可能不存在,实际项目可加保护;提示语不要直接说“token 验证失败”,用户看不懂,应说“请重新登录”。
提交记录
为什么需要:完成“请求拦截器统一设置 token”后提交,便于回退和追踪。
怎么做:执行下面命令。
易错点:提交前确认首页已移除单独携带 token 的旧逻辑,避免重复设置。
bash
git add .
git commit -m"axios请求拦截器-统一设置token"需求:为什么要把 token 失效处理抽出来
首页统计数据接口需要携带 token 才能访问。token 有有效期,转写稿中明确说本案例 token 有效期为两小时;过期或被篡改后,后端会返回 401。此时如果只在 getData 里写 try...catch,那么以后每个需要 token 的请求都要重复写“提示重新登录、清缓存、跳登录页”,代码会散落且容易漏。
所以本节的核心目标是:用 axios 响应拦截器统一处理 token 失效。核心步骤是:添加响应拦截器 → 统一处理 token 失效 → 移除首页对应逻辑。

响应拦截器基本写法:先理解两个回调
为什么需要:axios 的响应拦截器可以在请求返回后、业务代码拿到结果前统一做处理。成功进入第一个回调,非 2xx 进入第二个回调。
怎么想:token 失效属于响应错误,应放在错误回调里判断状态码;成功回调暂时只返回 response,不破坏原有数据。
怎么做:
javascript
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,比如: 数据剥离
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么: 比如统一处理token失效
return Promise.reject(error);
});易错点:错误回调最后要 return Promise.reject(error),否则错误链会被吞掉;不要把所有错误都当成 token 失效,必须先判断 error.response.status。
在 common.js 统一处理 401
为什么需要:common.js 是公共逻辑文件,多个页面都会引入,适合放统一的登录失效处理。
怎么想:转写稿中先通过打印错误对象确认结构:error.response.status 为 401,且是数字。因此判断 === 401。处理动作有三步:提示用户、删除本地缓存、延迟跳转登录页。
怎么做:
javascript
// 添加响应拦截器
// 统一处理token过期
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
return response;
}, function (error) {
// console.dir(error)
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么: 比如统一处理token失效
// 统一处理token失效
if (error.response.status === 401) {
// 弹框提示用户
showToast('请重新登录')
// 删除缓存
localStorage.removeItem('token')
localStorage.removeItem('username')
// 返回登录页
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
return Promise.reject(error);
});易错点:error.response 不一定存在,严谨写法可先判断 error.response && error.response.status === 401;401 是数字不要写成字符串;提示文案要让用户看懂,转写稿也强调不要直接提示“token 验证失败”,而应提示“登录过期,请重新登录”;清缓存要清 token 和用户名,避免回显旧信息。
首页 index.js 移除重复逻辑
为什么需要:统一处理后,首页不应再自己判断 401,否则会重复提示、重复跳转。
怎么想:转写稿中曾用 try...catch 在 getData 里捕获错误,验证 401 可行后,把这段逻辑抽到响应拦截器;首页只保留正常请求和渲染。
怎么做:
javascript
// 首页-统计数据
async function getData() {
// 调用接口(登录成功之后才可以调用)
const res = await axios({
url: '/dashboard',
// 通过axios请求拦截器统一携带
})
const overview = res.data.data.overview
// 渲染数据
Object.keys(overview).forEach(key => {
document.querySelector(`.${key}`).innerText = overview[key]
})
}易错点:移除首页逻辑时不要误删正常的数据渲染;请求头 token 的携带应由请求拦截器统一处理,不要在 getData 里再手写;跳转前保留 1.5 秒延迟,让提示能被用户看到。
调试过程:模拟 token 被篡改
转写稿中的调试方法是:登录后拿到 token,手动修改 localStorage 中的 token,删掉一部分字符串,再刷新首页。刷新会触发 getData 请求,后端返回 401,响应拦截器错误回调执行,控制台可看到 error.response.status 为 401。确认后补上 showToast 和 setTimeout,观察是否提示并回到登录页。这里要纠正同音字:token、localStorage、Authorization、headers、status、response、Promise.reject、showToast、setTimeout、location.href。
易错点:如果只删 token 不刷新,请求不会重新发出;如果提示没出现,检查是否忘了写延迟或提示函数;不要在前端判断 token 是否“有效”,前端只根据后端 401 做统一处理。
提交记录
完成本节后提交:
bash
git add .
git commit -m"axios响应拦截器-统一处理token失效"需求:页面少写一个 data
为什么需要:axios 默认返回的是完整响应对象,业务数据通常藏在 response.data 里;如果后端再包一层 { code, message, data },页面取值就会变成 res.data.data.xxx,层级多、容易写错。
怎么想:在响应拦截器里统一把 response.data 返回出去,让后续页面拿到的 res 直接就是“响应体”,从而少写一层 .data。
怎么做:先明确目标是对比“剥离前/后”的取值层级,再在 commons.js 的响应拦截器成功分支里返回 response.data。下图对比了数据剥离前后的使用差异:


易错点:不是把所有 .data 都删掉。剥离后 res 是响应体,业务数据仍在 res.data 中;例如首页取概览数据应写 res.data.overview,而不是 res.overview。错误处理里 error.response.data.message 也不能盲目删,因为失败请求不会走成功拦截器的 return response.data。
在 commons.js 中做数据剥离
为什么需要:登录、注册、首页等多个页面都要用响应数据,如果每个页面都手写一层 .data,代码重复且容易漏写。
怎么想:响应拦截器是 axios 的统一出口,成功响应统一“剥壳”,失败响应统一处理 token 过期。
怎么做:在 commons.js 中添加响应拦截器,成功时 return response.data;失败时判断 error.response.status === 401,提示重新登录、清缓存并跳回登录页。保留笔记代码如下:
javascript
// 添加响应拦截器
// 统一处理token过期
// 数据剥离
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,比如: 数据剥离
// 剥离data属性,页面中少写.data属性,直接可以获取到数据
return response.data;
}, function (error) {
// console.dir(error)
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么: 比如统一处理token失效
// 统一处理token失效
if (error.response.status === 401) {
// 弹框提示用户
showToast('请重新登录')
// 删除缓存
localStorage.removeItem('token')
localStorage.removeItem('username')
// 返回登录页
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
return Promise.reject(error);
});易错点:401 判断要放在失败回调里;调试时可用 console.dir(error) 查看 error.response.status。转写稿中模拟 token 被篡改后刷新页面,接口返回 401;如果提示没出现,要检查是否忘记加 setTimeout 延迟跳转,导致提示还没显示就跳走了。
首页数据使用逻辑调整
为什么需要:首页概览数据来自 /dashboard,剥离后页面取值层级减少,代码更简洁。
怎么想:响应拦截器已经把响应体返回给 res,所以业务数据从 res.data.overview 取。
怎么做:在 index.js 中移除多余的 .data,保留如下逻辑:
javascript
// 首页-统计数据
async function getData() {
// 调用接口(登录成功之后才可以调用)
const res = await axios({
url: '/dashboard',
// 通过axios请求拦截器统一携带
})
const overview = res.data.overview
// 渲染数据
Object.keys(overview).forEach(key => {
document.querySelector(`.${key}`).innerText = overview[key]
})
}易错点:res.data.overview 中的 .data 是后端业务数据字段,不是 axios 响应对象的 .data;不要把 overview 写成 res.overview。渲染时数据键与页面类名一致,才可以用 key 拼接选择器。
登录与注册的数据使用调整
为什么需要:登录、注册也要读取 message、token、username 等字段,统一剥离后页面取值更一致。
怎么想:成功时 res 已是响应体,所以 res.message、res.data.token 可直接使用;失败时仍从 error.response.data.message 取后端提示。
怎么做:在 register.js、login.js 的 try 中移除多余 .data,保留关键代码如下:
javascript
// register.js 关键片段
try {
const res = await axios.post('/register', { username, password })
showToast(res.message)
} catch (error) {
showToast(error.response.data.message)
}javascript
// login.js 关键片段
try {
const res = await axios.post('/login', { username, password })
showToast(res.message)
localStorage.setItem('token', res.data.token)
localStorage.setItem('username', res.data.username)
setTimeout(() => {
location.href = './index.html'
}, 1500)
} catch (error) {
showToast(error.response.data.message)
}易错点:成功分支和失败分支取值不同;catch 里不能写 res.message。登录成功后缓存 token 和 username,后续请求才能携带 token;转写稿中删除 token 后刷新首页会提示“请先登录”并跳回登录页,说明 token 校验和缓存清理要配套。
提交记录
为什么需要:完成一个完整功能后提交,便于回滚和追踪。
怎么做:保留笔记中的 git 记录:
bash
git add .
git commit -m"axios响应拦截器-数据剥离"易错点:提交前确认 commons.js、index.js、login.js、register.js 都已保存,避免只改了拦截器却忘记调整页面取值。
课件图示








代码示例
示例 1:commons.js —— 请求拦截器统一带 token + 响应拦截器统一处理 401 + 数据剥离
javascript
// commons.js —— 公共逻辑(所有页面都会引入)
// 1. 请求拦截器:统一设置 token
axios.interceptors.request.use(function (config) {
// 每次发送请求,都会执行这个回调函数
// 在发送请求之前做些什么,比如:统一设置 token
const token = localStorage.getItem('token')
// token 存在,才携带
if (token) {
config.headers['Authorization'] = token
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 2. 响应拦截器:数据剥离 + 统一处理 token 失效
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 剥离 data 属性,页面中少写 .data,直接可以获取到数据
return response.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 统一处理 token 失效
if (error.response.status === 401) {
// 弹框提示用户
showToast('请重新登录')
// 删除缓存
localStorage.removeItem('token')
localStorage.removeItem('username')
// 返回登录页
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
return Promise.reject(error);
});关键行
config.headers['Authorization'] = token:通过config修改请求头,一次注册,之后所有axios请求都会自动携带 token,页面里不用再单独写headers。if (token):必须先判断再赋值,未登录时不能带上undefined这种脏值。return response.data:把响应体中的data剥出来,页面里就少写一层.data。error.response.status === 401:401 是后端约定的“身份验证失败”状态码,token 过期或被篡改都会返回它。setTimeout(..., 1500):先让提示框显示一会儿再跳转,否则用户看不到提示。
易错点
- 401 分支里最后一定要
return Promise.reject(error),把错误继续抛给业务代码,否则调用处拿不到错误、无法catch。 - 响应拦截器成功回调改成
return response.data之后,所有页面的数据取值都要同步去掉一层.data(登录、注册、首页),漏改一处就会出现“明明请求成功却取不到值”。 - 两个拦截器都要写在
commons.js里,并且页面必须在自己的业务 JS 之前引入它,否则拦截器还没注册就已经发请求了。 - 清缓存要 token 和 username 一起清,只清 token 会导致页面顶部还残留旧用户名。
示例 2:index.js —— 首页统计数据(token 与 401 都交给拦截器处理)
javascript
// index.js —— 首页-统计数据
async function getData() {
try {
// 调用接口(登录成功之后才可以调用)
const res = await axios({
url: '/dashboard',
// 通过 axios 请求拦截器统一携带 token
})
const overview = res.data.overview
// 渲染数据:数据的键 与 页面元素的类名 一致
Object.keys(overview).forEach(key => {
document.querySelector(`.${key}`).innerText = overview[key]
})
} catch (error) {
// token 失效已在响应拦截器中统一处理,这里无需重复写
console.dir(error)
}
}
getData()关键行
axios({ url: '/dashboard' }):因为 token 交给请求拦截器统一携带,这里不再写headers,代码更干净。res.data.overview:res已经是剥离后的响应体,所以这里取res.data.overview;剥离前要写res.data.data.overview。Object.keys(overview).forEach(...)+ 模板字符串.${key}:利用“数据键名与页面类名一致”的约定,一次性渲染 salary、count、age、group 等所有数字,避免重复写querySelector。innerText赋值:把接口数据写进 DOM 文本节点。
易错点
- 数据剥离前后取值层级不同,是本段最容易踩的坑:剥离后是
res.data.overview,剥离前是res.data.data.overview。 - 手动在
index.js里写headers: { Authorization: token }与请求拦截器重复,属于冗余逻辑,应删除。 index.js里再写一遍 401 判断同样冗余,统一在响应拦截器处理即可,否则多页面重复维护。- 401 时响应拦截器已经跳转登录页,
catch中不要再弹一次提示,避免提示重复。
示例 3:login.js —— 数据剥离后的登录逻辑(注册页同理)
javascript
document.querySelector('#btn-login').addEventListener('click', async () => {
// 1. 收集并校验数据
const form = document.querySelector('.login-form')
const data = serialize(form, { empty: true, hash: true })
const { username, password } = data
// 非空判断
if (username === '' || password === '') {
showToast('用户名和密码不能为空')
return
}
// 格式判断
if (username.length < 8 || username.length > 30 || password.length < 6 || password.length > 30) {
showToast('用户名长度8-30,密码长度6-30')
return
}
// 2. 提交数据
try {
const res = await axios.post('/login', { username, password })
// 数据剥离后,res 就是响应体,res.message 直接可用
showToast(res.message)
// 3. 缓存响应数据
localStorage.setItem('token', res.data.token)
localStorage.setItem('username', res.data.username)
// 4. 跳转首页(延迟一会儿,让提示框显示)
setTimeout(() => {
location.href = './index.html'
}, 1500)
} catch (error) {
showToast(error.response.data.message)
}
})关键行
axios.post('/login', { username, password }):post 简写,参数 1 是 URL,参数 2 是提交数据。res.message:数据剥离后少写一层.data,直接用res.message提示。res.data.token/res.data.username:后端把业务数据放在响应体的data字段里,所以这里仍然要取res.data.xxx,这是剥离后正确的层级。setTimeout延迟跳转:保证 toast 提示能被用户看到。error.response.data.message:错误对象不受响应拦截器成功回调影响,仍是原始结构,所以这里要带完整的.response.data.message。
易错点
- 剥离后
showToast(res.message)正确,但如果写成showToast(res.data.message)就会拿到undefined。 - 反过来,
res.data.token不能写成res.token——token 在业务数据里,不在响应体顶层。 catch里写error.message拿不到后端提示,必须写error.response.data.message。- 注册页
register.js同样是showToast(res.message)+showToast(error.response.data.message)两处调整,不要只改一处。
常见错误
在
index.js里给每个接口单独手写Authorization请求头,后续新增接口时又漏写- 原因:把「携带 token」当成业务逻辑写在了页面里,没意识到这是所有请求的公共逻辑。
- 修法:在
common.js里用axios.interceptors.request.use统一处理,页面代码里删除headers配置。
请求拦截器里写
config.headers['Authorization'] = localStorage.getItem('token'),未登录时传了null/undefined- 原因:没做存在性判断,
localStorage.getItem取不到值时返回null,被当成字符串发给了后端,后端判定为非法 token 直接 401。 - 修法:先
const token = localStorage.getItem('token'),再if (token) { config.headers['Authorization'] = token }。
- 原因:没做存在性判断,
401 处理逻辑仍留在
index.js的catch里,换成别的页面(学生页等)调用接口时就不再跳登录页- 原因:只把「设置 token」抽走了,没把「处理 token 失效」一起抽走,职责划分不彻底。
- 修法:在
axios.interceptors.response.use的第二个回调里统一判断error.response.status === 401,页面里只保留try/catch的业务错误处理,甚至可以不写catch。
响应拦截器
return response.data之后,登录页仍写res.data.token,导致缓存里存进undefined,首页一直跳登录页- 原因:拦截器改变返回结构后,没有回头把已有的
login.js、register.js、index.js中的.data全部清理干净,新旧写法混用。 - 修法:改拦截器的同时全量搜索
.data.逐个删掉,res.data.data.overview→res.data.overview,res.data.token→res.token,res.data.message→res.message。
- 原因:拦截器改变返回结构后,没有回头把已有的
401 提示语直接用后端返回的
error.response.data.message(“token 验证失败”)- 原因:把面向开发者的接口信息直接暴露给终端用户。
- 修法:拦截器里统一
showToast('请重新登录'),用用户能理解的话术;同时setTimeout延迟 1500ms 再location.href = 'login.html',否则提示框来不及显示就跳走了。
手动改
localStorage里的 token 后忘记刷新页面,误以为失效逻辑没生效- 原因:
getData()只在页面加载时执行一次,不刷新不会重新发起请求。 - 修法:改完缓存后 F5 刷新,观察 Network 面板状态码是否为 401、Console 是否有
error.response.status。
- 原因:
练习
抽取登录校验函数:在
common.js中实现checkToken(),从localStorage读取userMessage,解析出token,存在则什么都不做,不存在则showToast('请先登录')并延时跳转login.html;在index.js顶部调用它。要求:解析用JSON.parse并对空值做兜底(|| '{}')。用户名回显 + 退出登录:在
common.js中新增renderUserName()和logout()两个函数。前者读取缓存中的username,存在则写入.username元素;后者给#logout绑定点击事件,清空userMessage缓存、提示「退出成功」、延时跳回登录页。在index.js中调用这两个函数,并在学生页验证复用效果。接口数据剥离改造:现有登录页代码为
localStorage.setItem('token', res.data.token),注册页为showToast(res.data.message),首页为res.data.data.overview。请添加响应拦截器return response.data,然后把这三处全部改写成剥离后的写法,并说明为什么catch里的error.response.data.message不能跟着改。模拟 token 失效并验证:登录成功后打开 DevTools → Application → Local Storage,把
userMessage中 token 字符串截掉后半段,刷新首页。记录:Network 中/dashboard的状态码、Console 中error.response.status的值、页面是否弹出提示、1.5 秒后是否回到登录页、缓存是否被清空。(进阶)区分 401 与其它错误:给
getData()加上try/catch,让 401 走响应拦截器统一处理,而 500 等其它错误在页面内用showToast('服务器繁忙,请稍后再试')提示。要求说明为什么 401 不需要在页面里再写一次。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 案例-用户登录验证 | 11:01 |
| 2 | 案例-用户名回显及退出功能 | 08:38 |
| 3 | 案例-获取首页数据-token | 09:42 |
| 4 | 案例-封装获取数据和渲染概览区域函数 | 07:18 |
| 5 | 案例-渲染概览区域完成 | 03:59 |
| 6 | 案例-token过期验证 | 08:37 |
小结
- 请求头带 token 属于「所有请求都要做」的公共逻辑,判断依据是:这段代码是否每个接口都要重复写一遍;如果是,就该放进拦截器。
- 请求拦截器负责「出门前统一装备」,响应拦截器负责「回来后统一验收」,两者职责不要混在页面文件里。
config.headers是请求拦截器里唯一该动的对象,error.response.status是响应拦截器里判断失效的唯一可靠依据。- 401 的完整处理链是「提示 → 清缓存 → 延时跳转」,三步顺序不能颠倒,清缓存必须在跳转前完成。
- 拦截器返回值一旦从
response改成response.data,整个项目的数据读取层级都会下降一级,必须同步改完所有调用点,否则会出现undefined这种隐蔽 bug。 - 页面文件里应该只剩下「取数据 → 渲染」,凡是和登录态、缓存、跳转相关的东西都应该沉到
common.js。 - 调试登录态问题最有效的手段是手动改 Local Storage 再刷新,而不是等两小时自然过期。