Skip to content

Git 分支与远程仓库(一) ​

本页对应 Git 分支与远程仓库,共 7 段视频,约 1 小时 4 分。

学习目标 ​

  1. 能完成登录按钮点击事件注册、表单数据收集与校验,并调用登录接口,在成功与失败分支分别给出提示和页面跳转。
  2. 能说明 JWT 的获取时机与作用,并在登录成功后保存 token 和用户信息,在后续请求中携带 token。
  3. 能封装并调用 checkToken 实现首页访问控制:读取本地存储判断 token 是否存在,不存在时提示并跳转登录页。
  4. 能读取本地存储中的用户信息完成首页用户名渲染,并实现退出登录时清除本地信息、跳转登录页。
  5. 能使用 axios 请求拦截器统一携带 token,使用响应拦截器剥离响应数据并统一处理 401 登录状态失效。
  6. 能根据接口文档调用统计接口完成首页统计数据展示,并验证 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。
易错点:

  1. 键名写错,如存的是 token,读的是 Token,会一直取不到。
  2. 跳转路径写错。当前在首页同级页面时用 login.html;若目录层级不同要调整相对路径。
  3. 只提示不跳转,用户会停留在无数据页面;只跳转不提示,体验突兀。
  4. 若后续还有代码依赖登录状态,最好在无 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'
  }
})

易错点:

  1. headers 的 key 要按接口文档写,不能自己改名。
  2. value 不能真的写字符串 'token',实际要从本地存储读取。
  3. 不要把密码等敏感信息长期存到本地;本项目登录后主要使用 token 和用户名信息。
  4. token 失效时通常返回 401,项目中会由响应拦截器统一处理,而不是在每个页面重复写一套跳转逻辑。

从登录态到首页请求 ​

为什么需要从登录态取 token?
统计接口依赖登录状态。登录成功后 token 已经存在本地,首页只需要读取并使用它,不需要重新登录。

怎么想?
调用接口前先执行 localStorage.getItem('token')。如果取不到 token,说明登录态不存在或已被清除;如果取到了,就按文档放入请求头。转写稿中多次强调:请求拦截器可以统一携带 token,但本节关键代码仍明确写出了 headers,便于理解“请求头携带 token”这件事。

怎么做?
在 index.js 中定义 getData,使用 async/await 调用接口。await 所在函数必须是 async。

易错点:

  1. await 不能写在普通函数里。
  2. 接口地址要和文档一致,本节是 /dashboard。
  3. 若项目配置了 axios 基地址,不需要再写完整域名;若没有配置,则要按项目实际配置处理。
  4. 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。

易错点:

  1. res.data.data.overview 的层级写错会得到 undefined。
  2. Object.keys 的参数必须是对象;若 overview 为空或不是对象,要先排查接口返回。
  3. 页面上类名必须和接口字段名一一对应,例如字段叫 total,元素类名也应是 .total。
  4. 使用 querySelector 前要确认元素存在,否则可能对 null 设置属性而报错。
  5. 渲染统计数字一般用 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 是否存在。

课件图示 ​

课件第4页:首页-页面访问控制 需求: 核心步骤: 访问首页 已登录 允许访问 未登录 提示

课件第2页:JWT介绍 JSON Web Token是目前最为流行的跨域认证解决方案 如何获

课件第6页:首页-退出登录 需求: 核心步骤: 抽取退出登录函数 绑定点击事件 删除缓存并跳

课件第8页:首页-登录状态失效 需求: token无效原因:过期、被篡改 核心步骤: 判断t

课件第5页:首页-用户名渲染 需求: 核心步骤: 抽取渲染函数 读取并渲染用户名 页面调用函

课件第7页:首页-统计数据 需求: 核心步骤: 调用接口 渲染数据 axios({ url:

课件第3页:总结 总结 1. JSON Web Token(JWT): 目前最为流行的跨域认

课件第9页:总结 总结 服务器 浏览器 用户信息 JWT(令牌) 缓存: JWT(令牌) J

代码示例 ​

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/[email protected]/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/[email protected]/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 处问题(提示:参数名、异常兜底、元素不存在、未登录就调用),并给出改造后的版本。

原视频对照 ​

#视频时长
11反馈08:25
22昨日回顾及今日内容12:24
33上午回顾04:28
44下午回顾12:15
5案例-登录功能108:18
6案例-登录功能完成11:38
7JWT介绍07:06

小结 ​

  1. 校验函数的价值在于“能拦住后续代码”,而不是“能弹提示”。没有返回值的校验函数,只是打了个招呼,业务流程照样往下走。
  2. 前端做的是体验,后端做的是安全。页面控制只能挡住正常操作路径,真正的鉴权判定永远发生在服务端,401 才是唯一可信的“你不行”信号。
  3. 登录态是“一处状态、多条路径”:登录写入、退出清除、401 清除,三条路径必须维护同一组缓存键,少一条就会出现状态错位。
  4. 请求头统一配置优于逐处手写。写 3 次不会错,写 30 次一定漏;拦截器是降低“遗漏概率”的手段,不是炫技。
  5. 错误处理要分层:网络层错误(无响应)和业务层错误(有响应、状态码非 2xx)必须分开处理,混在一起写必然在断网时崩。
  6. 提示与跳转是有时序的。凡是“先告知用户再离开当前页”的场景,中间必须有延迟,且延迟期间要考虑用户重复点击造成的多次跳转。
  7. 公共函数要能容忍“自己不该被执行”的场合。抽到 common.js 里的函数会被所有页面引入,取 DOM 前先判空,是成本最低的防御。
  8. 调试先看事实,再改代码。token 到底发没发、发成了什么名字,答案在 Network 面板的 Request Headers 里,不在你的记忆里。