Skip to content

滚动容器与页面交互(三) ​

本页对应 滚动容器与页面交互,共 8 段视频,约 1 小时 15 分。

学习目标 ​

  • 能够在响应拦截器的失败回调中识别状态码 401,并实现清除登录态、跳转登录页的重新登录逻辑。
  • 能够使用 Pinia 定义并持久化 redirectUrl 与 openType,保存登录前页面路径及跳转方式。
  • 能够通过页面栈获取当前页面完整路径(含参数),判断是否为 tabBar 页面,并选择 switchTab 或 redirectTo 完成登录成功跳转。
  • 能够配置接口请求白名单,使登录、验证码等接口不携带 token,避免 401 循环跳转。
  • 能够封装 scroll-page 组件,设置页面高度与安全区样式,并支持自定义底部边框线。
  • 能够为 scroll-page 组件接入下拉刷新与上拉加载事件,并通过自定义事件向父组件传递事件对象。

知识点 ​

一、在响应拦截器里识别401 ​

为什么需要:本地保存的 token 有有效期,过期后服务端会认为未登录。前后端约定:没有 token 或 token 失效时返回 401。
怎么想:401 是服务端响应结果,应该在响应拦截器的“失败回调”里判断,而不是请求拦截器或成功回调。
怎么做:在 request.js 的响应拦截器中打印错误并判断状态码,命中 401 就执行重新登录。

js
service.interceptors.response.use(
  (res) => res.data,
  (error) => {
    console.log(error)
    if (error.response.status === 401) {
      relogin()
    }
    return Promise.reject(error)
  }
)

function relogin() {
  uni.redirectTo({ url: '/pages/login/login' })
}

易错点:不要把 401 判断写进成功回调;跳转地址前的斜杠不要丢;调试时可故意把 token 改错,观察控制台是否打印 401。

二、记录登录后要返回的页面 ​

为什么需要:用户未登录访问某页被切到登录页,登录成功后应回到原页面。
怎么想:转写稿里演示过,若用地址拼接 redirectUrl,原地址本身已有 ? 时,小程序会过滤掉第二个问号后的参数;H5 可能正常,小程序不兼容。因此改用 Pinia 存。
怎么做:在用户 store 中增加 redirectUrl 和 openType,默认回首页,首页通常是 tabBar 页面,所以默认 switchTab;同时做持久化。

js
const redirectUrl = ref('/pages/index/index')
const openType = ref('switchTab')

易错点:默认地址要写完整;持久化数组里写字符串,不要写变量;openType 要记录跳转方式,不能只记地址。

三、获取当前页面路径与打开方式 ​

为什么需要:跳转前必须知道“当前在哪”和“目标是不是 tabBar 页面”。
怎么想:小程序用 getCurrentPages() 取页面栈,数组最后一项是当前页;fullPath 比 route 更完整,带参数。tabBar 页面最多 5 个,可用路径数组判断。
怎么做:取当前页,判断 route 是否在 tabBar 列表,决定 openType,再把 fullPath 存入 store。

js
const pages = getCurrentPages()
const current = pages[pages.length - 1]
const redirectUrl = current.fullPath
const tabBarList = [
  '/pages/index/index',
  '/pages/my/my'
]
const openType = tabBarList.includes(current.route)
  ? 'switchTab'
  : 'navigateTo'

易错点:判断 tabBar 用 route,保存地址用 fullPath;验证码、登录接口若也触发 401,可能把登录页自身存进去,后面要用白名单解决。

四、登录成功后按记录跳转 ​

为什么需要:登录成功不能永远写死跳首页,要回到刚才记录的页面。
怎么想:openType 是变量,不能写 uni.openType,要用中括号动态取方法。
怎么做:登录提交成功后,从 store 取 openType 和 redirectUrl 跳转。

js
const userStore = useUserStore()
uni[userStore.openType]({
  url: userStore.redirectUrl
})

易错点:动态属性用 uni[userStore.openType];普通属性才用点;redirectUrl 要带斜杠;测试前清空本地脏数据。

