Skip to content

用户中心与房屋信息(一) ​

本页对应 用户中心与房屋信息,共 7 段视频,约 1 小时 12 分。

学习目标 ​

  1. 能够封装统一的 token 保存函数,在登录成功后同时将 token 与 refreshToken 保存到应用实例和本地存储,并在应用初始化时从本地读取。
  2. 能够基于页面栈获取当前页面路径,在未登录跳转登录页时携带重定向地址,并在登录成功后使用 redirectTo 完成页面回跳。
  3. 能够配置响应拦截器,根据后端业务状态码判断请求成败,失败时展示错误提示并返回 Promise.reject,阻止后续业务代码继续执行。
  4. 能够在请求拦截器中安全创建并合并请求头,为需要鉴权的接口自动携带 Authorization: Bearer token。
  5. 能够调用用户信息接口获取头像、昵称等数据并渲染到个人中心页面,在页面显示时刷新最新用户信息。
  6. 能够通过输入框失焦事件提交昵称修改请求,并在个人信息页完成昵称回显与修改后数据同步更新。

知识点 ​

刷新 token 的整体思路 ​

为什么需要:登录后接口会返回 token 和 refreshToken 两份凭证。token 用于日常请求,过期后接口会返回业务状态码 401。如果每个页面都自己判断一次,代码会非常分散,所以更适合在响应拦截器里统一处理。

怎么想:响应拦截器能拿到所有接口的响应数据,因此可以在里面判断 data.code === 401。一旦发现失效,就读取应用实例上的 refreshToken,调用刷新接口换取新的 token 和 refreshToken。

怎么做:先在响应拦截器里判断并读取:

javascript
// utils/http.js
// 配置响应拦截器
http.intercept.response = async function ({ data, config }) {
  // 如果状态码为401,则表明token已失效
  if (data.code === 401) {
    // 获取应用实例来读取 refreshToken
    const app = getApp()
    console.log(app.refreshToken)
  }
  // 只保留data数据,其它的都过滤掉
  return data
}

易错点:这里的 401 是后端返回的业务 code,不是 HTTP 状态码。转写稿里也强调过,接口失败时 HTTP 仍可能是 200 OK,真正要看的是响应体里的 code;正常业务成功一般是 10000。所以不要只判断 HTTP 状态码。

先把 refreshToken 放进应用实例 ​

为什么需要:响应拦截器不是页面,不能直接读页面里的 data。通过 getApp() 读取应用实例,才能稳定拿到全局保存的 refreshToken。

怎么想:小程序启动时先从本地存储读取 token 和 refreshToken,挂到 App 实例上;登录成功后再通过统一的 setToken 同时更新应用实例和本地存储。

怎么做:在 app.js 启动时读取:

javascript
// app.js
App({
  onLaunch() {
    // 读取 token
    this.getToken()
  },
  getToken() {
    // 读取本地的token
    this.token = wx.getStorageSync('token')
    this.refreshToken = wx.getStorageSync('refreshToken')
  },
})

前文已经封装过 setToken,它的作用是动态写入 this[key],并同步 wx.setStorageSync。调试时可以先把本地存储清空,重新登录,观察登录接口返回的两份 token,再确认应用实例和 Storage 里都有值。

易错点:只存本地、不挂到 App 实例,响应拦截器里 app.refreshToken 就会是 undefined;只更新 app.token、不更新本地存储,下次启动又会读到旧值。

调用刷新接口换取新 token ​

为什么需要:token 过期后不能继续用旧值,必须把 refreshToken 发给服务端换新。

怎么想:刷新接口需要的 refreshToken 通过自定义请求头 Authorization 传递,格式是 Bearer 加空格再加 refreshToken。接口文档地址保留如下:
接口文档地址:https://apifox.com/apidoc/shared-8d66c345-7a9a-4844-9a5a-1201852f6faa/api-44946311

怎么做:在响应拦截器中发起刷新请求:

javascript
// utils/http.js
// 配置响应拦截器
http.intercept.response = async function ({ data, config }) {
  // 如果状态码为401,则表明token已失效
  if (data.code === 401) {
    // 获取应用实例
    const app = getApp()
    // 1.1 调用接口获取新的 token
    const res = await http({
      url: '/refreshToken',
      method: 'POST',
      header: {
        Authorization: 'Bearer ' + app.refreshToken,
      },
    })
    // 检测接口是否调用成功
    if (res.code !== 10000) return wx.utils.toast('更新token失败!')
    // 重新存储新的 token
    app.setToken('token', res.data.token)
    app.setToken('refreshToken', res.data.refreshToken)
  }
  // 只保留data数据,其它的都过滤掉
  return data
}

