主题
用户中心与房屋信息(一)
本页对应 用户中心与房屋信息,共 7 段视频,约 1 小时 12 分。
学习目标
- 能够封装统一的 token 保存函数,在登录成功后同时将
token与refreshToken保存到应用实例和本地存储,并在应用初始化时从本地读取。 - 能够基于页面栈获取当前页面路径,在未登录跳转登录页时携带重定向地址,并在登录成功后使用
redirectTo完成页面回跳。 - 能够配置响应拦截器,根据后端业务状态码判断请求成败,失败时展示错误提示并返回
Promise.reject,阻止后续业务代码继续执行。 - 能够在请求拦截器中安全创建并合并请求头,为需要鉴权的接口自动携带
Authorization: Bearer token。 - 能够调用用户信息接口获取头像、昵称等数据并渲染到个人中心页面,在页面显示时刷新最新用户信息。
- 能够通过输入框失焦事件提交昵称修改请求,并在个人信息页完成昵称回显与修改后数据同步更新。
知识点
刷新 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 和合法域名配置,再写代码调用。
怎么做:申请使用腾讯位置服务需要按如下步骤操作:
- 注册账号
- 创建应用
- 生成 key
- 小程序管理后台添加合法域名
步骤参考视频介绍和官方文档操作,在此不赘述。
易错点: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;重新选择后不要只更新地址而忘记刷新周边列表。
课件图示




代码示例
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刷新地址和小区列表,只更新经纬度是不会自动刷新页面的。
常见错误
刷新 token 成功后页面依旧拿不到数据
- 现象:401 之后控制台能看到新 token 已写入,但原接口的数据始终为空/页面不更新。
- 原因:拦截器里只做了「换 token + 存 token」就
return data,原请求从未重发。 - 修法:换完 token 后覆盖
config.header.Authorization(新 token),return http(config)把原请求重打一次。
刷新接口自身返回 401 时陷入死循环
- 现象:网络面板中
/refreshToken被反复请求,最终栈溢出或页面卡死。 - 原因:
/refreshToken的响应也走同一个拦截器,401 分支又去调/refreshToken。 - 修法:进 401 分支先判
config.url.includes('/refreshToken'),命中则直接wx.redirectTo去登录页并return,不再往下走。
- 现象:网络面板中
请求拦截器里给
config.header.Authorization赋值直接报错- 现象:
Cannot set property 'Authorization' of undefined,所有请求失败。 - 原因:该请求库的
config.header默认是undefined,不像 axios 有默认空对象。 - 修法:先兜底成对象再合并,如
config.header = Object.assign({}, config.header, { Authorization: 'Bearer ' + token }),同时保留调用方传入的头信息。
- 现象:
带上的 token 是
Bearer undefined- 现象:请求头里出现
Bearer undefined,接口仍返回 401。 - 原因:
setToken只写了wx.setStorageSync,没同步写应用实例;或onLaunch里没有从本地读回token/refreshToken。 - 修法:
setToken(key, value)内同时写this[key] = value和本地存储;onLaunch中把两份 token 读回实例。
- 现象:请求头里出现
业务失败后代码继续往下跑
- 现象:验证码填错,弹了 toast 之后仍报
Cannot read property 'token' of undefined,甚至页面被错误地跳走。 - 原因:拦截器只
toast没有中断 Promise 链,await之后的代码照常执行。 - 修法:非成功码时
return Promise.reject(new Error(msg)),让调用处的await之后的逻辑不再执行。
- 现象:验证码填错,弹了 toast 之后仍报
登录后无法回到原页面 / 后退能退回空白页
- 现象:登录成功停在登录页;或从登录页后退,回到一个空白/无权限页。
- 原因:跳登录用了
navigateTo(保留历史),且登录页没接redirectURL参数。 - 修法:跳登录用
wx.redirectTo;跳转前用getCurrentPages()取当前页route,补上前导斜杠/,拼成?redirectURL=/xxx传入;登录成功后wx.redirectTo回该地址。
修改昵称后返回个人中心仍是旧数据
- 现象:昵称已改成功,退回「我的」页面头像昵称不变。
- 原因:获取用户信息写在
onLoad,页面被缓存后不会再次执行。 - 修法:改为
onShow中拉取;昵称输入用bindblur而非bindchange,避免每次输入都发请求。
位置服务接口报错 / 无返回
- 现象:
wx.getLocation直接失败,或腾讯地图search、reverseGeocoder返回 key 相关错误。 - 原因:
app.json未声明requiredPrivateInfos与permission.scope.userLocation;或小程序后台未配置 request 合法域名。 - 修法:补齐
app.json配置,并在管理后台添加合法域名后再真机验证(模拟器定位需手动设置)。
- 现象:
练习
- 拦截器补全:把
utils/http.js的响应拦截器改造成「401 → 判断是否 refresh 接口 → 换 token → 存 token → 用新 token 重发原请求」的完整链路,并用断点/日志验证只重发一次。 - 构造 refreshToken 过期场景:手动把本地
refreshToken改成乱码,触发 401,观察是否跳到登录页;再确认登录成功后能回到触发 401 的那个页面(注意redirectURL前导斜杠)。 - 请求头健壮性测试:分别在「有 token」「无 token」「token 为空串」三种情况下发同一个请求,检查
config.header的构造结果,确保不会出现Bearer undefined且不覆盖调用方自定义头。 - 错误链路验证:故意输入错误验证码登录,确认 toast 只弹一次、
await之后的存 token 与跳转代码都不执行;再验证正确验证码时流程完整走通。 - 个人中心数据刷新:把「我的」页用户信息拉取从
onLoad改为onShow,在昵称修改页改完昵称后返回,验证无需手动刷新即可看到新昵称。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 反馈-学习目标 | 08:58 |
| 2 | 用户登录-保存两个token | 07:02 |
| 3 | 用户登录-重定向(页面跳转) | 14:16 |
| 4 | 用户登录-登录失败提醒 | 14:16 |
| 5 | 用户中心-请求拦截器携带token | 15: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,三者缺一都会在真机上失败。