五、接口请求白名单 ​

为什么需要:登录、验证码接口本身是为了拿 token,不应携带旧 token。否则旧 token 失效时,验证码请求也返回 401,又跳登录页,形成“登录页跳登录页”的死循环。
怎么想:在请求拦截器里判断当前接口是否属于白名单,不在白名单才加 token。
怎么做:定义白名单数组,用 includes 判断;先打印 config.url 确认路径。

js
const whiteList = ['/user/code', '/user/login', '/user/login/password']

if (!whiteList.includes(config.url)) {
  config.header.Authorization = 'Bearer ' + token
}

易错点:白名单路径必须和实际接口一致;不要靠响应拦截器兜底;验证码接口成功且未带 token,才说明白名单生效。

六、scroll-page 组件封装要点 ​

为什么需要:统一处理页面滚动、安全区、tabBar 顶部边框线、下拉刷新和上拉加载。
怎么想:本质是二次封装 scroll-view。高度取 windowHeight,底部安全区用样式适配,边框线用 border-bottom,事件用 defineEmits 中转。
怎么做:

html
<scroll-view
  scroll-y
  :style="{ height: windowHeight + 'px' }"
  :refresher-enabled="refresh"
  :refresher-triggered="triggered"
  @scrolltolower="onScrollToLower"
>
  <slot />
</scroll-view>
js
const windowHeight = uni.getSystemInfoSync().windowHeight
const emits = defineEmits(['scrolltolower', 'refresherrefresh'])

安全区可加 padding-bottom: env(safe-area-inset-bottom);;边框线可传 borderStyle,为 false 时不显示。
易错点:高度忘加 px;加边框后要 box-sizing: border-box,否则盒子变高出现滚动条;测试用的固定高度、背景色要删除;真机才能更好验证底部安全区。

先纠正一个词:本节讲的是“路径参数” ​

为什么需要:登录成功后要回到登录前的页面,就必须把“原页面路径”和“打开方式”记下来。怎么想:转写稿里的“中径参数”应理解为“路径参数”,不是新概念,而是登录回跳链路中的两个 Pinia 数据。怎么做:在用户 store 中增加 redirectURL 和 openType,前者默认指向首页,后者默认用 switchTab。易错点:不要继续只靠地址栏问号传参,小程序里地址再带参数时第二个问号后的内容可能被过滤。

401 要在响应拦截器的失败回调里处理 ​

为什么需要:本地有 token 不代表 token 永远有效,后端约定 token 不存在或失效时返回 401。怎么想:401 是响应结果,应该在响应拦截器里判断,而不是请求拦截器。怎么做:在响应拦截器的第二个回调中打印 error,故意把本地 token 改错,刷新页面观察控制台,确认 statusCode 为 401;随后调用重新登录方法跳登录页。易错点:把 401 判断写进成功回调,或者只看 data 不看状态码,都会导致失效逻辑不触发。核心形态是:

js
if (error.statusCode === 401) {
  relogin()
}

记录登录前的路径与打开方式 ​

为什么需要:用户从首页跳到登录页,登录成功后要回到首页;若原页面带参数,也要尽量保留。怎么想:路径放 redirectURL,跳转方式放 openType,并做持久化,防止刷新丢失。怎么做:在 Pinia 中追加两个 ref 数据,默认值分别指向首页和 switchTab;把它们加入持久化数组。易错点:只记路径不记打开方式;或持久化时把变量名直接写进数组而不是字符串,导致恢复失败。

获取当前页面路径:页面栈最后一项与 fullPath ​

为什么需要:跳转前必须知道“当前在哪”。怎么想:小程序没有 Vue 那样方便的路由对象,但可以用页面栈。怎么做:取 getCurrentPages() 的最后一个单元,打印后使用 $page.fullPath,它比 route 更完整,能带参数。调试时可在 401 分支里打印当前页对象,确认 fullPath 是否包含查询参数。易错点:用 route 丢掉参数;把页面栈顺序理解反;在非页面环境里取页面栈。