易错点:Authorization 拼写要正确;Bearer 后面必须有空格;refreshToken 不要误写成 reflexToken。调试时可在 Network 里查看刷新请求头是否带上 Bearer xxx,并确认返回 code 是否为 10000。

成功后替换两个 token ​

为什么需要:旧 token 已失效,新的 refreshToken 也可能发生变化,所以两个都要替换。

怎么想:刷新接口成功后,不要只改内存或只改本地,而应调用统一的 app.setToken,把应用实例和本地存储同时更新。

怎么做:上面代码中的 app.setToken('token', res.data.token) 和 app.setToken('refreshToken', res.data.refreshToken) 就是替换动作。刷新失败时提示“更新token失败!”,避免后续业务把失败当成功继续执行。

易错点:只更新 token 不更新 refreshToken,下一次续期还会失败;刷新失败后如果继续走后续 await 代码,可能读取不到 data.token 而报错。转写稿中统一错误处理时用 Promise.reject 阻止后续代码继续执行,这个思路同样适用于刷新失败分支。调试时用 wx.getStorageSync('token') 和 wx.getStorageSync('refreshToken') 检查两个值是否都已替换。

一、无感请求要解决什么问题 ​

为什么需要:实际项目里,用户登录后拿到 token,但调用某个接口时才发现 token 已经过期。此时如果只提示“登录失效”,用户刚才的操作就白做了;如果直接跳登录页,体验也会被打断。怎么想:既然问题出在“原接口已经发出但没拿到数据”,那就在刷新 token 成功后,自动把原接口再调用一次,让用户感觉不到中间发生过失效。怎么做:把判断和重发逻辑放到响应拦截器里,因为拦截器能同时拿到后端返回的 data 和本次请求的 config。易错点:不要在每个页面里单独判断 401;无感请求应统一在请求层处理,否则重复代码多且容易漏。

前面调试业务错误时已经看到:后端失败时 HTTP 状态码仍可能是 200,所以不能只看 HTTP 状态码,要看后端返回的业务 code。同理,token 失效也通过 data.code === 401 判断。

二、在响应拦截器里判断 401 ​

为什么需要:响应拦截器是统一处理“请求回来之后”的逻辑,适合做 token 失效检测。怎么想:先判断业务码是不是 401,是才进入刷新流程;不是则正常返回 data。怎么做:在 http.intercept.response 中接收 { data, config },写 if (data.code === 401)。易错点:拦截器内部拿到的是完整响应体,所以写 data.code;页面里 await 后通常已经取到 data 这一层,位置不要混。

javascript
// 配置响应拦截器
http.intercept.response = async function ({ data, config }) {
  // 如果状态码为401,则表明token已失效
  if (data.code === 401) {
    // 获取应用实例
    const app = getApp()
    // 1.1 调用接口获取新的 token
    const res = await http({
      url: '/refreshToken',
      method: 'POST',
      header: {
        Authorization: 'Bearer ' + app.refreshToken,
      },
    })
    // 检测接口是否调用成功
    if (res.code !== 10000) return wx.utils.toast('更新token失败!')
    // 重新存储新的 token
    app.setToken('token', res.data.token)
    app.setToken('refreshToken', res.data.refreshToken)

    // 1.2 获取到原来接口请求的参数
    config = Object.assign(config, {
      header: {
        // 更新后的 token
        Authorization: 'Bearer ' + res.data.token,
      },
    })
    // 重新发请求
    return http(config)
  }
  // 只保留data数据,其它的都过滤掉
  return data
}

三、刷新 token 与更新存储 ​

为什么需要:刷新接口需要证明“当前用户还有资格换新 token”,所以要用 refreshToken 去请求 /refreshToken。怎么想:前面已经把 token 和 refreshToken 同时存到应用实例和本地存储,因此这里可以直接取 app.refreshToken;刷新成功后要同时更新两份 token。怎么做:调用刷新接口,判断 res.code === 10000,成功后用 app.setToken 更新 token 和 refreshToken。易错点:刷新失败要立即提示并终止,不能继续用旧 token 重发;Bearer 后面有空格;刷新请求本身也要注意不要再次触发无感逻辑造成递归。

