主题
Git 分支与远程仓库(一)
本页对应 Git 分支与远程仓库,共 7 段视频,约 1 小时 4 分。
学习目标
- 能完成登录按钮点击事件注册、表单数据收集与校验,并调用登录接口,在成功与失败分支分别给出提示和页面跳转。
- 能说明 JWT 的获取时机与作用,并在登录成功后保存 token 和用户信息,在后续请求中携带 token。
- 能封装并调用
checkToken实现首页访问控制:读取本地存储判断 token 是否存在,不存在时提示并跳转登录页。 - 能读取本地存储中的用户信息完成首页用户名渲染,并实现退出登录时清除本地信息、跳转登录页。
- 能使用 axios 请求拦截器统一携带 token,使用响应拦截器剥离响应数据并统一处理 401 登录状态失效。
- 能根据接口文档调用统计接口完成首页统计数据展示,并验证 token 失效场景下的页面处理结果。
知识点
JWT 是什么:先理解它解决什么问题
为什么需要:有些页面和接口不是给所有人看的。转写稿里举了一个很直观的场景:如果这是公司的就业数据平台,里面有收入、统计数据等信息,别人知道你的网址,也知道首页通常叫 index.html,直接输入地址就能打开,这显然不行。公开网站如京东、淘宝,未登录也可以访问首页;但私有网站、公司报表类网站必须做权限控制。JWT 就是目前流行的跨域认证解决方案。
怎么想:不要一上来记名词,先想“谁可以访问什么”。用户没有登录、没有凭证时,不应该访问受保护的路由、服务和资源;用户登录成功后,服务端发一个令牌,后续访问带上它,服务端据此判断身份。
怎么做:把 JWT 理解为 JSON Web Token,注意转写稿里的“GWT”“杰森 web token”是口误/同音,正确写法是 JWT、JSON Web Token。它本质上是一个包含信息的字符串,前端把它交给后端,后端解析并判断。
易错点:不要把 JWT 写成 GWT;不要以为 JWT 是前端自己随便生成的;它解决的是身份认证和访问控制问题,不是用来存密码的。
获取 token:必须是在用户登录成功之后
为什么需要:访问受保护资源要有凭证,这个凭证就是 token。没有 token,后端无法判断请求来自谁、是否允许访问。
怎么想:token 不是凭空来的。笔记明确说:在使用 JWT 身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web Token(令牌)。转写稿也反复强调“用户登录并成功了,服务端才会返回 token”。所以推导链是:用户提交登录数据 → 服务端校验 → 登录成功 → 返回 token。

怎么做:登录成功后,接收服务端返回的 token。转写稿提到登录页之前已经拿到了 username,返回数据里也有 token,但当时没有处理它;学习 JWT 后就要明确:这个 token 是后续访问的凭证。
易错点:不是“用户登录”就一定有 token,必须是登录成功;不要把用户名、密码当成 token;也不要在前端随意保存密码,转写稿特别提醒密码不要轻易存。
token 的作用:访问受保护的路由、服务和资源
为什么需要:服务端返回 token 不是摆设,而是让前端在后续请求中证明“我已经登录成功”。
怎么想:笔记写得很清楚:允许用户访问使用该令牌(token)允许的路由、服务和资源。也就是说,token 是通行证。转写稿中用接口文档做了调试式推导:查看“学生数据”里的“获取学生统计数据”接口,发现除了请求方式外,还多了一个 header 参数,要求设置登录之后获取的 token 值;再看其他接口,也需要这个 token。说明这些接口不是随便调的。

怎么做:访问需要认证的接口时,把 token 携带到请求头参数中。接口文档里出现 header 参数时,要按文档要求携带登录后获取的 token 值。
易错点:不要漏掉请求头参数;不要把 token 放在错误的位置;不要以为前端能判断 token 真假。转写稿明确说:前端只负责这个人有没有登录,至于用户是真是假,由服务端通过携带的 token 判定;有效才允许调用接口,无效则不允许。
使用 JWT 的完整思路和常见误区
为什么需要:把“获取 token”和“使用 token”连起来,才能形成完整认证流程。
怎么想:先判断资源是否受保护;再判断用户是否登录成功并拿到 token;请求受保护接口时携带 token;后端校验 token 后决定是否返回数据。
怎么做:登录成功 → 接收服务端返回的 token → 后续请求按接口文档要求携带 token → 服务端校验有效后允许访问。转写稿还提到,后续会用请求拦截器统一携带 token、用响应拦截器统一处理 token 失效,但本节先把 JWT 的概念、获取和作用理解清楚。
易错点:第一,把 JWT 说成 GWT;第二,以为所有网站都必须登录,实际上公开网站首页可以公开访问,私有网站才需要权限控制;第三,登录成功却忘了处理返回的 token;第四,请求接口时漏带 token;第五,误以为 token 由前端判定真假,实际判定在后端。
场景:为什么首页要做访问控制