js
const current = getCurrentPages().at(-1)
const fullPath = current.$page.fullPath

判断 tabBar 页面,决定 switchTab 还是 redirectTo ​

为什么需要:tabBar 页面只能用 switchTab,普通页面用 redirectTo 一类跳转。怎么想:把 tabBar 页面路径列成数组,看当前 route 是否在数组里。怎么做:若命中,openType 记为 switchTab;否则记为普通跳转方式;再把 redirectURL、openType 写入 Pinia。易错点:不区分页面类型直接跳转;路径少了前导斜杠;判断时拿 fullPath 和只含路径的数组比较,导致带参数页面误判。

登录成功后的跳转与验证 ​

为什么需要:登录成功后不能永远写死跳首页,要回到记录的原页面。怎么想:Pinia 里已经存了地址和打开方式,用变量访问 uni 上的方法。怎么做:登录提交成功后执行 uni[userStore.openType]({ url: userStore.redirectURL })。验证时先清空本地存储,从首页触发登录,确认默认回首页;再模拟 token 失效,确认回登录页后重新登录能回原页。易错点:把 openType 写死;对象属性用点访问变量名;忘记清脏数据导致测试结果不可信。

白名单:解决验证码接口 401 死循环 ​

为什么需要:调试时发现获取验证码也带了错误 token,于是又返回 401,又跳登录页,形成自己跳自己。怎么想:登录、验证码、密码登录这类接口本来就不该受 token 限制。怎么做:在请求拦截器里定义 whiteList,用 includes(config.url) 判断;只有“有 token 且不在白名单”时才加 token 头。易错点:白名单路径与实际请求路径不一致;把白名单判断写在加 token 之后;忘记验证码接口也会触发响应拦截器。

js
if (token && !whiteList.includes(config.url)) {
  // 追加 token 头
}

scroll-page 组件:高度、安全区与边框 ​

为什么需要:页面要滚动、适配安全区、自定义 tabBar 顶部边框线,还要支持下拉刷新和上拉加载。怎么想:内部仍用 scroll-view 二次封装,外层盒子高度等于页面可用高度。怎么做:用 API 取 windowHeight,给组件根节点设置高度;底部安全区用样式处理;边框线通过 borderStyle 属性传入,组件内绑定样式。调试时给 border-bottom 加粗后出现多余滚动条,用 box-sizing: border-box 修复。易错点:高度漏单位;把边框宽度算到高度外;把测试用的固定高度留在组件里。

下拉刷新与上拉加载事件中转 ​

为什么需要:原生 scroll-view 支持下拉刷新和滚动到底事件,但要让使用组件的页面也能监听。怎么想:组件内开启 refresher-enabled,用 refresher-triggered 控制状态;滚动到底用 scrolltolower。怎么做:通过 defineEmits 声明事件,在原生事件触发时 emit 出去,并把事件对象传给父组件。易错点:只写原生事件不 emit;emit 事件名与父组件监听名不一致;测试完不删除临时高度和测试事件。

本节要解决的问题:登录后回到原页面 ​

在优医咨询项目里,响应拦截器一旦发现接口返回 401,就说明 token 不存在或已经失效,需要重新登录。这里有一个体验问题:用户可能是在“我的”页面或其他页面触发 401 的,重新登录成功后,不能一律跳回首页,而应尽量回到原来页面。要做到这一点,就必须在跳登录页之前,把“当前页面路径”和“页面打开方式”记下来。

为什么需要?因为登录页本身不知道用户从哪来;等登录成功再取页面栈,原页面已经丢了。怎么想?在响应拦截器的 401 处理逻辑里记录,而不是在登录页记录。怎么做?先用 getCurrentPages() 取页面栈,最后一个单元就是当前页,再取 fullPath 存入 Pinia。易错点:不要等到登录成功后才获取当前页,那时页面栈已经变化。

获取当前页面栈与 fullPath ​