四、覆盖旧 Authorization 后重发原请求 ​

为什么需要:原请求的 config 里保存的是旧 token,直接 http(config) 会再次携带失效 token。怎么想:重发前必须把 config.header.Authorization 覆盖成新 token。怎么做:用 Object.assign(config, { header: { Authorization: 'Bearer ' + res.data.token } }) 覆盖,然后 return http(config)。易错点:不要忘记 return,否则原接口调用拿不到重发结果;如果请求拦截器也会统一携带 token,要确保传入的 header 优先级正确,避免新 token 又被旧值覆盖。

javascript
// 获取到原来接口请求的参数
config = Object.assign(config, {
  header: {
    // 更新后的 token
    Authorization: 'Bearer ' + res.data.token,
  },
})

五、调试检查与常见坑 ​

为什么需要:无感请求涉及“原请求→刷新→重发”三段流程,任何一段返回值不对都会表现为页面拿不到数据。怎么想:先看 401 是否进入刷新分支,再看刷新接口是否成功,最后看重发请求是否带新 token。怎么做:在关键位置打印 data.code、res.code、config.header,确认新 token 已存储且已覆盖。易错点:刷新失败仍往下执行会读到不存在的 data 字段并报错;重发时丢掉原请求的 method、data 等配置;只更新内存不更新本地存储;并发多个 401 请求时重复刷新。掌握这些检查点,无感请求才算真正可用。

先判断 refreshToken 是否也失效 ​

刷新 token 完成后,还要继续想一步:如果 refreshToken 也失效了怎么办?比如用户超过 3 天都没有打开过小程序,这时 refreshToken 已经不能用来续期,继续请求普通接口也没有意义,只能让用户重新登录。因此,响应拦截器里遇到 401 后,不能无条件去刷新 token,而要先判断“当前失败的请求是不是 /refreshToken 接口本身”。为什么需要?因为普通接口 401 说明 token 过期,还可以尝试刷新;而 /refreshToken 接口也返回 401,说明刷新凭证也过期了,必须跳登录页。怎么想?在 401 分支里看 config.url,如果包含 /refreshToken,就走重新登录逻辑;否则才走 1.1 的刷新 token 逻辑。怎么做?用 config.url.includes('/refreshToken') 判断。易错点:不要用响应数据里的 data.code 判断接口类型,接口路径在 config.url 里;也不要在刷新失败后继续重发原请求,否则容易循环。

为什么用 redirectTo 而不是 navigateTo ​

确定要重新登录后,下一步是跳转到登录页。这里笔记代码用的是 wx.redirectTo。为什么需要?因为当前页面已经因为未登录/凭证失效不能正常访问,如果用 navigateTo,会把当前页面压入页面栈,用户后退时还可能回到一个空白或不可用的页面。怎么想?登录页是“替换”当前页面,而不是“叠加”一个新页面,所以用重定向更合适。转写稿里也提到:navigateTo 类似 push,会保留历史记录;redirectTo 类似 replace,不保留历史记录。怎么做?调用 wx.redirectTo({ url: '/pages/login/index?redirectURL=/' + redirectURL })。易错点:登录成功后如果还要回到原页面,必须把原页面路径带过去;否则登录完只能回首页,体验不完整。

获取当前页面路径并拼接 redirectURL ​

跳登录页之前,要拿到当前页面路径,保证登录成功后能跳回原来页面。为什么需要?用户可能是在某个二级页面发现登录失效,登录后应该回到刚才的位置。怎么想?用 getCurrentPages() 获取页面栈,它是一个数组,当前页面在最后一项;取出当前页实例后,通过 route 拿到页面路径。怎么做?笔记代码里是:

javascript
const pageStack = getCurrentPages()
const currentPage = pageStack.pop()
const redirectURL = currentPage.route
return wx.redirectTo({
  url: '/pages/login/index?redirectURL=/' + redirectURL,
})

易错点:route 通常不带前导斜杠,所以拼接时要补 /,否则跳转地址可能不完整。转写稿调试时也专门提醒“这个斜杠不能省”。另外,pop() 会改变数组,但这里只是取当前页,按笔记代码保留即可。

完整代码与调试过程 ​

把上面的判断放回响应拦截器,整体代码如下:

