主题
团队开发与基础封装(三)
本页对应 团队开发与基础封装,共 7 段视频,约 1 小时 15 分。
学习目标
- 能够对手机号和验证码执行格式校验,并在校验通过后调用登录/注册接口,且能根据返回
code给出失败提示。 - 能够封装并调用
setToken,将普通 token 与 refresh token 同时写入应用实例和小程序本地存储,并通过调试工具验证存储结果。 - 能够使用
getCurrentPages获取来源页面路径,在登录页通过onload接收redirectUrl,登录成功后完成回跳。 - 能够配置请求拦截器,将 token 以
authorization请求头传递,并通过对象合并方式避免覆盖默认请求头。 - 能够调用用户信息接口获取头像和昵称,处理空值默认展示,并将数据渲染到页面中。
- 能够使用
wx.uploadFile上传头像,正确传递文件路径、name、formData和请求头,解析返回的 JSON 字符串并同步更新页面与应用实例中的头像昵称数据。
知识点
公告管理的整体思路
公告管理主要有两部分业务逻辑:一是首页获取公告列表,二是点击公告后查看公告详情。列表接口每次只返回 3 条数据,因此本节不需要处理分页;详情页则需要把公告 ID 通过页面地址参数传过去,再根据 ID 请求详情数据。
说明:你提供的转写稿主要讲登录注册、头像昵称等用户管理内容,与本节“公告管理”不一致。为避免编造,下面不把无关的转写内容硬套到公告业务中,而是依据本节笔记和代码展开;调试部分只写公告业务中可验证的检查步骤。
公告列表:为什么需要、怎么想、怎么做
为什么需要:首页需要展示最新公告,让用户不用进入详情页也能看到标题、简要内容和时间。接口每次只返回 3 条,说明产品目标是“首页摘要展示”,不是完整公告中心,所以不需要做分页、下拉加载等复杂逻辑。
怎么想:页面加载后立即请求公告列表;请求成功后先判断业务状态码 code,再把接口返回的 data 重命名为 notices,通过 setData 渲染到模板。模板中用 navigator 包住每条公告,点击后跳转到详情页。
怎么做:参考代码如下,保留笔记中的写法:
javascript
Page({
onLoad() {
// 获取公告列表数据
this.getNotices()
},
// 调用公告列表接口
async getNotices() {
// 请求接口
const {code, data: notices} await wx.http.get('/announcement')
// 检测接口是否调用成功
if(code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({notices})
}
})模板中渲染数据:
xml
<view class="notices-body">
<navigator
wx:for="{{notices}}"
wx:key="id"
hover-class="none"
url="/pages/notify/index"
class="notice"
>
<view class="notice-title">{{item.title}}</view>
<view class="notice-brief">{{item.content}}</view>
<view class="notice-date">{{item.createdAt}}</view>
</navigator>
</view>易错点:笔记代码中 const {code, data: notices} await ... 缺少赋值号 =,实际应写成 const {code, data: notices} = await ...;await 必须放在 async 函数中;wx.key 使用 id 可以保证列表更新稳定;setData 后模板才会更新,不能只给变量赋值。
公告详情:参数传递与详情请求
为什么需要:列表只展示摘要,用户点击某条公告后需要看到完整标题、作者、时间和富文本内容,因此详情页必须知道“点的是哪一条公告”。
怎么想:列表页跳转时把公告 ID 拼到 URL 上,例如 /pages/notify/index?id=xxx;详情页在 onLoad 生命周期中接收页面参数,解构出 id,再调用 /announcement/:id 获取详情。拿到详情后先判断 code,成功再 setData。
怎么做:参考代码如下:
javascript
Page({
data: {
noticeDetail: {}
},
onLoad({id}) {
// 获取公告详情数据
this.getNoticeDetail(id)
},
// 调用公告详情接口
async getNoticeDetail(id) {
// 检测 id 是否存在
if(typeof id === undefined) return
// 调用接接口
const {code, data: noticeDetail} = await wx.http.get('/announcement/' + id)
// 验证接口调用是否成功
if(code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({noticeDetail})
}
})模板中渲染数据:
xml
<scroll-view enhanced show-scrollbar="{{false}}" scroll-y>
<view class="notify-meta">
<view class="title">{{noticeDetail.title}}</view>
<view class="extra">
<view class="author">{{noticeDetail.creatorName}}</view>
<view class="datetime">{{noticeDetail.createdAt}}</view>
</view>
</view>
<view class="notify-content">
<rich-text nodes="{{noticeDetail.content}}"></rich-text>
</view>
</scroll-view>易错点:typeof id === undefined 的写法不正确,typeof 返回的是字符串,应判断 typeof id === 'undefined',或更直接地写 if (!id) return;列表跳转 URL 要带 ?id=,否则详情页拿不到参数;详情接口路径要拼接 ID;setData 的字段名必须和模板一致,如 noticeDetail。
rich-text:富文本内容必须用专用组件
为什么需要:公告详情内容通常是后端返回的 HTML 富文本。小程序不能像浏览器一样直接把 HTML 字符串塞进普通 view 里解析,必须使用内置组件 rich-text。
怎么想:把富文本字符串赋值给 rich-text 的 nodes 属性,小程序会按支持的节点规则解析展示。
怎么做:
xml
<!-- rich-text 使用示例 -->
<rich-text nodes="{{'<h1>这里的 h1 标签可以被小程序解析</h1>'}}"></rich-text>易错点:不要写成普通文本插值;nodes 接收的是内容数据;富文本标签支持范围受小程序限制,复杂样式不能完全等同于浏览器。
调试与检查步骤
公告列表调试时,先看页面加载后是否发出 /announcement 请求,再检查响应中 code 是否为 10000、data 是否为数组;如果页面空白,优先检查 setData({notices}) 是否执行、模板字段是否写错。详情调试时,先看跳转 URL 是否带 id,再看 onLoad 解构出的 id 是否存在,最后检查详情请求路径和响应中的 noticeDetail 是否成功写入。这样按“参数→请求→状态码→渲染”的顺序排查,能快速定位问题。
登录检测:用 authorization 组件包住页面
为什么需要:项目里大部分页面必须登录后才能访问。如果每个页面都写一遍“读 token、判断、跳登录”,代码会重复且容易漏。
怎么想:把登录检测封装成全局组件 authorization,页面只要用它包住内容即可。组件读取应用实例中的 token:有 token 就通过 <slot> 渲染页面内容;没有就 redirectTo 到登录页,并把当前页面路径作为 redirectURL 带过去,方便登录后回跳。
怎么做:先全局注册组件:
json
{
"usingComponents": {
"authorization": "/components/authorization/index"
}
}页面中作为根节点使用:
xml
<authorization>
<view class="profile">...</view>
</authorization>app.js 启动时读取本地 token:
javascript
App({
onLaunch() {
this.getToken()
},
getToken() {
this.token = wx.getStorageSync('token')
}
})组件内检测登录并记录回跳地址:
xml
<!-- /components/authorization/index.wxml -->
<slot wx:if="{{isLogin}}"></slot>javascript
Component({
data: { isLogin: false },
lifetimes: {
attached() {
const isLogin = !!getApp().token
this.setData({ isLogin })
const pageStack = getCurrentPages()
const currentPage = pageStack.pop()
if (!isLogin) {
wx.redirectTo({
url: '/pages/login/index?redirectURL=/' + currentPage.route
})
}
}
}
})易错点:getCurrentPages() 取到的是页面栈,pop() 拿到当前页面实例;route 不带前导 /,拼 redirectURL 时要手动补上。当前未登录时 token 是 undefined,所以用 !! 转布尔值。
短信验证码:倒计时与手机号校验
为什么需要:获取验证码要防频繁点击,同时手机号、验证码格式必须在发请求前校验,避免无效请求。
怎么想:点击“获取验证码”后显示 Vant 的 van-count-down,通过 change 更新 timeData;倒计时结束再隐藏。表单校验用 wechat-validate:在 rules 中定义规则,validate('mobile') 校验单个字段,validate() 无参校验全部字段。
怎么做:
xml
<text bind:tap="getSMSCode" wx:if="{{!countDownVisible}}">获取验证码</text>
<van-count-down wx:else use-slot time="{{6000}}" bind:change="countDownChange">
<text>{{timeData.seconds}}秒后重新获取</text>
</van-count-down>javascript
Page({
data: {
mobile: '',
code: '',
countDownVisible: false
},
behaviors: [wxValidate],
rules: {
mobile: [
{ required: true, message: '请填写手机号码!' },
{ pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号码!' }
],
code: [
{ required: true, message: '请填写验证码!' },
{ pattern: /^\d{6}$/, message: '请填写正确的验证码!' }
]
},
async getSMSCode() {
const { valid, message } = this.validate('mobile')
if (!valid) return wx.utils.toast(message)
this.setData({ countDownVisible: true })
const { code } = await wx.http.get('/code', { mobile: this.data.mobile })
if (code !== 10000) return wx.utils.toast('发送失败,稍后重试!')
},
countDownChange(ev) {
this.setData({
timeData: ev.detail,
countDownVisible: ev.detail.minutes === 1 || ev.detail.seconds > 0
})
}
})易错点:笔记中 this.set({countDownVisible:true}) 应写成 this.setData。mobile、code 要给初始空字符串,否则校验插件可能把“未填写”误判成“格式不正确”,提示会不对。生产环境短信验证码不会通过接口返回,开发环境才方便取到。
登录&注册:提交验证码、存 token、回跳原页
为什么需要:手机号+验证码提交后,后端判断该手机号是否已注册:已注册执行登录,未注册执行注册。成功后要保存 token,并回到登录前页面。
怎么想:提交前先 this.validate() 全量校验;通过后 POST /login。拿到 token、refreshToken 后,用 app.setToken 同时写入应用实例和本地存储。登录页在 onLoad 接收 redirectURL,成功后 redirectTo 回去。
怎么做:
javascript
App({
setToken(key, token) {
this[key] = token
wx.setStorageSync(key, token)
}
})javascript
const app = getApp()
Page({
onLoad({ redirectURL }) {
this.redirectURL = redirectURL
},
async submitForm() {
if (!this.validate()) return
const { code, data } = await wx.http.post('/login', {
mobile: this.data.mobile,
code: this.data.code
})
if (code !== 10000) return wx.utils.toast('登录失败,稍后重试!')
app.setToken('token', data.token)
app.setToken('refreshToken', data.refreshToken)
wx.redirectTo({ url: this.redirectURL })
}
})易错点:存 token 的方法名是 setToken,不要误写成 getToken;token 和 refreshToken 都要保存。validate() 不传参数时校验全部规则,只要有一个不合法就返回 false。redirectURL 要作为完整页面地址使用。
头像和昵称:请求头、展示、更新与上传
为什么需要:获取用户信息、更新昵称、上传头像都需要登录态。通过请求拦截器统一加 Authorization,避免每个接口手写。
怎么想:拦截器里用 Object.assign 合并默认 header 和自定义 header,避免覆盖;token 前拼 Bearer 。展示信息用 GET /userInfo;昵称用 PUT /userInfo 更新;头像用 wx.uploadFile 上传,注意它不支持 Promise,且 result.data 是 JSON 字符串,要 JSON.parse。上传成功用箭头函数保证 this 指向页面实例。
怎么做:
javascript
http.intercept.request = function (options) {
const defaultHeader = {}
defaultHeader.Authorization = 'Bearer ' + getApp().token
options.header = Object.assign({}, defaultHeader, options.header)
return options
}javascript
Page({
async getUserProfile() {
const { code, data: { avatar, nickName } } = await wx.http.get('/userInfo')
if (code !== 10000) return wx.utils.toast()
this.setData({ avatar, nickName })
getApp().userProfile = { avatar, nickName }
},
getUserNickname(ev) {
this.updateNickname(ev.detail.value)
},
async updateNickname(nickName) {
if (nickName === '') return
const { code } = await wx.http.put('/userInfo', { nickName })
if (code !== 10000) return wx.utils.toast('更新昵称失败!')
const pageStack = getCurrentPages()
pageStack[0].setData({ nickName })
getApp().userProfile.nickName = nickName
},
updateAvatar(avatar) {
wx.uploadFile({
url: wx.http.baseURL + '/upload',
filePath: avatar,
name: 'file',
header: { Authorization: 'Bearer ' + getApp().token },
formData: { type: 'avatar' },
success: (result) => {
const data = JSON.parse(result.data)
if (data.code !== 10000) return wx.utils.toast('上传头像失败!')
this.setData({ avatar: data.data.url })
getCurrentPages()[0].setData({ avatar: data.data.url })
getApp().userProfile.avatar = data.data.url
}
})
}
})易错点:Bearer 后必须有空格;wx.uploadFile 要提前在小程序后台配置 uploadFile 合法域名;success 不要用普通函数,否则 this.setData 会报不存在;更新头像/昵称后要同步当前页、上一页和 app.userProfile,否则返回“我的”页面仍显示旧数据。新用户头像和昵称可能为空,昵称可给“微信用户”默认值,头像可用背景图兜底。
课件图示








代码示例
示例一:公告列表 + 公告详情
pages/index/index.js
javascript
Page({
data: {
notices: []
},
onLoad() {
// 获取公告列表数据
this.getNotices()
},
// 调用公告列表接口
async getNotices() {
// 请求接口
const { code, data: notices } = await wx.http.get('/announcement')
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ notices })
}
})pages/index/index.wxml
xml
<view class="notices-body">
<navigator
wx:for="{{notices}}"
wx:key="id"
hover-class="none"
url="/pages/notify/index?id={{item.id}}"
class="notice"
>
<view class="notice-title">{{item.title}}</view>
<view class="notice-brief">{{item.content}}</view>
<view class="notice-date">{{item.createdAt}}</view>
</navigator>
</view>pages/notify/index.js
javascript
Page({
data: {
noticeDetail: {}
},
onLoad({ id }) {
// 获取公告详情数据
this.getNoticeDetail(id)
},
// 调用公告详情接口
async getNoticeDetail(id) {
// 检测 id 是否存在
if (!id) return
// 调用接口
const { code, data: noticeDetail } = await wx.http.get('/announcement/' + id)
// 验证接口调用是否成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ noticeDetail })
}
})pages/notify/index.wxml
xml
<scroll-view enhanced show-scrollbar="{{false}}" scroll-y>
<view class="notify-meta">
<view class="title">{{noticeDetail.title}}</view>
<view class="extra">
<view class="author">{{noticeDetail.creatorName}}</view>
<view class="datetime">{{noticeDetail.createdAt}}</view>
</view>
</view>
<view class="notify-content">
<rich-text nodes="{{noticeDetail.content}}"></rich-text>
</view>
</scroll-view>关键行与易错点
const { code, data: notices } = await ...:解构重命名,把接口返回的data直接命名成notices,少写一行中间变量;笔记原稿这里漏了=,写成const {code, data: notices} await ...会直接语法报错。data: { notices: [] }必须给初始值:wx:for遍历undefined时页面渲染会报错。wx:key="id":列表项唯一标识,写成wx:key="index"或不写会有性能与状态错乱问题。- 列表跳详情必须把 id 拼到 URL 上:
url="/pages/notify/index?id={{item.id}}",否则详情页onLoad({ id })拿到的是undefined。 - 详情页
onLoad({ id })用的是参数解构,等价于onLoad(options) { const id = options.id }。 - 笔记中的
if (typeof id === 'undefined') return容易写错成typeof id === undefined——typeof返回的是字符串'undefined',拿字符串和undefined比较永远为false,判断直接失效;这里用if (!id) return更稳妥。 <rich-text nodes="{{...}}">是解析富文本的唯一方式,直接把 HTML 字符串写在 WXML 里只会被当成纯文本显示。
示例二:登录检测组件 + 短信验证码 + 登录注册
app.js
javascript
App({
onLaunch() {
// 读取本地存储的 token 数据
this.getToken()
},
getToken() {
// 将 token 数据记到应用实例中
this.token = wx.getStorageSync('token')
},
setToken(key, token) {
// 将 token 记录在应用实例中
this[key] = token
// 将 token 存入本地
wx.setStorageSync(key, token)
}
})components/authorization/index.wxml
xml
<slot wx:if="{{isLogin}}"></slot>components/authorization/index.js
javascript
Component({
data: {
isLogin: false
},
lifetimes: {
attached() {
// 获取登录状态
const isLogin = !!getApp().token
// 变更登录状态
this.setData({ isLogin })
// 获取页面栈
const pageStack = getCurrentPages()
// 获取当前页面路径
const { route } = pageStack.pop()
// 未登录的情况下跳转到登录页面,并携带回跳地址
if (!isLogin) {
wx.redirectTo({
url: '/pages/login/index?redirectURL=/' + route
})
}
}
}
})pages/login/index.js
javascript
// 导入表单验证插件
import wxValidate from 'wechat-validate'
const app = getApp()
Page({
data: {
mobile: '',
code: '',
// 初始状态不启用倒计时组件
countDownVisible: false,
timeData: {}
},
// 将插件注入到页面实例中
behaviors: [wxValidate],
rules: {
mobile: [
{ required: true, message: '请填写手机号码!' },
{ pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号码!' }
],
code: [
{ required: true, message: '请填写验证码!' },
{ pattern: /^\d{6}$/, message: '请填写正确的验证码!' }
]
},
onLoad({ redirectURL }) {
// 获取跳转路径
this.redirectURL = redirectURL
},
// 监听时间的变化
countDownChange(ev) {
this.setData({
// 倒计时当前的时间值
timeData: ev.detail,
// 倒计时结束时关闭组件
countDownVisible: ev.detail.minutes === 1 || ev.detail.seconds > 0
})
},
// 获取短信验证码
async getSMSCode() {
// 获取验证结果
const { valid, message } = this.validate('mobile')
// 如果验证不合法则不再执行后面的逻辑
if (!valid) return wx.utils.toast(message)
// 显示倒计时组件
this.setData({ countDownVisible: true })
// 调用接口获取验证码
const { code } = await wx.http.get('/code', { mobile: this.data.mobile })
// 检测验证码是否发送成功
if (code !== 10000) return wx.utils.toast('发送失败,稍后重试!')
},
// 提交表单数据,完成登录/注册的功能
async submitForm() {
// 验证数据是否合法
if (!this.validate()) return
// 调用登录接口
const { code, data } = await wx.http.post('/login', {
mobile: this.data.mobile,
code: this.data.code
})
// 验证是否登录成功
if (code !== 10000) return wx.utils.toast('登录失败,稍后重试!')
// 记录登录状态
app.setToken('token', data.token)
app.setToken('refreshToken', data.refreshToken)
// 重定向回原来的页面
wx.redirectTo({ url: this.redirectURL })
}
})pages/login/index.wxml
xml
<view class="login">
<van-field model:value="{{mobile}}" placeholder="请输入手机号" />
<van-field model:value="{{code}}" placeholder="请输入验证码">
<text slot="button" bind:tap="getSMSCode" wx:if="{{!countDownVisible}}">获取验证码</text>
<van-count-down wx:else use-slot time="{{6000}}" bind:change="countDownChange">
<text>{{timeData.seconds}}秒后重新获取</text>
</van-count-down>
</van-field>
<van-button type="primary" block bind:tap="submitForm">登录/注册</van-button>
</view>关键行与易错点
!!getApp().token:getStorageSync取不到时返回空字符串,用!!转成布尔值再赋给isLogin。getCurrentPages().pop()取到的是当前页面实例,从里面解构route(不带开头/),拼 URL 时要手动补/。- 组件里必须用
wx.redirectTo而不是navigateTo:登录页不该留在页面栈里,否则用户点返回会回到登录页。 rules里mobile和code必须与data中字段名完全一致,且data里要给初始空字符串——转写稿里踩过这个坑:不给初始值时会话提示变成"请填写正确的手机号码"而不是"请填写手机号码",required规则失效。this.validate('mobile')传参数只校验一个字段;this.validate()不传参数校验全部字段,返回false表示至少有一个不合法。model:value="{{mobile}}"是简易双向绑定,少了model:前缀就取不到输入值。countDownChange里countDownVisible的判断用来在倒计时归零后关闭组件、重新显示"获取验证码"。app.setToken中应用实例属性用this[key] = token中括号写法,因为key是变量,写成this.key只会得到一个名为key的属性。- 转写稿里踩过的坑:
setToken内部误调用了wx.getStorageSync,导致 storage 里没有数据,应该是wx.setStorageSync。
示例三:请求拦截器 + 头像和昵称
utils/http.js
javascript
// 导入 wechat-http 模块
import http from 'wechat-http'
// 配置接口基础路径
http.baseURL = 'https://live-api.itheima.net'
// 配置请求拦截器
http.intercept.request = function (options) {
// 扩展头信息
const defaultHeader = {}
// 身份认证
defaultHeader.Authorization = 'Bearer ' + getApp().token
// 与默认头信息合并
options.header = Object.assign({}, defaultHeader, options.header)
// 处理后的请求参数
return options
}pages/my/index.js(获取并展示头像昵称)
javascript
// 获取应用实例
const app = getApp()
Page({
onLoad() {
// 用户未登录时不必请求
app.token && this.getUserProfile()
},
async getUserProfile() {
// 调用接口获取昵称和头像
const { code, data: { avatar, nickName } } = await wx.http.get('/userInfo')
// 检测接口是否正常返回结果
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ avatar, nickName })
// 将头像和昵称存到应用实例中
app.userProfile = { avatar, nickName }
}
})pages/profile/index.js(更新头像和昵称)
javascript
const pageStack = getCurrentPages()
Page({
onLoad() {
// 获取实例
const app = getApp()
// 将头像和昵称渲染到页面中
this.setData({ ...app.userProfile })
},
// 获取用户昵称
getUserNickname(ev) {
// 更新昵称
this.updateNickname(ev.detail.value)
},
// 更新昵称接口
async updateNickname(nickName) {
if (nickName === '') return
// 调用接口更新用户昵称
const { code } = await wx.http.put('/userInfo', { nickName })
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast('更新昵称失败!')
// 借助于页面栈实例来更新 pages/my/index.wxml 中的昵称
pageStack[0].setData({ nickName })
// 同步数据到应用实例中
const app = getApp()
app.userProfile.nickName = nickName
},
// 获取用户头像
getUserAvatar(ev) {
// 更新用户头像
this.updateAvatar(ev.detail.avatarUrl)
},
// 更新用户头像
updateAvatar(avatar) {
// 调用 API 上传文件
wx.uploadFile({
// 接口地址
url: wx.http.baseURL + '/upload',
// 待上传的文件路径
filePath: avatar,
name: 'file',
header: {
// 用户登录状态
Authorization: 'Bearer ' + getApp().token
},
formData: { type: 'avatar' },
success: (result) => {
// 处理返回的数据
const data = JSON.parse(result.data)
// 检测接口是否调用成功
if (data.code !== 10000) return wx.utils.toast('上传头像失败!')
// 借助于页面栈实例来更新 pages/my/index.wxml 中的头像
pageStack[0].setData({ avatar: data.data.url })
// 更新当前页面的头像预览
this.setData({ avatar: data.data.url })
// 同步更新数据到应用实例中
const app = getApp()
app.userProfile.avatar = data.data.url
}
})
}
})pages/profile/index.wxml
xml
<!-- 需要检测登录状态 -->
<authorization>
<view class="profile">
<van-cell center title="头像">
<van-icon slot="right-icon" name="arrow" size="16" color="#c3c3c5" />
<button
class="button"
size="mini"
hover-class="none"
bind:chooseavatar="getUserAvatar"
open-type="chooseAvatar">
<image class="avatar" src="{{avatar}}"></image>
</button>
</van-cell>
<van-field
center
label="昵称"
input-align="right"
bind:blur="getUserNickname"
type="nickname"
value="{{nickName}}"
placeholder="请输入昵称"
/>
</view>
</authorization>关键行与易错点
- 请求拦截器里
return options不能漏:不返回处理后的参数,请求就发不出去。 - 头信息合并必须用
Object.assign({}, defaultHeader, options.header),直接写options.header.Authorization = ...会把已有的默认头信息覆盖掉。 'Bearer ' + getApp().token中Bearer后的空格不能少,少了服务端解析 token 会失败。app.token && this.getUserProfile():未登录时没有 token,接口必然拿不到数据,用短路提前拦住请求。- 新用户的
avatar/nickName返回的是null,模板里用{{nickName || '微信用户'}}兜底;头像则靠image的背景图兜底,不需要单独处理默认值。 wx.uploadFile不支持 Promise,只能走success回调,不能写await;且必须提前在小程序管理后台把域名加到uploadFile合法域名里。wx.uploadFile不会自动带baseURL,地址要写wx.http.baseURL + '/upload';登录态也得手动在header里补Authorization。JSON.parse(result.data)不能省:上传接口返回的result.data是 JSON 字符串,不是对象,直接取result.data.code会拿到undefined。- 上传成功回调必须写成箭头函数:转写稿里踩过
this.setData is not a function的坑,普通函数里的this不是页面实例。 pageStack[0]是上一个页面(pages/my/index)的实例,用它setData才能让"我的"页面数据同步刷新;同时别忘了同步app.userProfile,否则下次进入完善信息页还是旧数据。<button open-type="chooseAvatar" bind:chooseavatar="...">中事件名全小写,open-type不写就拿不到用户头像;昵称输入框必须type="nickname"才能带出微信昵称。
常见错误
1. 登录成功后回不到原页面
- 现象:从「我的 → 完善信息」被踢到登录页,登录成功后却停在首页或报错。
- 原因:
getCurrentPages()返回的是数组,直接对数组取.route得到undefined;或拼接时漏了前导/。 - 修法:先
const currentPage = pageStack.pop()拿到当前页实例再取route,拼成/pages/login/index?redirectURL=/' + currentPage.route,登录页onLoad({ redirectURL })里存到this上,登录成功后wx.redirectTo({ url: this.redirectURL })。
2. wx.redirectTo 报 "can not redirectTo a tabbar page"
- 现象:登录成功跳转失败,控制台红字。
- 原因:
redirectURL指向了 tabBar 页面(首页 / 我的),redirectTo、navigateTo都不支持 tabBar 页面。 - 修法:跳转前判断目标路径是否在
app.json的tabBar.list中,是则改用wx.switchTab;或限制 authorization 组件只包裹非 tabBar 页面。
3. 上传头像后提示「上传头像失败」,但网络面板显示 code 是 10000
- 现象:
data.code为undefined,code !== 10000恒成立。 - 原因:
wx.uploadFile的success回调里result.data是 JSON 字符串,不是对象。 - 修法:先
const data = JSON.parse(result.data),再取data.code、data.data.url(注意是两层data)。
4. success 回调里 this.setData 报 undefined
- 现象:换头像后控制台报
Cannot read property 'setData' of undefined。 - 原因:
success写成了普通function,this不再指向页面实例。 - 修法:改成箭头函数,或提前
const that = this。
5. 配了请求拦截器后接口 401,或原有默认头信息丢了
- 现象:所有请求都没带上
Authorization;或某些接口自定义头被覆盖。 - 原因:直接
options.header = { Authorization: ... }覆盖了默认头;忘记return options;token 拼接少了空格写成'Bearer' + token。 - 修法:
const defaultHeader = {}→ 赋值 →options.header = Object.assign({}, defaultHeader, options.header)→return options,前缀写成'Bearer '(带空格)。
6. 表单校验提示文不对题
- 现象:什么都没填,却提示「请填写正确的手机号码」。
- 原因:
data里mobile、code没有初始值(undefined),必填规则未按预期触发。 - 修法:
data里显式声明mobile: ''、code: '';同时检查rules的 key 与data字段名严格一致(写成cold之类就直接失效)。
7. token 存不进 storage
- 现象:登录成功,但 Storage 面板里查不到 token。
- 原因:
app.js里方法名定义与调用不一致(如定义了getToken却调用app.setToken),或页面里忘了const app = getApp()。 - 修法:方法统一命名
setToken(key, token),内部this[key] = token+wx.setStorageSync(key, token),调用前先getApp()。
练习
笔记中公告详情页的
if (typeof id === undefined) return永远不成立,请指出原因并改写;再补一条「id 存在但接口 404」的兜底处理。给
authorization组件加白名单:登录页自身不能被拦截,否则会无限重定向。请设计实现方案(白名单从哪来、判断放在哪一步、redirectTo还是navigateTo)。昵称更新后同步到「我的」页面,笔记里用了页面栈
pageStack[0].setData。请再实现onShow重新拉取的方案,并从「数据一致性 / 请求次数 / 适用场景」三个角度做对比,说明各自何时该用。把
wx.uploadFile封装成一个返回 Promise 的uploadFile(filePath, formData),要求:自动拼baseURL、自动带Authorization、自动JSON.parse返回值、失败时 reject。封装后在头像上传处替换原回调写法。给获取验证码按钮加上「防连点」:倒计时期间点击不再发请求,且手机号被修改后倒计时状态如何处置?写出你的判断逻辑。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 享+生活-用户管理(登录&注册)-1 | 13:01 |
| 2 | 享+生活-用户管理(登录&注册)-2 | 06:05 |
| 3 | 享+生活-用户管理(登录&注册)-3 | 07:51 |
| 4 | 享+生活-用户管理(头像和昵称)-1 | 13:02 |
| 5 | 享+生活-用户管理(头像和昵称)-2 | 10:52 |
| 6 | 享+生活-用户管理(头像和昵称)-3 | 15:48 |
| 7 | 享+生活-用户管理(头像和昵称)-4 | 09:04 |
小结
- 登录与注册在后端合一,前端只负责校验并提交
mobile + code,不参与「是登录还是注册」的判断,也不要根据返回值做分支。 - 页面访问控制做成组件(
slot+isLogin)比在每个页面写判断更可维护;但必须处理白名单和 tabBar 跳转两个边界。 - token 采用「应用实例 + 本地存储」双写:内存供全局同步读取,Storage 供冷启动恢复,两者通过
setToken一个入口维护,避免漏写。 - 请求拦截器的价值在于「一次配置、全局生效」,核心是
Object.assign合并不覆盖 +return options不能漏。 validate()不传参即校验全部规则,任一不合法返回false;校验通过才发请求,是表单类页面的固定前置动作。wx.uploadFile的三个坑集中在:不支持 Promise、返回体是字符串、需在小程序后台配置合法域名,封装成 Promise 后可以一次性消掉前两个。- 跨页面数据同步没有唯一解:页面栈
setData适合「返回即刷新」的即时反馈,onShow重拉适合「进入即最新」的一致性优先,全局实例适合多处共享但需手动双向同步。 - 富文本内容必须交给
rich-text的nodes渲染,直接插值只会显示成纯文本。