小程序没有 Vue Router 那样方便的路由对象,但可以通过 getCurrentPages() 拿到页面栈数组。数组最后一项是当前页。当前页上有 route 和 fullPath 两个常用属性:route 更偏页面路径,fullPath 会带查询参数,更完整。转写稿里通过打印 currentPage 验证了这一点:fullPath 比单纯路径多了参数部分,因此存跳转地址时应优先用 fullPath。

js
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
console.log('currentPage', currentPage)

const fullPath = currentPage.fullPath

易错点:页面栈可能为空,实际项目里要判空;route 通常不带前导斜杠,而 fullPath 带斜杠和参数,比较时不要混用。

判断 tabBar 页面并确定 openType ​

为什么要判断?小程序里 tabBar 页面只能用 switchTab 跳转,普通页面才用 redirectTo 或 navigateTo。如果统一用普通跳转,tabBar 页面会跳不过去。怎么想?tabBar 页面数量有限,把项目 app.json 中 tabBar.list 的 pagePath 整理成数组,判断当前页面 route 是否在数组里。怎么做?用 includes 判断,并记录 openType。

js
const currentRoute = '/' + currentPage.route
const tabBarPages = [
  // 按项目 app.json 中 tabBar.list 的 pagePath 填写,记得统一加前导斜杠
]
const isTabBar = tabBarPages.includes(currentRoute)

const userStore = useUserStore()
userStore.redirectURL = fullPath
userStore.openType = isTabBar ? 'switchTab' : 'redirectTo'

易错点:判断 tabBar 时建议用 route 转成统一格式,不要直接用带参数的 fullPath 去 includes,否则可能因参数导致匹配失败。

把路径和打开方式存入 Pinia ​

为什么放 Pinia?因为响应拦截器、登录页等多个位置都要用,且要跨页面保存。怎么想?在用户 store 里增加 redirectURL 和 openType 两个数据,并做持久化,防止刷新丢失。转写稿里默认值设置为首页路径和 switchTab,表示没有记录时登录成功默认回首页。

js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const redirectURL = ref('/pages/index/index') // 以项目实际首页路径为准
  const openType = ref('switchTab')

  // 按项目已有持久化方式,把 redirectURL、openType 加入
  return { redirectURL, openType }
})

易错点:持久化时要把这两个字段加入存储配置;默认值要和项目实际首页、tabBar 配置对应。

登录成功后按记录跳转 ​

登录提交成功后,根据 openType 动态调用 uni 的跳转方法,并传入 redirectURL。转写稿强调这里要用中括号取动态方法,不能写成 uni.openType。

js
const userStore = useUserStore()
uni[userStore.openType]({
  url: userStore.redirectURL
})

易错点:openType 是变量,必须用 uni[userStore.openType];如果是 switchTab,URL 参数不能被当作普通 query 传递,带参场景要另行处理。调试时还发现:登录、验证码等接口若带着错误 token 请求,会再次返回 401 并跳登录页,形成自己跳自己的循环,因此需要接口白名单,把登录相关接口排除在 token 校验之外。这样,获取页面路径、存 Pinia、登录后跳回原页面的链路才算完整。

代码示例 ​

示例一:请求/响应拦截器(401 检测 + 接口白名单) ​

js
// utils/request.js
import { reLogin } from './auth'

const BASE_URL = 'https://api-xxx.example.com'

// 接口白名单:这些接口不受 token 限制(登录相关接口本身就是为了拿 token)
const whiteList = ['/login', '/login/password', '/code']

// ===== 请求拦截器 =====
const requestInterceptor = (config) => {
  const token = uni.getStorageSync('token')
  // 有 token 且不在白名单中,才把 token 带过去
  if (token && !whiteList.includes(config.url)) {
    config.header.Authorization = token
  }
  return config
}

// ===== 响应拦截器:成功回调 =====
const responseSuccess = (response) => response.data

// ===== 响应拦截器:失败回调 =====
const responseError = (error) => {
  console.log('error', error)
  // 401 是前后端约定好的「未登录 / token 失效」状态码
  if (error.statusCode === 401) {
    reLogin()
  }
}