javascript
// 配置响应拦截器
http.intercept.response = async function ({ data, config }) {
  // 如果状态码为401,则表明token已失效
  if (data.code === 401) {
    // 获取应用实例
    const app = getApp()
    // 1.3 状态为 401 且接口为 /refreshToken 表明 refreshToken 也过期了
    if (config.url.includes('/refreshToken')) {
      // 获取当前页面的路径,保证登录成功后能跳回到原来页面
      const pageStack = getCurrentPages()
      const currentPage = pageStack.pop()
      const redirectURL = currentPage.route
      // 跳由跳转(登录页面)
      return wx.redirectTo({
        url: '/pages/login/index?redirectURL=/' + redirectURL,
      })
    }
    // 1.1 调用接口获取新的 token
    const res = await http({
      url: '/refreshToken',
      method: 'POST',
      header: {
        Authorization: 'Bearer ' + app.refreshToken,
      },
    })
    // 检测接口是否调用成功
    if (res.code !== 10000) return wx.utils.toast('更新token失败!')
    // 重新存储新的 token
    app.setToken('token', res.data.token)
    app.setToken('refreshToken', res.data.refreshToken)
    
    // 1.2 获取到原来接口请求的参数
    config = Object.assign(config, {
      header: {
        // 更新后的 token
        Authorization: 'Bearer ' + res.data.token,
      },
    })
    // 重新发请求
    return http(config)
  }
  // 只保留data数据,其它的都过滤掉
  return data
}

调试时可以按转写稿思路做:先确认 401 分支能进;再打印 config.url,确认普通接口和 /refreshToken 能区分;然后打印 getCurrentPages() 观察页面栈,确认当前页 route 的形态;最后测试重定向到登录页后,后退不会回到不可用页面。易错点:/refreshToken 的判断必须放在 401 分支内部;跳登录页后要 return,不要继续执行刷新逻辑;redirectURL 拼接时别漏斜杠。这样,refreshToken 失效后的重新登录流程就闭环了。

位置服务与准备工作 ​

位置服务(LBS)是基于用户位置来提供服务的技术,通常要配合第三方服务实现,例如腾讯地图、高德地图、百度地图等。本项目采用腾讯的位置服务。

为什么需要:只靠小程序自身很难完成地址搜索、逆地址解析等能力,接入成熟的地图服务可以减少开发量。
怎么想:先完成账号、应用、Key 和合法域名配置,再写代码调用。
怎么做:申请使用腾讯位置服务需要按如下步骤操作:

  1. 注册账号
  2. 创建应用
  3. 生成 key
  4. 小程序管理后台添加合法域名

步骤参考视频介绍和官方文档操作,在此不赘述。
易错点:Key 要使用自己申请的;合法域名未配置时,相关请求会失败。

声明位置权限 ​

为什么需要:获取用户位置属于敏感能力,小程序要求先声明并申请。
怎么想:在 app.json 中声明 requiredPrivateInfos 和 permission,再到小程序管理后台申请。
怎么做:

json
{
  "requiredPrivateInfos": [
    "getLocation",
    "chooseLocation"
  ],
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
   }
}

相关限制请参考文档说明。
易错点:只写代码不声明、不申请,接口可能无法正常调用;desc 要写清楚用途。

获取当前位置 ​

为什么需要:周边搜索、逆地址解析都需要经纬度。
怎么想:先用小程序 API 拿到 latitude、longitude,再交给后续业务。
怎么做:调用 wx.getLocation() 获取用户当前位置,该 API 支持返回 Promise:

javascript
Page({
  async getLocation() {
    // 调用 API
    const {latitude, longitude} = await wx.getLocation()
    console.log(latitude, longitude)
  }
})

易错点:调试时先确认控制台能打印经纬度,再继续写搜索逻辑;不要跳过权限声明。

让用户选择位置 ​

为什么需要:用户可能不在当前位置,需要手动选择。
怎么想:调用 wx.chooseLocation 打开地图,用户选完后返回该位置经纬度。
怎么做:该 API 支持返回 Promise:

javascript
Page({
  async chooseLocation() {
    // 调用小程序 API 获取新的位置
    const {latitude, longitude} = await wx.chooseLocation()
		console.log(latitude, longitude)
  },
})

易错点:方法里用了 await,外层函数要写成 async;笔记示例中该方法缺少 async,实际编写要补上。

导入腾讯位置服务 SDK ​

为什么需要:官方 SDK 封装了搜索、逆地址解析等方法,避免重复造轮子。
怎么想:下载微信小程序 JavaScript SDK,放到项目中统一实例化。
怎么做:导入位置服务微信小程序 Javascript SDK,它是官方封装好的一个 .js 文件:

