Skip to content

团队开发准备与登录(三) ​

本页对应 团队开发准备与登录,共 7 段视频,约 1 小时 22 分。

学习目标 ​

  1. 能使用微信开发者工具创建自定义组件并生成四件套文件,在 app.json 的 usingComponents 中正确注册全局组件,并在页面模板中引用验证其可正常渲染。
  2. 能通过 <slot> 接收页面传入的结构,结合组件生命周期(lifetimes.attached)读取 token 并做布尔转换,实现「有 token 才渲染插槽内容、无 token 不渲染」的鉴权效果。
  3. 能使用 wx.getStorageSync 在组件内动态读取持久化 token,并在未登录时调用 wx.redirectTo 跳转登录页,说明 redirectTo 与 navigateTo 在页面栈保留与销毁上的区别及选择理由。
  4. 能基于 Vant 小程序版 field 组件改造短信验证码结构(maxlength、type="number"、use-button-slot、count-down 倒计时插槽),并通过 bind:change 提取秒数、用布尔变量控制倒计时启停与按钮文案切换。
  5. 能安装并构建 async-validator(npm install + 工具「构建 npm」),在页面中引入 behavior 与 rules 编写手机号、验证码的必填与正则规则,调用 this.validate 完成单项/全表单校验,并在校验失败时给出提示、通过后才发送请求。
  6. 能通过 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=&#123;&#123;item.id&#125;&#125;。这一点是根据“点击公告列表后将公告的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,方便其他页面读取。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

示例一:公告列表 + 公告详情(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 标签的唯一入口,直接 &#123;&#123;noticeDetail.content&#125;&#125; 到 <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,空格和前缀都不能少,否则服务端鉴权失败。

常见错误 ​

  1. app.json 里出现两个 usingComponents 现象:控制台提示键重复,之前注册的组件突然失效。 原因:直接在文件顶部新增了一个同名键,JSON 后者覆盖前者。 修法:合并进已有的 usingComponents 对象,全局组件只保留一处注册。

  2. 组件路径写成 @/components/... 现象:编译报「未找到组件」,页面白屏。 原因:小程序不支持 Webpack 的 @ 别名。 修法:用 /components/authorization/index(相对 src 根)或 ./ 开头。

  3. 包了 authorization 但页面仍然空白 现象:isLogin 为 true 时内容也不显示。 原因:组件 wxml 里漏写 <slot>,或写成了 <slot wx:if="&#123;&#123;!isLogin&#125;&#125;">。 修法:<slot wx:if="&#123;&#123;isLogin&#125;&#125;"></slot>,并确认页面根节点确实是 authorization。

  4. 登录成功后返回原页面,数据还是旧的 现象:redirectURL 跳回去了,但页面不重新请求接口。 原因:用了 wx.navigateTo 跳登录页,旧页面只是被压在栈里,没有销毁重建。 修法:改用 wx.redirectTo,让登录后重新 onLoad 触发数据加载。

  5. behaviors / rules 写进了 data 现象:this.validate() 报未定义,或校验永远返回 true。 原因:注入层级不对,插件能力没挂到 Page 实例上。 修法:behaviors、rules 与 data 同级,且 data 里先初始化 mobile: ''、code: ''。

  6. npm install 之后直接 import 现象:报「module not found」,构建目录里没有 miniprogram_npm。 原因:小程序不会自动解析 node_modules。 修法:开发者工具 → 工具 → 构建 npm,之后每次装包都要重新构建。

  7. wx.uploadFile 用 await 拿结果 现象:data 是 undefined,或上传成功但页面头像不更新。 原因:该 API 不返回 Promise,且不走 http 拦截器,Authorization 不会自动带上。 修法:用 success 回调 + JSON.parse(result.data),header 手动拼 'Bearer ' + token。

  8. if (typeof id === undefined) return 现象:判断永远不成立,id 缺失时仍发出 /announcement/undefined 请求。 原因:typeof 返回的是字符串 'undefined',不是值 undefined。 修法:if (!id) return wx.utils.toast('参数错误') 并 navigateBack。

  9. 倒计时结束后按钮不恢复 现象:显示「0 秒后重新获取」,点击无反应。 原因:countDownVisible 关闭条件写错(如只判断 seconds > 0,忽略了 minutes === 1 的整分钟边界)。 修法:countDownVisible: ev.detail.seconds > 0,或严格按 minutes === 1 || seconds > 0 判断,并实测 3 秒短倒计时验证。

练习 ​

  1. 鉴权组件增强:在 authorization 未登录时不要留白,渲染一个「正在跳转登录…」的占位结构;并把 redirectURL 拼接改为保留原页面的 query 参数(用 encodeURIComponent 处理)。 验收:从带参数的页面进入登录页,登录成功后可回到原页面且参数不丢失。

  2. 倒计时时序修正:当前是「点击 → 开倒计时 → 发请求」,改为「校验 → 发请求 → 成功才开倒计时」,失败时给出 toast 且按钮可再次点击。 验收:断网或手机号格式错误时,倒计时不启动。

  3. 公告详情兜底:修正 id 判断,并补上「无 id 或接口失败」时的处理(toast + navigateBack);同时给列表页跳转补上 ?id=&#123;&#123;item.id&#125;&#125;。 验收:手动访问无参数详情页不白屏、不发无效请求。

  4. 错误处理收敛:在 utils/http.js 增加响应拦截器,统一处理 code !== 10000 的 toast,然后删除各页面里重复的 if (code !== 10000) return wx.utils.toast()。 验收:随机让某个接口返回错误码,页面仍能弹出提示,业务代码中不再出现重复判断。

  5. 封装上传:把 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登录-登录成功保存token15:00

小结 ​

  • 小程序没有路由守卫 API,鉴权是「组件包裹 + 插槽 + 生命周期」组合出来的模式,不是框架能力,因此包裹位置和插槽条件必须成对检查。
  • 登录态的唯一真相源应放在 app 实例 + 本地存储,页面只读不写,避免多处副本不同步。
  • 跳转方式的选择直接决定目标页生命周期:需要「回来时重新加载」就必须用 redirectTo,这是登录链路的隐含前提。
  • 第三方能力(校验插件、组件库)的接入成本主要不在 API,而在「注入位置」和「构建」这两个非代码环节。
  • 开发环境的便利特性(验证码回显、通用验证码 123456)要当作临时开关看待,不能写进业务判断。
  • 不走统一封装的 API(uploadFile、rich-text 等)是错误高发区,越早封装成统一形态,后期维护成本越低。
  • 跨页面数据同步(pageStack[0].setData、app.userProfile)在页面数少时够用,但要意识到它是临时方案。
  • 先跑通正向链路,再补校验与错误处理;而错误处理的最终归宿是拦截器,不是每个页面各写一遍。