Skip to content

团队开发与基础封装(三) ​

本页对应 团队开发与基础封装,共 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,否则返回“我的”页面仍显示旧数据。新用户头像和昵称可能为空,昵称可给“微信用户”默认值,头像可用背景图兜底。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

示例一:公告列表 + 公告详情 ​

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=&#123;&#123;item.id&#125;&#125;",否则详情页 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="&#123;&#123;...&#125;&#125;"> 是解析富文本的唯一方式,直接把 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="&#123;&#123;mobile&#125;&#125;" 是简易双向绑定,少了 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,模板里用 &#123;&#123;nickName || '微信用户'&#125;&#125; 兜底;头像则靠 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()。

练习 ​

  1. 笔记中公告详情页的 if (typeof id === undefined) return 永远不成立,请指出原因并改写;再补一条「id 存在但接口 404」的兜底处理。

  2. 给 authorization 组件加白名单:登录页自身不能被拦截,否则会无限重定向。请设计实现方案(白名单从哪来、判断放在哪一步、redirectTo 还是 navigateTo)。

  3. 昵称更新后同步到「我的」页面,笔记里用了页面栈 pageStack[0].setData。请再实现 onShow 重新拉取的方案,并从「数据一致性 / 请求次数 / 适用场景」三个角度做对比,说明各自何时该用。

  4. 把 wx.uploadFile 封装成一个返回 Promise 的 uploadFile(filePath, formData),要求:自动拼 baseURL、自动带 Authorization、自动 JSON.parse 返回值、失败时 reject。封装后在头像上传处替换原回调写法。

  5. 给获取验证码按钮加上「防连点」:倒计时期间点击不再发请求,且手机号被修改后倒计时状态如何处置?写出你的判断逻辑。

原视频对照 ​

#视频时长
1享+生活-用户管理(登录&注册)-113:01
2享+生活-用户管理(登录&注册)-206:05
3享+生活-用户管理(登录&注册)-307:51
4享+生活-用户管理(头像和昵称)-113:02
5享+生活-用户管理(头像和昵称)-210:52
6享+生活-用户管理(头像和昵称)-315:48
7享+生活-用户管理(头像和昵称)-409:04

小结 ​

  1. 登录与注册在后端合一,前端只负责校验并提交 mobile + code,不参与「是登录还是注册」的判断,也不要根据返回值做分支。
  2. 页面访问控制做成组件(slot + isLogin)比在每个页面写判断更可维护;但必须处理白名单和 tabBar 跳转两个边界。
  3. token 采用「应用实例 + 本地存储」双写:内存供全局同步读取,Storage 供冷启动恢复,两者通过 setToken 一个入口维护,避免漏写。
  4. 请求拦截器的价值在于「一次配置、全局生效」,核心是 Object.assign 合并不覆盖 + return options 不能漏。
  5. validate() 不传参即校验全部规则,任一不合法返回 false;校验通过才发请求,是表单类页面的固定前置动作。
  6. wx.uploadFile 的三个坑集中在:不支持 Promise、返回体是字符串、需在小程序后台配置合法域名,封装成 Promise 后可以一次性消掉前两个。
  7. 跨页面数据同步没有唯一解:页面栈 setData 适合「返回即刷新」的即时反馈,onShow 重拉适合「进入即最新」的一致性优先,全局实例适合多处共享但需手动双向同步。
  8. 富文本内容必须交给 rich-text 的 nodes 渲染,直接插值只会显示成纯文本。