const request = (options) => {
  const config = requestInterceptor({ header: {}, ...options })
  return new Promise((resolve, reject) => {
    uni.request({
      url: BASE_URL + config.url,
      method: config.method || 'GET',
      data: config.data || {},
      header: config.header,
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(responseSuccess(res))
        } else {
          // 非 200 视为请求失败,交给响应拦截器的失败回调
          responseError(res)
          reject(res)
        }
      },
      fail: (err) => {
        responseError(err)
        reject(err)
      }
    })
  })
}

export default request

要点与易错点

  • statusCode === 401 要写在响应拦截器的第二个(失败)回调里,因为 401 本身就代表请求已经失败。
  • 白名单必须先判断再带 token:登录/验证码接口如果也带上了失效 token,就会返回 401 → 跳登录页 → 又发验证码请求 → 又 401,形成自己跳自己的死循环。
  • 白名单判断用 !whiteList.includes(config.url) 取反,语义是「不是白名单才加 token」。
  • config.url 是相对路径,要和白名单里的字符串完全一致(含前导 /),否则匹配不上。

示例二:pinia 存跳转地址 + 获取页面路径 + 登录成功跳转 ​

js
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const token = ref('')
  // 登录成功后要跳转的地址,默认首页
  const redirectUrl = ref('/pages/index/index')
  // 跳转方式:tabBar 页用 switchTab,普通页用 navigateTo
  const openType = ref('switchTab')

  return { token, redirectUrl, openType }
}, {
  persist: {
    // 字符串数组,刷新页面后数据不丢
    paths: ['token', 'redirectUrl', 'openType']
  }
})
js
// utils/auth.js
import { useUserStore } from '@/stores/user'

// tabBar 页面路径(最多 5 个)
const tabBarList = [
  '/pages/index/index',
  '/pages/my/my'
]

export const reLogin = () => {
  // 页面栈的最后一个单元就是当前页
  const pageStack = getCurrentPages()
  const currentPage = pageStack[pageStack.length - 1]
  console.log('currentPage', currentPage)

  const userStore = useUserStore()
  // fullPath 才是带参数的完整路径
  userStore.redirectUrl = currentPage.fullPath
  // 判断是不是 tabBar 页面,决定跳转方式
  userStore.openType = tabBarList.includes(currentPage.route)
    ? 'switchTab'
    : 'navigateTo'

  uni.redirectTo({ url: '/pages/login/login' })
}
js
// pages/login/login.vue —— 登录成功、数据存好之后
const userStore = useUserStore()

// openType 是变量,方法名必须用中括号取
uni[userStore.openType]({
  url: userStore.redirectUrl
})

要点与易错点

  • 参数不要拼在地址栏传(?redirectUrl=xxx?a=b 会出现两个问号,小程序会把第二个问号后的参数过滤掉),要放进 pinia。
  • 取路径用 currentPage.fullPath(带参数),不要用 route(只有部分路径、不带参数)。
  • 判断 tabBar 页面:拿 currentPage.route 去 tabBarList 里 includes,命中就用 switchTab,否则 navigateTo。
  • uni[userStore.openType] 中括号语法只能用于变量方法名,写成 uni.userStore.openType 是错的。
  • 三个字段都要写进 persist.paths 数组,且必须是字符串,否则刷新页面后数据丢失。
  • 跳转地址前导斜杠 /pages/... 不能丢。

示例三:scroll-page 组件封装 ​

vue
<!-- components/scroll-page/scroll-page.vue -->
<template>
  <scroll-view
    class="scroll-page"
    :scroll-y="true"
    :style="[{ height: windowHeight + 'px' }, { borderBottom: borderStyle }]"
    :refresher-enabled="refresherEnabled"
    :refresher-triggered="refresherTriggered"
    @scrolltolower="onScrollToLower"
    @refresherrefresh="onRefresherRefresh"
  >
    <view class="scroll-page-content">
      <slot />
    </view>
  </scroll-view>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  // 可以是字符串(如 '1px solid #eee'),也可以是 false(不要边框线)
  borderStyle: {
    type: [String, Boolean],
    default: '1px solid #eee'
  },
  // 是否启用下拉刷新
  refresherEnabled: {
    type: Boolean,
    default: false
  },
  // 关闭下拉刷新动画
  refresherTriggered: {
    type: Boolean,
    default: false
  }
})