就业数据平台这类页面往往包含公司内部数据。只要别人知道网址,又知道首页通常叫 index.html,就可能直接访问。公开购物网站不登录也能看首页,是为了卖东西;但私有数据平台不能这样放开。因此需要在访问特定页面时,根据是否登录决定是否允许访问。
为什么需要:防止未登录用户直接进入首页看到内部数据。
怎么想:不是所有页面都拦,本节先处理首页;进入页面前先检查本地是否有登录凭证 token。
怎么做:抽取公共校验函数,在首页 JS 中调用。
易错点:不要把“页面访问控制”和“接口权限校验”混为一谈。前端做的是入口提示和跳转,真正的用户真伪仍由服务端根据 token 判断。
转写中提到的 JWT,即 JSON Web Token,是登录成功后服务端返回给前端的令牌。前端缓存 token,后续请求接口时携带;本节先完成“进入首页前检查有没有 token”。
核心思路:抽取公共校验函数
笔记列出四步:抽取校验函数、判断缓存中的 token、提示用户并跳转登录页、页面调用。多个页面都可能需要判断登录,所以不能把校验逻辑写死在首页里,而要放到公共文件 common.js 中。
为什么需要:多个页面复用同一套判断逻辑,避免重复代码。
怎么想:校验函数只负责“有没有 token”;没有就提示并跳登录页,有就什么都不做,让页面继续执行。
怎么做:在 common.js 中定义 checkLogin。
易错点:函数名要统一。转写口述中曾称为 check token,本节代码落名为 checkLogin,页面调用时必须以实际代码为准。
javascript
// 抽取校验函数(判断是否登录)
function checkLogin() {
// 判断token
const token = localStorage.getItem('token')
// console.log(token)
// token为null说明没有缓存
if (token === null) {
showToast('请先登录')
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
}这里用 localStorage.getItem('token') 读取缓存。若从未存过,返回 null,说明未登录。提示使用公共轻提示 showToast,延迟 1.5 秒再跳转,是为了让提示先显示出来;如果立刻 location.href,提示可能来不及被用户看到。
判断 token 与提示跳转的调试过程
调试时可以先在 checkLogin 里临时打印 token,观察本地存储是否有值。若登录成功后本地存储里没有 token,或键名不是 'token',首页就会一直提示“请先登录”。因此要确认登录成功时已经把服务端返回的 token 写入本地存储,并且读取键名一致。
为什么需要:token 是“是否登录过”的前端判断依据。
怎么想:只判断是否为 null;有值就放行,没值才拦截。
怎么做:提示后 setTimeout 延迟跳转 login.html。
易错点:
- 键名写错,如存的是
token,读的是Token,会一直取不到。 - 跳转路径写错。当前在首页同级页面时用
login.html;若目录层级不同要调整相对路径。 - 只提示不跳转,用户会停留在无数据页面;只跳转不提示,体验突兀。
- 若后续还有代码依赖登录状态,最好在无
token时return,避免继续执行;本节函数末尾无后续逻辑,按笔记代码即可。
首页调用:目前只处理首页
在 index.js 中调用校验函数:
javascript
// 调用判断是否登录的函数
checkLogin()为什么需要:函数定义在 common.js 只是准备好工具,首页不调用就不会生效。
怎么想:首页加载时先校验,再执行首页自己的数据渲染等逻辑。
怎么做:确认首页先引入 common.js,再引入 index.js,然后在 index.js 中调用 checkLogin()。
易错点:如果 common.js 没引入或引入顺序不对,会报 checkLogin is not defined 或 showToast is not defined。本节“目前考虑首页即可”,不要擅自扩散到所有页面;其他页面需要时再调用同一个函数。
提交记录与本节小结
完成代码后按笔记提交:
bash
git add .
git commit -m"首页-页面访问控制"本节完成的是页面级访问控制:登录成功后缓存 token,首页加载时调用 checkLogin,没有 token 就提示“请先登录”并延迟跳转登录页。后续 axios 拦截器会统一在请求头携带 token,响应拦截器再统一处理 401 等失效场景;那是接口层面的统一处理,本节先把首页入口守住。
需求:把登录成功后的用户名渲染到首页右上角
为什么需要:登录成功后,页面右上角如果能显示当前用户名,用户就能确认自己已经登录,页面也更完整。笔记中的目标很明确:把登录成功后缓存下来的用户名,渲染到页面右上角。
怎么想:登录页负责“写入缓存”,首页负责“读取缓存并显示”。首页不能重新请求用户信息,先把缓存里的用户名取出来即可。
怎么做:先确认登录成功后缓存了用户名,再在首页找到右上角显示用户名的元素,把值写进去。效果如下:

易错点:缓存 key 必须和登录页写入时一致。本节笔记代码读取的是 username;如果登录页实际缓存的是 userMessage 这样的对象字符串,直接读 username 会得到 null,需要先确认登录页写入的 key 和格式。
抽取 renderUsername:多页面复用
为什么需要:用户名渲染不只首页会用,后续其他页面也可能需要显示当前登录用户。如果每个页面都写一遍“读缓存、找元素、赋值”,重复且容易漏改。
怎么想:把“读取缓存中的用户名并渲染到 .username 元素”封装成一个公共函数,放到 common.js 中。页面需要时只调用函数。
怎么做:在 common.js 中保留笔记里的关键代码:
javascript
// 抽取渲染函数(渲染缓存中的用户名)
function renderUsername() {
// 读取并渲染用户名
const username = localStorage.getItem('username')
// console.log(username)
document.querySelector('.username').innerText = username
}这里先 localStorage.getItem('username') 读缓存,再用 document.querySelector('.username') 找到页面元素,最后用 innerText 写入。
易错点:querySelector('.username') 依赖页面上确实存在 class 为 username 的元素;如果选择器写错或元素还没加载,赋值会报错。调试时可先取消 console.log(username) 注释,确认读到的值。
首页调用:index.js 中执行 renderUsername
为什么需要:公共函数只有被页面调用才会执行。目前先考虑首页,所以要在首页对应脚本中调用。
怎么想:common.js 负责定义函数,index.js 负责首页业务。首页加载后调用 renderUsername(),把缓存用户名显示出来。
怎么做:在 index.js 中写入:
javascript
// 调用渲染用户名的函数
renderUsername()易错点:要保证 common.js 在 index.js 之前加载,否则会提示 renderUsername is not defined。另外,如果脚本放在 <head> 中且没有延迟执行,DOM 还没生成,document.querySelector('.username') 可能找不到元素;可把脚本放到页面底部,或确认页面结构已加载。
调试过程:从登录缓存到首页回显
为什么需要:首页右上角不显示时,不能盲目改代码,要按“缓存有没有 → 读得对不对 → DOM 找没找到”的顺序排查。
怎么想:转写稿里登录成功时提到要缓存用户信息,并且“存成一个对象格式”,还提醒“待会获取的时候也要用这个名字”。所以首页渲染前要先看本地存储里到底存了什么。
怎么做:登录成功后打开浏览器开发者工具,在 Application/本地存储中查看 key 和值;在 renderUsername 中打印 username;再检查 .username 元素是否存在。若缓存是对象字符串,应先 JSON.parse 再取 username。转写稿中的“头肯”即 token,“杰森”即 JSON,调试时注意这些术语。
易错点:最常见的是 key 不一致:登录页写 userMessage,首页却读 username;或者缓存的是 JSON 字符串,却直接当普通字符串渲染,页面会显示 [object Object] 或 null。
提交记录:完成首页用户名渲染
为什么需要:每完成一个小功能就提交一次,便于回退和查看历史。
怎么想:本次改动包括 common.js 抽取函数和 index.js 调用函数,提交信息写清楚“首页-用户名渲染”。
怎么做:按笔记记录执行:
bash
git add .
git commit -m"首页-用户名渲染"易错点:提交前先确认缓存 key、选择器和脚本加载顺序都已验证;不要只提交 index.js 而漏掉 common.js。
需求场景:退出登录是登录状态的反向操作
首页是登录后的私有页面,用户登录成功后才把 token、用户名等信息存到本地。首页访问控制会通过 checkToken 判断本地是否有 token,没有就提示重新登录。因此退出登录要做的不是“只跳回登录页”,而是先把登录状态清掉,再跳转。
为什么需要:如果只跳转、不删除缓存,本地仍有 token 和用户名,页面访问控制可能仍认为用户处于登录状态。
怎么想:退出登录 = 删除本地登录缓存 + 返回登录页。本节需求明确删除 token、用户名。
怎么做:点击退出按钮后执行删除,并跳转 login.html。
易错点:只删一个缓存;删除的 key 和登录时写入的 key 不一致;跳转路径写错;只跳转不清理。

抽取退出登录函数:把复用逻辑放到 common.js
退出登录不是某一个页面独有的动作,后面其他页面也可能需要退出。如果每个页面都写一遍“绑定点击事件、删缓存、跳转”,代码会重复,也容易漏删。因此本节先抽取公共函数 registerLogout,放在 common.js 中。
为什么需要:复用退出逻辑,页面只负责调用。
怎么想:公共函数里完成“找按钮、绑事件、退出动作”;页面文件只调用函数。
怎么做:在 common.js 中定义 registerLogout:
javascript
// 抽取退出登录函数
function registerLogout() {
// 绑定点击事件
document.querySelector('#logout').addEventListener('click', () => {
// console.log('点了退出')
// 删除缓存并跳转登录页
localStorage.removeItem('username')
localStorage.removeItem('token')
location.href = 'login.html'
})
}易错点:函数名写错;#logout 选择器漏写 #;common.js 没有被页面引入;函数只定义却没有调用。
绑定点击事件与调试过程
退出按钮需要通过 id 找到。代码中使用 document.querySelector('#logout') 获取按钮,再绑定 click 事件。代码里保留了 // console.log('点了退出'),调试时可以临时打开,先确认点击事件是否触发,再检查删除缓存和跳转是否执行。
为什么需要:没有事件绑定,点击按钮不会执行退出逻辑。
怎么想:先验证“点得到”,再验证“删得掉”,最后验证“跳得走”。
怎么做:在回调中依次删除 username、token,然后设置 location.href = 'login.html'。
易错点:按钮 id 不是 logout;脚本在 DOM 渲染前执行导致找不到按钮;选择器写成 logout 而不是 #logout。
删除缓存并跳转登录页
本节删除的是 localStorage 中的 username 和 token。结合前后文,登录成功后会缓存登录信息,首页访问控制依赖 token;退出登录就是把这些信息移除,让后续访问回到未登录状态。转写稿中还提到响应拦截器处理 401 时会清除本地信息并跳转登录页,这和退出登录的目标一致:统一清理登录态。
为什么需要:token 是身份凭证,用户名用于页面回显;不清除会保留登录痕迹。
怎么想:按登录时写入的 key 逐个 removeItem,再跳转登录页。
怎么做:
javascript
localStorage.removeItem('username')
localStorage.removeItem('token')
location.href = 'login.html'易错点:key 名必须和登录写入时一致;不要只删 token 或只删用户名;不要把密码存到本地;跳转路径要相对于当前页面正确;如果项目中有其他业务缓存,不要随意用 clear() 全清。
页面调用与 Git 提交
函数定义后不会自动生效,需要在页面 JS 中调用。本节目前考虑首页,所以在 index.js 中调用 registerLogout(),给退出按钮注册点击事件。
为什么需要:公共函数只是定义,调用后才执行绑定。
怎么想:common.js 提供能力,index.js 负责首页初始化。
怎么做:
javascript
// 调用退出登录函数 给退出按钮注册点击事件
registerLogout()完成功能后提交代码:
bash
git add .
git commit -m"首页-退出登录"易错点:页面没有先引入 common.js;registerLogout() 调用时机太早;只在 common.js 写函数但首页没调用;提交前没有检查本地存储是否真的被删除。建议点击退出后到浏览器 Application 面板确认 username、token 已消失,并确认页面已回到 login.html。
需求与接口约束
本节要完成的是首页统计数据的获取与渲染。页面最终需要把接口返回的统计值展示到对应位置,效果如下:

先明确一个前提:统计数据接口不是公开接口,必须登录后才能调用;调用时还要在请求头中携带 token。接口文档如下:

为什么需要先看接口约束?
因为首页统计数据属于登录后的业务数据,服务端要通过 token 判断“当前用户是否已登录、token 是否有效”。如果前端只写 axios({ url: '/dashboard' }),请求里没有身份凭证,服务端会直接拒绝。
怎么想?
登录成功后,服务端会返回 token。前端把 token 存到本地,在调用需要鉴权的接口时,从本地取出 token,按接口文档要求放进请求头。转写稿里提到的 JWT(JSON Web Token)就是这类身份验证方案:用户登录并成功后,服务端返回 token;后续请求携带 token,服务端据此判断用户身份。注意“JWT”不要写成“GWT”,“token”也不要写成“头肯/命令”。
怎么做?
axios 通过 headers 属性设置请求头对象,key 按文档写,例如 Authorization,value 是携带到服务器的 token 值:
javascript
axios({
url: '/dashboard',
headers: {
Authorization: 'token'
}
})易错点:
headers的 key 要按接口文档写,不能自己改名。- value 不能真的写字符串
'token',实际要从本地存储读取。 - 不要把密码等敏感信息长期存到本地;本项目登录后主要使用 token 和用户名信息。
- token 失效时通常返回 401,项目中会由响应拦截器统一处理,而不是在每个页面重复写一套跳转逻辑。
从登录态到首页请求
为什么需要从登录态取 token?
统计接口依赖登录状态。登录成功后 token 已经存在本地,首页只需要读取并使用它,不需要重新登录。
怎么想?
调用接口前先执行 localStorage.getItem('token')。如果取不到 token,说明登录态不存在或已被清除;如果取到了,就按文档放入请求头。转写稿中多次强调:请求拦截器可以统一携带 token,但本节关键代码仍明确写出了 headers,便于理解“请求头携带 token”这件事。
怎么做?
在 index.js 中定义 getData,使用 async/await 调用接口。await 所在函数必须是 async。
易错点:
await不能写在普通函数里。- 接口地址要和文档一致,本节是
/dashboard。 - 若项目配置了 axios 基地址,不需要再写完整域名;若没有配置,则要按项目实际配置处理。
- token 变量为空时,应先处理登录态,而不是继续发请求。
调用统计数据接口
核心代码如下,保留原笔记实现:
javascript
// 首页-统计数据
async function getData() {
const token = localStorage.getItem('token')
// 调用接口(登录成功之后才可以调用)
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]
})
}
getData()为什么需要先取 overview?
接口返回的数据通常有多层包装,真正要渲染的是 overview 对象。转写稿提到响应拦截器会做“数据剥离”:axios 自身会包一层 data,项目拦截器还可能再处理一层。因此实际层级要以打印结果为准。本节笔记代码写的是 res.data.data.overview,就按本节代码理解。
怎么想?
先拿到完整响应 res,再逐层取到 overview。如果层级不确定,调试时先 console.log(res),确认 data 在哪一层,不要凭感觉多点或少点。
怎么做?
用 Object.keys(overview) 取出所有统计字段名,再通过类名选择器找到页面元素,把值写入 innerText。
易错点:
res.data.data.overview的层级写错会得到undefined。Object.keys的参数必须是对象;若overview为空或不是对象,要先排查接口返回。- 页面上类名必须和接口字段名一一对应,例如字段叫
total,元素类名也应是.total。 - 使用
querySelector前要确认元素存在,否则可能对null设置属性而报错。 - 渲染统计数字一般用
innerText,不要用innerHTML拼接口内容。
调试与提交记录
调试时建议先确认三件事:请求是否携带了 token、响应层级是否正确、overview 的 key 是否与页面类名一致。转写稿中提到的 401 处理、响应拦截器数据剥离,都是为这类鉴权接口服务的:token 无效时统一清除本地信息并跳转登录页;数据层级统一处理后可减少每个页面重复点 data。
完成本节后按 git 记录提交:
bash
git add .
git commit -m"首页-统计数据"易错点: 提交前确认 index.js 已保存、接口地址和请求头没有写死错误值;不要把调试用的临时 console.log 和无关文件一起提交。
需求背景与目标
首页的统计数据需要登录后才能访问。页面调用接口时会把本地缓存中的 token 放到请求头里;如果 token 有效,接口正常返回并渲染数据;如果 token 无效,就提示用户、清除缓存并返回登录页。效果可参考下图:

本节要完成的核心步骤是:判断 token 失效(401 状态码)→ 删除缓存并提示用户 → 返回登录页。调试时可以通过修改缓存中的 token 模拟失效,服务端默认失效时间约为 2 小时。
为什么需要处理登录状态失效
为什么需要: 首页数据不是公开数据,必须登录后才能查看。用户登录成功后,服务端返回 token,前端缓存后在请求头中携带。若 token 过期或被篡改,接口会拒绝访问;此时如果页面不处理,用户只会看到数据加载失败,不知道下一步该做什么。
怎么想: 前端不需要自己判断用户真假,而是根据接口返回的状态码判断。401 通常表示未认证或认证失效,因此前端要做三件事:清掉本地登录信息、用用户能理解的方式提示、跳回登录页重新登录。
怎么做: 在首页请求的异常处理中判断 error.response.status === 401,然后执行清理和跳转。
易错点: 不要把 401 当成普通网络错误直接吞掉;也不要只提示不清理缓存,否则用户再次进入首页仍会带着失效 token。
怎么想:识别401并完成三件事
为什么需要: 只有明确识别 401,才能把“接口失败”转换成“登录状态失效”的业务处理。
怎么想: 请求成功时走 try,渲染统计数据;请求失败时走 catch。在 catch 中先看是否有响应状态码,再判断是否为 401。确认失效后,删除 username 和 token,调用 showToast('请重新登录'),并延迟跳转到 login.html。
怎么做: 延迟跳转使用 setTimeout,给提示框留出展示时间。笔记中使用 1500 毫秒,避免提示刚出现页面就跳走。
易错点: 删除缓存时不要只删 token,用户名等登录信息也应清理;跳转地址要写当前项目中的登录页路径 login.html。
怎么做:首页请求携带token并捕获401
为什么需要: 首页接口需要认证信息,所以请求头必须携带 token;同时要把失效处理放在请求调用处,保证页面能及时响应。
怎么想: 先从 localStorage 取出 token,在 axios 请求的 headers.Authorization 中携带。接口返回后取 res.data.data.overview 渲染页面。异常时判断 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()易错点: error.response 只有在服务端返回响应时才存在;实际项目中更稳妥的写法是先判断 error.response && error.response.status === 401。本节代码按笔记保留,但理解时要意识到这个边界。另外,Authorization 的值按接口要求直接使用 token,不要自行加多余格式。
调试过程:篡改token模拟失效
为什么需要: 服务端默认失效时间约 2 小时,实际中不可能一直等,所以要主动模拟失效。
怎么想: 登录后本地缓存中有 token,手动把 localStorage 中的 token 改成一个无效值,再刷新首页。此时请求头携带的是错误 token,接口返回 401,页面应触发清理、提示和跳转。
怎么做: 在浏览器开发者工具的应用/存储中修改 token,刷新首页观察:是否出现“请重新登录”提示,username 和 token 是否被删除,约 1.5 秒后是否回到 login.html。调试通后,再重新登录恢复。
易错点: 修改的是本地缓存中的 token,不是请求头代码;测试后要重新登录,避免后续接口一直 401。若没有跳转,优先检查状态码判断和 setTimeout 是否执行。
易错点与提交记录
为什么需要: 登录失效处理涉及缓存、提示、跳转三个动作,任何一步遗漏都会造成体验问题。
怎么想: 把处理顺序记成“判断 401 → 清缓存 → 提示 → 延迟跳转”。多个接口都需要处理时,后续可考虑统一封装;本节先把首页这个场景写通。
怎么做: 完成后提交代码:
bash
git add .
git commit -m"首页-登录状态失效"易错点: 不要把 showToast 写成控制台打印;不要立即跳转导致提示看不到;不要忘记删除 username;判断状态码时注意 error.response 是否存在。
课件图示