javascript
// utils/qqmap.js
// 导入腾讯位置服务 SDK
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用个人申请的 key)
export default new QQMapWX({
  key: '填写自已的 KEY',
})

易错点:路径要按项目实际目录调整;Key 不能照抄示例。

搜索周边小区 ​

为什么需要:项目要展示附近社区。
怎么想:先取经纬度,再调用 SDK 的 search,最后过滤并渲染数据。
怎么做:调用 SDK 提供的方法 search 实现搜索周边小区:

javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'

Page({
  onLoad() {
    // 获取用户经纬度
    this.getLocation()
  },
  async getLoaction() {
    // 调用小和序 API 获取用户位置
    const { latitude, longitude } = await wx.getLocation()
    // 获取周边小区
    this.getPoint(latitude, longitude)
  },
  getPoint(latitude, longitude) {
    // search 是实现地点搜索功能的方法
    QQMap.search({
      keyword: '住宅小区', //搜索关键词
      location: [latitude, longitude].join(','), //设置周边搜索中心点
      page_size: 5,
      success: (result) => {
        // 过滤掉多余的数据
        const points = result.data.map(({ id, title, _distance }) => {
          return { id, title, _distance }
        })
        // 渲染数据
        this.setData({ points })
      },
    })
  }
})

将获取的小区信息渲染到页面上:

xml
<!-- 检测用户登录状态 -->
<authorization>
  <view class="locate">
    <van-cell-group border="{{false}}" title="当前地点">
      ...
    </van-cell-group>
    <van-cell-group border="{{false}}" title="附近社区">
      <van-cell
        wx:for="{{points}}"
        wx:key="id"
        title="{{item.title}}"
        link-type="navigateTo"
        url="/house_pkg/pages/building/index"
        is-link
      />
    </van-cell-group>
  </view>
</authorization>

易错点:location 要用 经纬度拼接成字符串;this.getLoaction 在笔记代码中有拼写错误,实际应与 onLoad 调用名一致;调试时先确认 result.data 有值再渲染。

逆地址解析 ​

为什么需要:经纬度不直观,页面需要展示具体地址。
怎么想:用 SDK 的 reverseGeocoder 根据经纬度换地址,再 setData。
怎么做:调用 reverseGeocoder 实现逆地址解析,逆地址解析是指根据经纬度来获取具体的地址信息:

javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'
Page({
  // ...
  getPoint(latitude, longitude) {
    // 逆地址解析(根据经纬度来获取地址)
    QQMap.reverseGeocoder({
      location: [latitude, longitude].join(','),
      success: ({ result: { address } }) => {
        // 数据数据
        this.setData({ address })
      },
    })
    
    // search 是实现地点搜索功能的方法
    QQMap.search({
      keyword: '住宅小区', //搜索关键词
      location: [latitude, longitude].join(','), //设置周边搜索中心点
      page_size: 5,
      success: (result) => {
        // 过滤掉多余的数据
        const points = result.data.map(({ id, title, _distance }) => {
          return { id, title, _distance }
        })
        // 渲染数据
        this.setData({ points })
      },
    })
  }
})

易错点:success 回调参数结构要按文档取;this.setData({ address }) 中字段名要与 wxml 对应。

重新选择位置并刷新 ​

为什么需要:用户换位置后,地址和附近小区都要更新。
怎么想:先 chooseLocation 拿新经纬度,再复用 getPoint。
怎么做:

javascript
// 导入腾讯位置服务
import QQMap from '../../../utils/qqmap'
Page({
  // ...
	chooseLocation() {
    // 调用小程序 API 获取新的位置
    const { latitude, longitude } = await wx.chooseLocation()
    // 获取新的位置附近的小区
    this.getPoint(latitude, longitude)
  },
  getPoint(latitude, longitude) {
    // ...
  }
})

易错点:该方法使用 await,要补 async;重新选择后不要只更新地址而忘记刷新周边列表。

课件图示 ​

课件第5页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第6页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第3页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

课件第4页:多⼀句没有,少⼀句不⾏,用更短时间,教会更实用的技术! ⾼级软件⼈才培训专家 l

代码示例 ​

1. app.js —— 双 token 的存取(全局 + 本地持久化) ​

