主题
团队开发准备与登录(三)
本页对应 团队开发准备与登录,共 7 段视频,约 1 小时 22 分。
学习目标
- 能使用微信开发者工具创建自定义组件并生成四件套文件,在
app.json的usingComponents中正确注册全局组件,并在页面模板中引用验证其可正常渲染。 - 能通过
<slot>接收页面传入的结构,结合组件生命周期(lifetimes.attached)读取 token 并做布尔转换,实现「有 token 才渲染插槽内容、无 token 不渲染」的鉴权效果。 - 能使用
wx.getStorageSync在组件内动态读取持久化 token,并在未登录时调用wx.redirectTo跳转登录页,说明redirectTo与navigateTo在页面栈保留与销毁上的区别及选择理由。 - 能基于 Vant 小程序版
field组件改造短信验证码结构(maxlength、type="number"、use-button-slot、count-down倒计时插槽),并通过bind:change提取秒数、用布尔变量控制倒计时启停与按钮文案切换。 - 能安装并构建
async-validator(npm install+ 工具「构建 npm」),在页面中引入behavior与rules编写手机号、验证码的必填与正则规则,调用this.validate完成单项/全表单校验,并在校验失败时给出提示、通过后才发送请求。 - 能通过
model:value完成手机号与验证码的双向绑定,收集数据后调用登录接口,登录成功后使用getApp()将 token 同时写入全局对象与本地存储,实现 token 的内存访问与持久化双写。
知识点
公告管理的两个业务点
公告管理这一节主要处理两件事:首页获取公告列表,以及点击公告后查看详情。列表接口每次只返回 3 条数据,因此笔记明确说明“不需要处理分页”。这意味着我们不要把列表页写成完整的分页列表,而应把它当作首页的一个小模块:进入页面时请求一次,拿到数据后直接渲染。
这里先说明一点:转写稿记录的是登录检测、鉴权组件、短信验证码和登录保存 token,并不是公告管理本身。不能把登录模块的业务硬套到公告管理上。可迁移的调试思路是:先让接口按正确参数跑通,再补错误提示和边界处理;先跑通正确业务,再统一处理失败情况。下面只依据本节笔记和接口代码展开。
公告列表:先请求,再判断,再渲染
为什么需要:首页需要展示最新公告,用户进入首页就应看到标题、简要内容和时间。因为接口只返回 3 条,所以不需要做分页、上拉加载等复杂逻辑。
怎么想:页面加载时触发请求;请求成功后先判断业务状态码 code 是否为 10000;成功则把 data 中的公告数组赋给 data.notices,模板用 wx:for 渲染。
怎么做:在 onLoad 中调用 getNotices。笔记参考代码如下,注意原笔记中解构赋值处漏写了 =,实际应写成 const { code, data: notices } = await wx.http.get('/announcement'):
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 })
}
})接口文档地址保留如下:公告列表接口文档
易错点:第一,await 解构赋值不能漏 =;第二,data: notices 是给返回数据起别名,别误写成 data.notices;第三,setData 的 key 必须与模板中的 notices 一致;第四,失败时不要继续渲染空数据,应先 return。
公告列表模板:字段名和跳转地址要对应
模板中遍历 notices,用 wx:key="id" 保证列表更新稳定。每一项用 navigator 包起来,点击后跳到公告详情页。笔记中的模板如下:
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>为什么需要:列表只展示标题、简要内容、创建时间,点击后进入详情页,符合“列表—详情”的常见信息架构。
易错点:wx:key 不要用 index 代替唯一 id;字段名要和接口返回一致,如 createdAt、title、content;navigator 的 url 要写目标页面真实路径。
公告详情:通过地址参数传 id
为什么需要:详情页需要知道用户点的是哪一条公告,因此列表跳转时应把公告 id 带到详情页。笔记中列表的 url 只写了 /pages/notify/index,实际业务中应把 id 作为地址参数拼上,例如 /pages/notify/index?id={{item.id}}。这一点是根据“点击公告列表后将公告的ID通过地址参数传递”这一业务要求推导出的必要做法。
怎么想:详情页在 onLoad 中接收页面参数,取出 id;先判断 id 是否存在,再请求 /announcement/{id};成功后把详情对象赋给 noticeDetail。
怎么做:参考代码如下。注意原笔记中 if(typeof id === undefined) 写法不正确,typeof 返回字符串,应改为 if (!id) return 或 if (typeof id === 'undefined') return:
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 })
}
})接口文档地址保留如下:公告详情接口文档
易错点:onLoad({ id }) 依赖页面参数名必须是 id;拼接 URL 时不要漏斜杠;typeof id === undefined 永远为假,因为 typeof 的结果是字符串 'undefined';noticeDetail 初始值写成空对象,模板访问 noticeDetail.title 时才不会报错。
详情模板与 rich-text 解析富文本
详情模板用 scroll-view 承载滚动,标题、作者、时间、正文分别渲染。笔记模板如下:
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>为什么需要:公告正文通常是富文本,包含 HTML 标签。小程序不能像浏览器一样直接解析 HTML,必须用内置组件 rich-text,把富文本内容赋给 nodes 属性。
xml
<!-- rich-text 使用示例 -->
<rich-text nodes="{{'<h1>这里的 h1 标签可以被小程序解析</h1>'}}"></rich-text>易错点:不要把富文本直接放进普通 view 里;nodes 绑定的是内容字段,不是整个详情对象;如果详情接口返回字段名不是 content,要按接口实际字段调整。调试时先确认列表能渲染,再确认详情 id 是否传对,最后检查 rich-text 是否拿到内容。
2.4.1 登录检测:用 authorization 组件包住页面
为什么需要:项目中很多页面必须登录后才能访问。小程序没有 Vue 那种现成的路由守卫,因此要把“是否登录”的判断封装到组件里,页面用组件包起来即可。
怎么想:转写稿里说的“健全”应理解为“鉴权”。思路是:组件内部读取 token,有 token 才通过 <slot> 渲染页面内容;没有则跳转登录页,并把当前页面路径带到 redirectURL,登录成功后再回来。
怎么做:先全局注册组件,再在页面根节点使用:
json
{
"usingComponents": {
"authorization": "/components/authorization/index"
}
}xml
<authorization>
<view class="profile">...</view>
</authorization>组件用插槽控制渲染:
xml
<!-- /components/authorization/index.wxml -->
<slot wx:if="{{isLogin}}"></slot>js
// app.js
App({
onLaunch() { this.getToken() },
getToken() { this.token = wx.getStorageSync('token') },
setToken(key, token) {
this[key] = token
wx.setStorageSync(key, token)
}
})js
// /components/authorization/index.js
Component({
data: { isLogin: false },
lifetimes: {
attached() {
const isLogin = !!getApp().token
this.setData({ isLogin })
if (!isLogin) {
const currentPage = getCurrentPages().pop()
wx.redirectTo({
url: '/pages/login/index?redirectURL=/' + currentPage.route
})
}
}
}
})易错点:调试时可先把 isLogin 写死为 true/false 验证插槽;token 要持久化到本地存储;未登录用 redirectTo 关闭当前页,登录后重新加载页面,而不是用 navigateTo 保留旧页面。
2.4.2 短信验证码:倒计时与表单校验
为什么需要:点击“获取验证码”后要防止重复发送,同时手机号格式必须正确,否则请求没有意义。
怎么想:用 Vant 的 van-count-down 做倒计时;用 countDownVisible 控制显示“获取验证码”还是倒计时;用 wechat-validate 做表单校验,先校验再发请求。转写稿里的“VchaHTB”即 wx.http,“别还”即 behaviors。
怎么做:
xml
<text bind:tap="getSMSCode" wx:if="{{!countDownVisible}}">获取验证码</text>
<van-count-down wx:else use-slot time="{{60000}}" bind:change="countDownChange">
<text>{{timeData.seconds}}秒后重新获取</text>
</van-count-down>js
import wxValidate from 'wechat-validate'
Page({
data: { mobile: '', countDownVisible: false, timeData: {} },
behaviors: [wxValidate],
rules: {
mobile: [
{ required: true, message: '请填写手机号码!' },
{ pattern: /^1[3-8]\d{9}$/, message: '请填写正确的手机号码!' }
]
},
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('发送失败,稍后重试!')
}
})xml
<van-field model:value="{{mobile}}" />易错点:安装 wechat-validate 后必须在小程序开发者工具里“构建 npm”;behaviors 和 rules 要与 data 同级;model:value 绑定的数据要在 data 中初始化;开发阶段接口可能直接返回验证码,生产环境不会。
2.4.3 登录&注册:调用 /login 并保存 token
为什么需要:手机号和验证码校验通过后,要调用接口完成登录或注册,并保存登录状态。
怎么想:登录和注册合二为一,后端根据手机号是否注册过决定登录还是注册。前端先校验 mobile、code,再 post('/login');成功后把 token、refreshToken 存到应用实例和本地存储,并按 redirectURL 回跳。
怎么做:
js
Page({
data: { mobile: '', code: '' },
rules: {
code: [
{ required: true, message: '请填写验证码!' },
{ pattern: /^\d{6}$/, message: '请填写正确的验证码!' }
]
},
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('登录失败,稍后重试!')
const app = getApp()
app.setToken('token', data.token)
app.setToken('refreshToken', data.refreshToken)
wx.redirectTo({ url: this.redirectURL })
}
})易错点:先写死手机号调通接口,再改成双向绑定;提交前要校验全部规则;开发测试可用通用验证码 123456;失败提示后续可在请求拦截器统一处理,练习时先保证正确流程。
2.4.4 头像和昵称:/userInfo 与上传
为什么需要:登录后要展示用户头像、昵称,并允许修改。接口需要携带登录状态。
怎么想:在请求拦截器里统一加 Authorization: Bearer token;/userInfo 获取资料;昵称用 type="nickname" 输入并 PUT /userInfo;头像用 open-type="chooseAvatar" 获取临时路径,再用 wx.uploadFile 上传。
怎么做:
js
// utils/http.js
http.intercept.request = function (options) {
const defaultHeader = {}
defaultHeader.Authorization = 'Bearer ' + getApp().token
options.header = Object.assign({}, defaultHeader, options.header)
return options
}js
async getUserProfile() {
const { code, data: { avatar, nickName } } = await wx.http.get('/userInfo')
if (code !== 10000) return wx.utils.toast()
this.setData({ avatar, nickName })
}js
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('更新昵称失败!')
getCurrentPages()[0].setData({ nickName })
}js
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('上传头像失败!')
getCurrentPages()[0].setData({ avatar: data.data.url })
}
})易错点:token 前要拼 Bearer ;wx.uploadFile 不支持 Promise,且要提前在小程序后台配置服务器域名;未登录不必请求 /userInfo;更新后要把头像昵称同步到应用实例 app.userProfile,方便其他页面读取。
课件图示