// 自定义事件,把原生 scroll-view 的事件中转出去
const emits = defineEmits(['scrolltolower', 'refresherrefresh'])

// 页面高度,让组件和屏幕一样高
const { windowHeight } = uni.getSystemInfoSync()

const onScrollToLower = (e) => {
  emits('scrolltolower', e)
}

const onRefresherRefresh = (e) => {
  emits('refresherrefresh', e)
}
</script>

<style lang="scss" scoped>
.scroll-page {
  // 加了边框线后不撑大盒子,否则会出现多余的滚动条
  box-sizing: border-box;
}

.scroll-page-content {
  // 适配底部安全区(刘海屏 / 小黑条)
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
</style>

要点与易错点

  • 本质是对原生 scroll-view 的二次包装,scroll-y 加上即可垂直滚动。
  • 高度用 uni.getSystemInfoSync() 的 windowHeight,拼接时单位 px 不能丢。
  • 加 border-bottom 后必须配 box-sizing: border-box,否则边框宽度会被算进高度,出现多余滚动条。
  • 安全区适配写在内层包裹 slot 的 view 上,写在外层盒子会让整块内容都缩进。
  • 原生事件(scrolltolower、refresherrefresh)要通过 defineEmits 重新抛出,emits('xxx', e) 里事件对象也要传,否则父组件拿不到参数。
  • 下拉刷新需要 refresher-enabled 开启、refresher-triggered 关闭动画,两者配合使用。
  • 调试时临时写死的高度(如 1000px)和背景色,测完一定要删掉,否则组件高度被固定死。

常见错误 ​

  1. 401 判断写在响应拦截器的成功回调里,永远不触发

    • 现象:手动把 token 改错,接口明明返回 401,但页面毫无反应,控制台也没打印。
    • 原因:401 属于请求失败,走的是拦截器第二个回调(error 分支),成功回调里拿不到。
    • 修法:把 console.log(error) 和状态码判断整体搬到 (error) => {} 里,从 error.response.status(或 error.status,看封装层)取状态码,等于 401 时执行重新登录。
  2. 白名单缺失,登录页自己跳自己,形成死循环

    • 现象:token 失效后刷新首页 → 跳到登录页 → 点"获取验证码" → 页面又被刷回登录页,手机号和验证码被清空,怎么都登不进去。
    • 原因:验证码/登录接口本身也带着那个失效 token 发出去,同样返回 401,于是又触发一次跳转登录页。
    • 修法:定义接口白名单数组(获取验证码、账号登录、密码登录),在请求拦截器里判断:只有在"有 token 且不在白名单"时才往 header 里塞 token;同时 401 跳转逻辑保持不动。
  3. 回跳地址用地址栏传参,参数被吃掉

    • 现象:登录成功后回到首页,地址上 ?a=b 这类参数没了,或只回到一个光秃秃的路径。
    • 原因:小程序里 URL 里出现第二个 ? 时,后面的参数会被过滤(H5 无此问题,所以本地测不出来)。
    • 修法:不再用 ?redirectUrl=xxx 传,改为把回跳地址和打开方式放进 Pinia(redirectURL + openType),并加入持久化列表,防止刷新丢失。
  4. 取路径用了 root 而不是 fullPath

    • 现象:回跳后页面参数丢失,或回跳地址和当初进入的地址对不上。
    • 原因:root 只有路径部分,不带 query;fullPath 才是带参数的完整路径。
    • 修法:取页面栈最后一项后,用 fullPath 存回跳地址;root 只用来和白名单/tabBar 路径数组做比对判断打开方式。
  5. 动态调用写成 uni.openType / uni.redirectURL

    • 现象:控制台报 "xxx is not a function",或跳转根本不执行。
    • 原因:方法名和地址都是变量,用点语法访问等于去找一个叫 openType 的固定属性。
    • 修法:变量作属性名一律用中括号:uni[openType]({ url: redirectURL });同时确认 openType 的值是 switchTab / navigateTo / redirectTo 这类真实方法名。
  6. scroll-page 加了边框线后整页多出一条滚动条

    • 现象:给组件加 border-bottom 后,页面出现竖向滚动条,内容被顶出去一点点。
    • 原因:高度已经按 windowHeight 写死,边框宽度被额外算进盒子尺寸,总高超出屏幕。
    • 修法:给滚动容器加 box-sizing: border-box;边框线用 props(borderStyle,支持 boolean 关闭)传入,不要写死颜色和宽度。
  7. 组件里遗留测试代码

    • 现象:页面怎么都滚不动,或高度永远是 1000px。
    • 原因:调试上拉加载时临时写死的 height: 1000px、调试用的背景色没删。
    • 修法:验证完立即删除;高度始终由 uni.getSystemInfoSync().windowHeight 动态给,背景色只在调试时临时加。

练习 ​

  1. 复现并修复 401:把本地 token 手动改成错误值 → 刷新首页 → 确认跳到登录页;在拦截器里打印出状态码来源,说明它为什么在 error 分支。再把 401 判断临时挪到成功回调里,观察"不生效"的具体表现并复原。

  2. 补一条白名单:新增一个"忘记密码-发送短信"接口,把它加入白名单。用错误 token 触发该接口,在网络面板确认请求头里没有 token;再用同样方式请求一个普通业务接口,确认 token 仍在。给出判断条件的完整写法。

  3. 深链回跳验证:从首页带参数进入(如 /pages/index/index?name=xxx),让 token 失效触发跳登录,登录成功后检查:①是否回到带参数的完整地址;②openType 是否为 switchTab;③刷新页面后 Pinia 里两个值是否还在。若参数丢了,定位是哪一步写错了。

  4. tabBar 判定:分别从 tabBar 页面和普通页面进入,打印两次的 root 与 openType,说明判定依据;再故意把路径数组里某一项写错一个字符,观察跳转方式变成什么、会报什么错。

  5. 扩展 scroll-page:把 scrolltolower 和下拉刷新两个原生事件用 defineEmits 抛给父组件,在"我的"页面接收并打印事件对象;同时给组件传一个蓝色 borderStyle,验证既不出现多余滚动条,边框颜色也可自定义。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医咨询-权限验证检测状40107:04
2优医咨询-初始页面中径参数06:42
3优医咨询-获取页面路径11:29
4优医咨询-登录成功跳转10:30
5优医咨询-接口请求白名单08:59
6优医咨询-scroll-page组件介绍06:54
7优医咨询-scroll-page高度和安全区10:58
8优医咨询-scroll-page边框线和事件12:23

小结 ​

  1. 状态码 401 是前后端约定好的"未登录/登录失效"信号,处理位置只能是响应拦截器的失败回调。
  2. 权限校验必须配白名单,否则登录相关接口自身返回 401 会把用户困在登录页,形成死循环。
  3. 回跳地址不要走地址栏传参,小程序对第二个 ? 不兼容;用状态管理(+持久化)承载 redirectURL 与 openType。
  4. 当前页路径从页面栈最后一项取,且要用 fullPath(带参数);root 只用于判断是不是 tabBar 页面。
  5. tabBar 页面必须用 switchTab,普通页面用 navigateTo/redirectTo,所以打开方式必须随地址一起被记录。
  6. 变量作为方法名/属性名时只能用中括号访问,这是本模块跳转能跑通的关键语法点。
  7. scroll-page 的本质是 scroll-view 的二次封装,价值在于:统一处理刘海屏安全区、隐藏滚动条、自定义 tabBar 边框线、复用下拉刷新与上拉加载。
  8. 组件高度动态给、边框宽度用 border-box 兜住、调试代码及时清理——这三点决定组件是否会"莫名其妙多出滚动条"。