javascript
// app.js
App({
  onLaunch() {
    // 启动时先读取本地存储,挂到应用实例上,供 getApp() 随时读取
    this.getToken()
  },

  // 读取 token / refreshToken
  getToken() {
    this.token = wx.getStorageSync('token')
    this.refreshToken = wx.getStorageSync('refreshToken')
  },

  // 统一的 token 存储方法:key 是动态的,所以用中括号语法
  setToken(key, value) {
    // 1. 存到应用实例(内存中,方便直接访问)
    this[key] = value
    // 2. 持久化到本地存储
    wx.setStorageSync(key, value)
  },
})
  • 关键行 this[key] = value:key 是动态参数,必须用中括号(对象属性插值),写成 this.key 只会得到一个名为 key 的属性。
  • 关键行 getToken():getApp().refreshToken 能读到值的前提是启动时已经从本地存储同步到实例上,否则刷新 token 时拿到的是 undefined。
  • 易错点:登录成功后要调用两次 app.setToken('token', ...) 和 app.setToken('refreshToken', ...),两个都存,缺一个后续续期就失败。
  • 易错点:存完本地存储不代表实例已更新,setToken 里必须两边都写。

2. utils/http.js —— 请求拦截器携带 token + 响应拦截器刷新/无感请求/重新登录/统一报错 ​

javascript
// utils/http.js

// ========== 请求拦截器:统一携带 token ==========
http.intercept.request = function (config) {
  // 易错:此时 config.header 默认是 undefined,直接 config.header.Authorization = ... 会报错
  config.header = Object.assign(
    {
      // 注意 Bearer 后面有一个空格
      Authorization: 'Bearer ' + getApp().token,
    },
    // 调用方自己传了 header 就保留,并覆盖上面的默认值
    config.header
  )
  // 配置一定要 return 出去
  return config
}

// ========== 响应拦截器 ==========
http.intercept.response = async function ({ data, config }) {
  // 1. token 失效
  if (data.code === 401) {
    const app = getApp()

    // 1.3 refreshToken 也过期了:说明请求的是 /refreshToken 且仍返回 401
    if (config.url.includes('/refreshToken')) {
      // 拿到当前页面路径,保证登录成功后能跳回来
      const pageStack = getCurrentPages()
      const currentPage = pageStack.pop()
      const redirectURL = currentPage.route
      return wx.redirectTo({
        // route 不带前导斜杠,这里要手动补上
        url: '/pages/login/index?redirectURL=/' + redirectURL,
      })
    }

    // 1.1 用 refreshToken 换取新的 token 和 refreshToken
    const res = await http({
      url: '/refreshToken',
      method: 'POST',
      header: {
        Authorization: 'Bearer ' + app.refreshToken,
      },
    })
    if (res.code !== 10000) return wx.utils.toast('更新token失败!')

    // 重新存储新的 token(覆盖已失效的旧值)
    app.setToken('token', res.data.token)
    app.setToken('refreshToken', res.data.refreshToken)

    // 1.2 无感请求:原 config 里带的是旧 token,必须用新 token 覆盖
    config = Object.assign(config, {
      header: {
        Authorization: 'Bearer ' + res.data.token,
      },
    })
    // 把原来那次请求重新发一次,用户感知不到 token 曾经失效
    return http(config)
  }

  // 统一业务错误处理:code 不是 10000 就提示
  if (data.code !== 10000) {
    wx.utils.toast(data.message || '出现错误!')
    // 主动返回失败的 Promise,避免 await 后面的代码继续执行
    return Promise.reject(data)
  }

  // 只保留 data 数据,其它的都过滤掉
  return data
}
  • 关键行 config.header = Object.assign({ ... }, config.header):拦截器里 header 初始为 undefined,这是和 axios 最大的差异(axios 有默认值),直接点语法赋值必崩。
  • 关键行 return http(config):这就是“无感请求”的核心——刷新成功后拿原 config 重发一次,调用方拿到的就是正常数据。
  • 易错点:重发前必须用新 token 覆盖 config.header.Authorization,否则重发的还是旧 token,会再次 401 死循环。
  • 易错点:config.url.includes('/refreshToken') 的判断必须放在最前面,否则 /refreshToken 返回 401 时会被当成普通接口再去刷新,形成死循环。
  • 易错点:getCurrentPages().pop() 拿到的 route 不带 /,拼 redirectURL 时一定要补上。
  • 易错点:错误分支里必须 Promise.reject,否则 await 后面的“存 token、跳页面”等代码仍会执行,报 Cannot read property 'token' of undefined。
  • 易错点:Bearer 后必须有空格,否则后端解析不出 token。