代码示例
示例一:公告列表 + 公告详情(WXML / JS)
xml
<!-- pages/index/index.wxml:公告列表 -->
<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>javascript
// pages/index/index.js
Page({
data: {
notices: []
},
onLoad() {
// 获取公告列表数据
this.getNotices()
},
// 调用公告列表接口
async getNotices() {
// 请求接口,每次只返回 3 条,不需要处理分页
const { code, data: notices } = await wx.http.get('/announcement')
// 检测接口是否调用成功
if (code !== 10000) return wx.utils.toast()
// 渲染数据
this.setData({ notices })
}
})javascript
// pages/notify/index.js:公告详情
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
<!-- pages/notify/index.wxml:详情渲染,富文本必须用 rich-text -->
<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 wx.http.get(...):解构时把data重命名为notices,正好与setData({ notices })的简写对应;笔记原文漏了=,写成} await会直接语法报错,这是最常见的抄错点。if (code !== 10000) return wx.utils.toast():接口业务码不是 10000 就提前 return,避免把错误响应当成列表数据渲染。- 列表用
<navigator url="/pages/notify/index">跳转,详情页靠onLoad({ id })从地址参数取 ID;注意typeof id === 'undefined'要写成字符串比较,typeof返回的是字符串。 rich-text的nodes属性是小程序解析 HTML 标签的唯一入口,直接{{noticeDetail.content}}到<view>里会原样输出标签文本。
示例二:登录检测(鉴权组件 authorization + token 持久化)
javascript
// app.js
App({
onLaunch() {
// 读取本地存储的 token 数据
this.getToken()
},
getToken() {
// 将 token 数据记到应用实例中
this.token = wx.getStorageSync('token')
},
setToken(key, token) {
// 将 token 记录在应用实例中
this[key] = token
// 将 token 存入本地
wx.setStorageSync(key, token)
}
})json
// app.json:全局注册组件,任意页面可直接使用
{
"usingComponents": {
"authorization": "/components/authorization/index"
}
}xml
<!-- components/authorization/index.wxml -->
<slot wx:if="{{isLogin}}"></slot>javascript
// components/authorization/index.js
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
})
}
}
}
})xml
<!-- pages/my/index.wxml:页面根节点包裹 authorization -->
<authorization>
<view class="profile">
<view class="profile-base">
<image class="avatar" src="{{avatar}}"></image>
<text class="nickname">{{nickName || '微信用户'}}</text>
</view>
</view>
</authorization>要点解释:
!!getApp().token做布尔转换:token为undefined时得到false,有值时得到true,再交给<slot wx:if>决定渲染与否。- 组件生命周期必须写在
lifetimes里,且用attached而不是created——created阶段不能调用setData,调用无效。 wx.redirectTo关闭当前页再跳登录页(登录成功后重新进入页面才会重新触发onLoad拉数据);若用wx.navigateTo会保留原页面,后退回来仍是旧数据。pageStack.pop()取出当前页面实例,用currentPage.route拼redirectURL参数,实现登录后回跳。
示例三:登录页(倒计时 + 表单校验 + 保存 token)
xml
<!-- pages/login/index.wxml -->
<van-field
model:value="{{mobile}}"
type="number"
maxlength="11"
placeholder="请输入手机号"
use-button-slot
>
<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-field model:value="{{code}}" type="number" maxlength="6" placeholder="请输入验证码" />
<view class="btn" bind:tap="submitForm">登录</view>javascript
// pages/login/index.js
// 导入表单验证插件
import wxValidate from 'wechat-validate'
const app = getApp()
Page({
data: {
mobile: '',
code: '',
// 初始状态不启用倒计时组件
countDownVisible: false,
timeData: {}
},
// 将插件注入到页面实例中(类似 mixin)
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() {
// 不传参数表示校验 rules 中的全部字段
const { valid, message } = this.validate()
if (!valid) return wx.utils.toast(message)
// 调用登录接口
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 })
}
})javascript
// utils/http.js:请求拦截器,让每个请求自动携带登录态
import http from 'wechat-http'
http.baseURL = 'https://live-api.itheima.net'
http.intercept.request = function (options) {
const defaultHeader = {}
// 注意 Bearer 后的空格
defaultHeader.Authorization = 'Bearer ' + getApp().token
options.header = Object.assign({}, defaultHeader, options.header)
return options
}要点解释:
behaviors和rules必须与data同级,写进data里就注入失败;model:value只能做简易双向绑定,且被绑定的字段必须在data里先声明。countDownVisible初值为false,点击后才置true;countDownChange里用minutes === 1 || seconds > 0判断是否继续显示,倒计时归零时组件自动隐藏。this.validate('mobile')只校验手机号,this.validate()不传参校验全部;返回值{ valid, message }中message就是规则里写的提示文案。- 安装
wechat-validate后必须执行“构建 npm”才能用;wx.http.get/post的第二个参数统一是data对象,GET 也一样。 app.setToken同时写应用实例和本地存储,http.intercept.request里拼接'Bearer ' + getApp().token,空格和前缀都不能少,否则服务端鉴权失败。
常见错误
app.json里出现两个usingComponents现象:控制台提示键重复,之前注册的组件突然失效。 原因:直接在文件顶部新增了一个同名键,JSON 后者覆盖前者。 修法:合并进已有的usingComponents对象,全局组件只保留一处注册。组件路径写成
@/components/...现象:编译报「未找到组件」,页面白屏。 原因:小程序不支持 Webpack 的@别名。 修法:用/components/authorization/index(相对src根)或./开头。包了
authorization但页面仍然空白 现象:isLogin为true时内容也不显示。 原因:组件wxml里漏写<slot>,或写成了<slot wx:if="{{!isLogin}}">。 修法:<slot wx:if="{{isLogin}}"></slot>,并确认页面根节点确实是authorization。登录成功后返回原页面,数据还是旧的 现象:
redirectURL跳回去了,但页面不重新请求接口。 原因:用了wx.navigateTo跳登录页,旧页面只是被压在栈里,没有销毁重建。 修法:改用wx.redirectTo,让登录后重新onLoad触发数据加载。behaviors/rules写进了data现象:this.validate()报未定义,或校验永远返回true。 原因:注入层级不对,插件能力没挂到 Page 实例上。 修法:behaviors、rules与data同级,且data里先初始化mobile: ''、code: ''。npm install之后直接 import 现象:报「module not found」,构建目录里没有miniprogram_npm。 原因:小程序不会自动解析node_modules。 修法:开发者工具 → 工具 → 构建 npm,之后每次装包都要重新构建。wx.uploadFile用await拿结果 现象:data是undefined,或上传成功但页面头像不更新。 原因:该 API 不返回 Promise,且不走http拦截器,Authorization不会自动带上。 修法:用success回调 +JSON.parse(result.data),header手动拼'Bearer ' + token。if (typeof id === undefined) return现象:判断永远不成立,id缺失时仍发出/announcement/undefined请求。 原因:typeof返回的是字符串'undefined',不是值undefined。 修法:if (!id) return wx.utils.toast('参数错误')并navigateBack。倒计时结束后按钮不恢复 现象:显示「0 秒后重新获取」,点击无反应。 原因:
countDownVisible关闭条件写错(如只判断seconds > 0,忽略了minutes === 1的整分钟边界)。 修法:countDownVisible: ev.detail.seconds > 0,或严格按minutes === 1 || seconds > 0判断,并实测 3 秒短倒计时验证。
练习
鉴权组件增强:在
authorization未登录时不要留白,渲染一个「正在跳转登录…」的占位结构;并把redirectURL拼接改为保留原页面的 query 参数(用encodeURIComponent处理)。 验收:从带参数的页面进入登录页,登录成功后可回到原页面且参数不丢失。倒计时时序修正:当前是「点击 → 开倒计时 → 发请求」,改为「校验 → 发请求 → 成功才开倒计时」,失败时给出 toast 且按钮可再次点击。 验收:断网或手机号格式错误时,倒计时不启动。
公告详情兜底:修正
id判断,并补上「无 id 或接口失败」时的处理(toast +navigateBack);同时给列表页跳转补上?id={{item.id}}。 验收:手动访问无参数详情页不白屏、不发无效请求。错误处理收敛:在
utils/http.js增加响应拦截器,统一处理code !== 10000的 toast,然后删除各页面里重复的if (code !== 10000) return wx.utils.toast()。 验收:随机让某个接口返回错误码,页面仍能弹出提示,业务代码中不再出现重复判断。封装上传:把
wx.uploadFile封装成返回 Promise 的工具函数,自动注入Authorization头、自动JSON.parse与错误码判断,并用它重写头像上传逻辑。 验收:调用处可以用await一行拿到url。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 登录检测-鉴权组件准备 | 07:17 |
| 2 | 登录检测-鉴权组件插槽和token关系 | 09:31 |
| 3 | 登录检测-未登录跳转登录页 | 10:37 |
| 4 | 登录-微调短信验证码静态结构 | 13:56 |
| 5 | 登录-发送验证码 | 11:28 |
| 6 | 登录-发送验证码-表单校验 | 15:07 |
| 7 | 登录-登录成功保存token | 15:00 |
小结
- 小程序没有路由守卫 API,鉴权是「组件包裹 + 插槽 + 生命周期」组合出来的模式,不是框架能力,因此包裹位置和插槽条件必须成对检查。
- 登录态的唯一真相源应放在
app实例 + 本地存储,页面只读不写,避免多处副本不同步。 - 跳转方式的选择直接决定目标页生命周期:需要「回来时重新加载」就必须用
redirectTo,这是登录链路的隐含前提。 - 第三方能力(校验插件、组件库)的接入成本主要不在 API,而在「注入位置」和「构建」这两个非代码环节。
- 开发环境的便利特性(验证码回显、通用验证码
123456)要当作临时开关看待,不能写进业务判断。 - 不走统一封装的 API(
uploadFile、rich-text等)是错误高发区,越早封装成统一形态,后期维护成本越低。 - 跨页面数据同步(
pageStack[0].setData、app.userProfile)在页面数少时够用,但要意识到它是临时方案。 - 先跑通正向链路,再补校验与错误处理;而错误处理的最终归宿是拦截器,不是每个页面各写一遍。