代码示例
1. index.html(首页结构)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>首页 - 就业数据平台</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
/>
<style>
.navbar {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
padding: 12px 24px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.stats {
display: flex;
gap: 16px;
padding: 24px;
}
.stats .card {
flex: 1;
padding: 16px;
border-radius: 8px;
background: #f5f7fa;
text-align: center;
}
.stats .card p {
font-size: 28px;
margin: 0 0 8px;
color: #4c6ef5;
}
.stats .card span {
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<!-- 顶部:用户名 + 退出登录 -->
<nav class="navbar">
<span class="username"></span>
<button id="logout" class="btn btn-sm btn-outline-danger">退出登录</button>
</nav>
<!-- 统计数据:每个 p 的 class 名 = 接口返回 overview 对象的 key -->
<div class="stats">
<div class="card"><p class="totalStudent"></p><span>学生总数</span></div>
<div class="card"><p class="totalClass"></p><span>班级总数</span></div>
<div class="card"><p class="totalTeacher"></p><span>教师总数</span></div>
<div class="card"><p class="totalCourse"></p><span>课程总数</span></div>
</div>
<!-- 轻提示(bootstrap toast) -->
<div class="toast position-fixed top-0 start-50 translate-middle-x mt-3" id="toast">
<div class="toast-body"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="./common.js"></script>
<script src="./index.js"></script>
</body>
</html>要点解释 / 易错点
- 用户名节点用
.username、退出按钮用id="logout",这两个选择器必须和后面common.js里的选择器完全一致,改一个就取不到元素。 - 统计卡片的
class名就是接口overview对象的 key,因为 JS 是用document.querySelector(`.${key}`)动态找元素的,key 名写错就渲染不出来。 - 脚本加载顺序不能乱:
axios→bootstrap→common.js→index.js。common.js必须在前,否则index.js里调用checkLogin()会报is not defined。 - 轻提示的容器必须有
id="toast",showToast靠它找节点。
2. common.js(公共函数抽取)
javascript
// 轻提示(基于 bootstrap toast)
function showToast(msg) {
const toastEl = document.querySelector('#toast')
toastEl.querySelector('.toast-body').innerText = msg
new bootstrap.Toast(toastEl, { delay: 1500 }).show()
}
// 抽取校验函数(判断是否登录)
function checkLogin() {
// 判断token
const token = localStorage.getItem('token')
// console.log(token)
// token为null说明没有缓存
if (token === null) {
showToast('请先登录')
setTimeout(() => {
location.href = 'login.html'
}, 1500)
}
}
// 抽取渲染函数(渲染缓存中的用户名)
function renderUsername() {
// 读取并渲染用户名
const username = localStorage.getItem('username')
// console.log(username)
document.querySelector('.username').innerText = username
}
// 抽取退出登录函数
function registerLogout() {
// 绑定点击事件
document.querySelector('#logout').addEventListener('click', () => {
// console.log('点了退出')
// 删除缓存并跳转登录页
localStorage.removeItem('username')
localStorage.removeItem('token')
location.href = 'login.html'
})
}要点解释 / 易错点
localStorage.getItem()取不到时返回的是null,不是undefined,所以判断写token === null。- 提示之后必须
setTimeout延时约 1.5 秒再跳转,否则 toast 还没显示出来页面就被跳走了,用户什么也看不到。 renderUsername用的是innerText赋值,不要用innerHTML,用户名里的特殊字符不会被当成标签解析。- 缓存键名
'username'、'token'必须和登录页存的时候用的键名一模一样,否则取出来是null,页面上会直接显示null。 registerLogout里 token 和用户名两个都要删,只删一个会导致“退出后页面还显示用户名”或“重新进首页仍被当成已登录”。- 这些函数都抽在
common.js里,是为了让多个页面(首页、学生页、班级页…)都能复用,页面里只写一行调用即可。
3. index.js(首页业务逻辑)
javascript
// 首页-页面访问控制
checkLogin()
// 首页-用户名渲染
renderUsername()
// 首页-退出登录
registerLogout()
// 首页-统计数据
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()要点解释 / 易错点
- 请求头里的
Authorization是按接口文档规定的 key,值是直接拿到的 token 字符串,不要自己额外拼Bearer之类的字符。 res.data.data.overview有两层data:第一层是 axios 包装的(转写稿里说的“数据剥离”),第二层是后端返回体里的data。少点一层就是undefined。- 渲染用
Object.keys(overview).forEach(...)遍历,key 名就是页面上卡片的 class 名,两边必须对得上。 await拿不到错误,所以必须包try...catch;catch里通过error.response.status判断,401 就是 token 过期或被篡改。error.response在网络断开时是undefined,直接.status会二次报错,实际项目里可以写成error.response?.status === 401。- 想模拟失效,可以在控制台里手动改一下缓存中的 token,再刷新页面,看是否弹出“请重新登录”并跳回
login.html(默认失效时间 2 小时)。 - 易错点:
checkLogin()只是“提示 + 延时跳转”,并不会中断后面的代码,所以没登录时getData()仍会发出一次请求、再触发一次 401 提示。需要严格一点时,可以让checkLogin返回布尔值,在index.js里判断后再调用getData()。 /dashboard是相对地址,依赖 axios 的基地址配置(项目里在统一配置文件中设置过baseURL),没配就会请求到本地文件路径而报错。
常见错误
1. 未登录时页面仍然继续往下跑,控制台报 Cannot read properties of null
- 现象:打开首页未登录,先弹出“请先登录”,1.5 秒后跳转,但控制台同时报错,统计数据接口照样发出去了。
- 原因:
checkLogin()内部只做了提示和setTimeout跳转,没有中断调用方的执行流;index.js里checkLogin()之后紧跟着renderUsername()、getData(),它们照常执行。 - 修法:让校验函数有返回值——
return false/return true,调用方写成if (!checkLogin()) return,把“是否继续”的决定权交回调用方;不要依赖定时器去“挡”后面的代码。
2. 只在首页做了校验,其他页面直接被 URL 绕过
- 现象:手动在地址栏输入
index.html会被拦,但输入别的内页地址照样能进。 - 原因:校验函数抽出来了,但只在
index.js里调用了一次,其余页面没调。 - 修法:把“需要登录的页面”列出来逐个调用;或者在
common.js里做成按页面自动执行的守卫(读location.pathname判断白名单),新增页面时不用再想起来补一行。
3. 请求头参数名写错或值写错,接口一直返回未授权
- 现象:本地明明有 token,
/dashboard还是 401。 - 原因:
headers里写成了token: token、Token: token,或把整个对象{ token }塞进去;也有把 token 存进去时多套了一层JSON.stringify,取出来是带引号的字符串。 - 修法:参数名严格照接口文档(
Authorization),值就是缓存里取出的原始字符串;调试时先在 Network 面板看 Request Headers 实际发了什么,不要靠猜。
4. catch 里直接读 error.response.status,遇到断网就二次报错
- 现象:断网或后端没起时,控制台报
Cannot read properties of undefined (reading 'status'),页面卡住不跳转。 - 原因:只有“服务器有响应”时
error.response才存在;网络错误、超时、请求被取消时它都是undefined。 - 修法:先判空——
if (error.response && error.response.status === 401) { ... },其余情况走另一条提示分支(如“网络异常,请稍后重试”)。
5. 401 处理时提示还没看清就跳走了 / 或者干脆不跳
- 现象:token 过期后 toast 一闪而过,或者提示弹了但人还停在首页,数据是空的。
- 原因:
showToast()和location.href写在一起同步执行;或只清了缓存忘了跳转,页面停留在“已登出但仍是首页”的状态。 - 修法:提示 → 延迟 → 跳转,三步固定顺序;同时清
token和username两项缓存,缺一项就会出现“跳回登录页但首页还显示旧用户名”的错位。
6. 退出登录用 localStorage.clear()
- 现象:退出后别的功能数据也没了(比如主题、草稿、上次访问页)。
- 原因:
clear()清的是整个域下所有键,而不是本业务的登录态。 - 修法:用
removeItem精确删除登录相关的键;如果键变多了,抽一个clearLoginState()统一维护,避免以后加一个键就要改三处。
7. renderUsername 在没有 .username 元素的页面被调用
- 现象:登录页/注册页控制台报
Cannot set properties of null。 - 原因:函数被塞进
common.js后被所有页面引入,但只有部分页面有对应 DOM。 - 修法:函数内部先取元素再判空(
if (!el) return),或把这类“依赖具体 DOM”的函数从公共文件里拆出去,只在该页面调用。
8. axios.post(url, { data }) 把参数包成了两层
- 现象:接口报参数校验失败,后端收到的 body 是
{ data: {...} }。 - 原因:
data本身已经是对象,外面又套了一层对象字面量。 - 修法:直接
axios.post('/login', data);传参前想清楚“我传的是一个对象,还是一个包着对象的对象”。
练习
练习 1|把校验函数改成“可中断”的守卫 改造 checkLogin():未登录时返回 false 并完成提示+跳转,已登录返回 true。在首页按 if (!checkLogin()) return 的方式串起 renderUsername() 和 getData()。 验收:清空 localStorage 刷新首页,Network 面板里不能出现 /dashboard 请求,控制台无报错。
练习 2|统一携带 token 把“每次请求手写 headers: { Authorization: token }”改成在请求拦截器里统一加;同时准备一个白名单(/login、/register)不加 token。 验收:把 index.js 里所有 headers 配置删掉,接口仍能正常返回;登录接口的 Request Headers 中没有 Authorization。
练习 3|把 401 处理搬到响应拦截器 删除 getData() 的 try/catch,改为在响应拦截器中判断 401:清缓存 → 提示 → 延迟跳转。顺便把 res.data.data 的层级在拦截器里剥成一层。 验收:手动把缓存里的 token 改乱后刷新首页,只弹一次提示、只跳一次登录页;同时 getData() 里的 res.data.data.overview 变成 res.data.overview。
练习 4|健壮渲染 给统计数据渲染加容错:接口返回字段缺失或为 null 时,页面上显示 - 而不是 undefined;overview 为空对象时不报错。 验收:用浏览器 DevTools 的 Network 面板手动篡改响应(或临时改 mock 数据)删掉一个字段,页面显示 -,其余字段正常。
练习 5|找出下面代码的问题并说明修法
javascript
async function getData() {
const token = localStorage.getItem('token')
const res = await axios({ url: '/dashboard', headers: { token } })
Object.keys(res.data.data.overview).forEach(key => {
document.querySelector(`.${key}`).innerText = res.data.data.overview[key]
})
}
getData()要求:至少指出 3 处问题(提示:参数名、异常兜底、元素不存在、未登录就调用),并给出改造后的版本。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 1反馈 | 08:25 |
| 2 | 2昨日回顾及今日内容 | 12:24 |
| 3 | 3上午回顾 | 04:28 |
| 4 | 4下午回顾 | 12:15 |
| 5 | 案例-登录功能1 | 08:18 |
| 6 | 案例-登录功能完成 | 11:38 |
| 7 | JWT介绍 | 07:06 |
小结
- 校验函数的价值在于“能拦住后续代码”,而不是“能弹提示”。没有返回值的校验函数,只是打了个招呼,业务流程照样往下走。
- 前端做的是体验,后端做的是安全。页面控制只能挡住正常操作路径,真正的鉴权判定永远发生在服务端,401 才是唯一可信的“你不行”信号。
- 登录态是“一处状态、多条路径”:登录写入、退出清除、401 清除,三条路径必须维护同一组缓存键,少一条就会出现状态错位。
- 请求头统一配置优于逐处手写。写 3 次不会错,写 30 次一定漏;拦截器是降低“遗漏概率”的手段,不是炫技。
- 错误处理要分层:网络层错误(无响应)和业务层错误(有响应、状态码非 2xx)必须分开处理,混在一起写必然在断网时崩。
- 提示与跳转是有时序的。凡是“先告知用户再离开当前页”的场景,中间必须有延迟,且延迟期间要考虑用户重复点击造成的多次跳转。
- 公共函数要能容忍“自己不该被执行”的场合。抽到
common.js里的函数会被所有页面引入,取 DOM 前先判空,是成本最低的防御。 - 调试先看事实,再改代码。token 到底发没发、发成了什么名字,答案在 Network 面板的 Request Headers 里,不在你的记忆里。