3. 位置服务 —— 腾讯地图 SDK 获取当前位置与周边小区 ​

javascript
// utils/qqmap.js
// 导入腾讯位置服务微信小程序 SDK(官方封装好的 .js 文件)
import QQMapWX from '../libs/qqmap-wx-jssdk'
// 实例化位置服务(使用自己申请的 key)
export default new QQMapWX({
  key: '填写自已的 KEY',
})
javascript
// pages/index/index.js
import QQMap from '../../../utils/qqmap'

Page({
  data: {
    address: '',
    points: [],
  },

  onLoad() {
    this.getLocation()
  },

  // 获取用户当前经纬度
  async getLocation() {
    // wx.getLocation 支持 Promise 调用
    const { latitude, longitude } = await wx.getLocation()
    this.getPoint(latitude, longitude)
  },

  // 用户重新选择位置
  async chooseLocation() {
    const { latitude, longitude } = await wx.chooseLocation()
    this.getPoint(latitude, longitude)
  },

  getPoint(latitude, longitude) {
    // 逆地址解析:根据经纬度拿到具体地址
    QQMap.reverseGeocoder({
      location: [latitude, longitude].join(','),
      success: ({ result: { address } }) => {
        this.setData({ address })
      },
    })

    // 搜索周边小区
    QQMap.search({
      keyword: '住宅小区', // 搜索关键词
      location: [latitude, longitude].join(','), // 周边搜索中心点
      page_size: 5,
      success: (result) => {
        // 过滤掉多余的数据
        const points = result.data.map(({ id, title, _distance }) => {
          return { id, title, _distance }
        })
        this.setData({ points })
      },
    })
  },
})
json
// app.json 中必须先声明,否则获取位置接口不可用
{
  "requiredPrivateInfos": ["getLocation", "chooseLocation"],
  "permission": {
    "scope.userLocation": {
      "desc": "你的位置信息将用于小程序位置接口的效果展示"
    }
  }
}
  • 关键行 location: [latitude, longitude].join(','):SDK 要求的是 "纬度,经度" 的字符串,顺序不能颠倒。
  • 关键行 result.data.map(({ id, title, _distance }) => ...):SDK 返回的数据字段多,只挑渲染需要的三个,避免 setData 传大对象影响性能。
  • 易错点:wx.getLocation、wx.chooseLocation 必须在 app.json 的 requiredPrivateInfos 中声明,并且在小程序管理后台申请权限,否则真机上直接失败。
  • 易错点:reverseGeocoder 和 search 是回调风格(success),不能 await,this 要用箭头函数保证指向 Page 实例。
  • 易错点:await wx.getLocation() 依赖小程序基础库的 Promise 化支持,若编译报错需在开发者工具中开启“增强编译”。
  • 易错点:chooseLocation 返回后要重新调用 getPoint 刷新地址和小区列表,只更新经纬度是不会自动刷新页面的。

常见错误 ​

  1. 刷新 token 成功后页面依旧拿不到数据

    • 现象:401 之后控制台能看到新 token 已写入,但原接口的数据始终为空/页面不更新。
    • 原因:拦截器里只做了「换 token + 存 token」就 return data,原请求从未重发。
    • 修法:换完 token 后覆盖 config.header.Authorization(新 token),return http(config) 把原请求重打一次。
  2. 刷新接口自身返回 401 时陷入死循环

    • 现象:网络面板中 /refreshToken 被反复请求,最终栈溢出或页面卡死。
    • 原因:/refreshToken 的响应也走同一个拦截器,401 分支又去调 /refreshToken。
    • 修法:进 401 分支先判 config.url.includes('/refreshToken'),命中则直接 wx.redirectTo 去登录页并 return,不再往下走。
  3. 请求拦截器里给 config.header.Authorization 赋值直接报错

    • 现象:Cannot set property 'Authorization' of undefined,所有请求失败。
    • 原因:该请求库的 config.header 默认是 undefined,不像 axios 有默认空对象。
    • 修法:先兜底成对象再合并,如 config.header = Object.assign({}, config.header, { Authorization: 'Bearer ' + token }),同时保留调用方传入的头信息。
  4. 带上的 token 是 Bearer undefined

    • 现象:请求头里出现 Bearer undefined,接口仍返回 401。
    • 原因:setToken 只写了 wx.setStorageSync,没同步写应用实例;或 onLaunch 里没有从本地读回 token/refreshToken。
    • 修法:setToken(key, value) 内同时写 this[key] = value 和本地存储;onLaunch 中把两份 token 读回实例。
  5. 业务失败后代码继续往下跑

    • 现象:验证码填错,弹了 toast 之后仍报 Cannot read property 'token' of undefined,甚至页面被错误地跳走。
    • 原因:拦截器只 toast 没有中断 Promise 链,await 之后的代码照常执行。
    • 修法:非成功码时 return Promise.reject(new Error(msg)),让调用处的 await 之后的逻辑不再执行。
  6. 登录后无法回到原页面 / 后退能退回空白页

    • 现象:登录成功停在登录页;或从登录页后退,回到一个空白/无权限页。
    • 原因:跳登录用了 navigateTo(保留历史),且登录页没接 redirectURL 参数。
    • 修法:跳登录用 wx.redirectTo;跳转前用 getCurrentPages() 取当前页 route,补上前导斜杠 /,拼成 ?redirectURL=/xxx 传入;登录成功后 wx.redirectTo 回该地址。
  7. 修改昵称后返回个人中心仍是旧数据

    • 现象:昵称已改成功,退回「我的」页面头像昵称不变。
    • 原因:获取用户信息写在 onLoad,页面被缓存后不会再次执行。
    • 修法:改为 onShow 中拉取;昵称输入用 bindblur 而非 bindchange,避免每次输入都发请求。
  8. 位置服务接口报错 / 无返回

    • 现象:wx.getLocation 直接失败,或腾讯地图 search、reverseGeocoder 返回 key 相关错误。
    • 原因:app.json 未声明 requiredPrivateInfos 与 permission.scope.userLocation;或小程序后台未配置 request 合法域名。
    • 修法:补齐 app.json 配置,并在管理后台添加合法域名后再真机验证(模拟器定位需手动设置)。

练习 ​

  1. 拦截器补全:把 utils/http.js 的响应拦截器改造成「401 → 判断是否 refresh 接口 → 换 token → 存 token → 用新 token 重发原请求」的完整链路,并用断点/日志验证只重发一次。
  2. 构造 refreshToken 过期场景:手动把本地 refreshToken 改成乱码,触发 401,观察是否跳到登录页;再确认登录成功后能回到触发 401 的那个页面(注意 redirectURL 前导斜杠)。
  3. 请求头健壮性测试:分别在「有 token」「无 token」「token 为空串」三种情况下发同一个请求,检查 config.header 的构造结果,确保不会出现 Bearer undefined 且不覆盖调用方自定义头。
  4. 错误链路验证:故意输入错误验证码登录,确认 toast 只弹一次、await 之后的存 token 与跳转代码都不执行;再验证正确验证码时流程完整走通。
  5. 个人中心数据刷新:把「我的」页用户信息拉取从 onLoad 改为 onShow,在昵称修改页改完昵称后返回,验证无需手动刷新即可看到新昵称。

原视频对照 ​

#视频时长
1反馈-学习目标08:58
2用户登录-保存两个token07:02
3用户登录-重定向(页面跳转)14:16
4用户登录-登录失败提醒14:16
5用户中心-请求拦截器携带token15:10
6用户中心-修改用户昵称09:36
7用户中心-用户昵称回显03:34

小结 ​

  • 401 处理必须闭环三件事:换 token、存 token(实例 + 本地)、用新 token 重发原请求,缺一就会出现「刷新成功但页面无数据」。
  • 拦截器里判断 refreshToken 自身失效是防止死循环的唯一闸门,必须写在换 token 之前。
  • 拦截器内部拿的是 data.code,页面里拿的是 res.code,层级写错会静默失效。
  • 该请求库的 config.header 默认不存在,必须先兜底成对象再合并,并保留调用方传入的头信息;Bearer 后的空格不能丢。
  • 业务失败要 Promise.reject 主动中断,否则后续代码会读到 undefined 造成二次报错甚至误跳转。
  • 登录跳转统一用 redirectTo 且携带 redirectURL,route 取回后要补前导斜杠,登录成功后再 redirectTo 回跳。
  • 会被其他页面修改的数据,拉取时机用 onShow 而不是 onLoad;表单类的「提交时机」优先 blur 而非 change。
  • 位置服务属于「配置先行」的能力:app.json 声明 + 后台合法域名 + 个人 key,三者缺一都会在真机上失败。