Skip to content

数据绑定、渲染与表单(二) ​

本页对应 数据绑定、渲染与表单,共 7 段视频,约 1 小时 6 分。

学习目标 ​

  • 能够在小程序开发设置中勾选“不校验合法域名”完成未备案接口调试,并说明该设置仅用于开发练习、上线前必须配置合法域名的限制。
  • 能够使用 wx.showLoading、wx.hideLoading、wx.showToast 实现加载与提示交互,并根据成功/失败/完成时机正确隐藏加载框,避免 hideLoading 误关闭轻提示。
  • 能够使用 wx.setStorageSync、wx.getStorageSync、wx.removeStorageSync、wx.clearStorageSync 完成本地数据的增删查清,并直接存取数组或对象而无需手动序列化。
  • 能够区分同步 API、异步回调 API 与支持 Promise 的 API,并使用 async/await 调用支持 Promise 的接口完成图片选择与结果渲染。
  • 能够综合运用事件绑定、双向绑定、条件渲染和列表渲染,实现搜索历史功能,并完成非空校验、去重、追加顺序控制、搜索框清空及无数据时隐藏历史区域。

知识点 ​

模板语法总览:先分清“内容绑定”和“控制属性” ​

在页面中渲染数据时所用到的一系列语法叫做模板语法,对应到 Vue 中就是指令的概念。小程序里同样用 {{}} 把数据接到模板上,但它和 Vue 有一个明显差异:无论是属性绑定还是内容绑定,都必须要使用 {{}}。理解这一点后,后面的条件渲染、列表渲染才不会把“值”和“表达式”混在一起。

为什么需要:模板语法解决的是“数据如何出现在页面、页面结构如何随数据变化”的问题。
怎么想:先判断是渲染文本内容,还是控制属性值,还是控制组件是否显示/重复显示。
怎么做:内容用插值,属性用 {{表达式}},条件用 wx:if,列表用 wx:for。
易错点:属性里直接写字符串 "false" 并不等于布尔值 false。

数据绑定:插值与属性绑定 ​

为什么需要:页面不能写死,开关状态、输入内容、提示文字都要来自 data。
怎么想:文本节点用 {{数据名}};组件属性也要用 {{表达式}},否则会被当成普通字符串。
怎么做:笔记中的示例:

xml
<!-- 数据绑定 -->
<view class="binding">
  <!-- 开关组件 -->
  <switch checked="{{isOpen}}"/>
  <!-- 双向数据绑定 -->
  <input type="text" model:value="{{message}}" />
  <view class="message">{{message}}</view>
</view>
javascript
Page({
  data: {
    isOpen: true,
    message: 'hello world!'
  }
})

笔记特别强调:

xml
<!-- 此处的 false 是字符串的 'false',因此布尔结果为 true -->
<switch checked="false" />
<!-- 如下才是正确表示布尔值 false 的方法 -->
<switch checked="{{false}}" />

易错点:checked="false" 里的 false 是字符串,字符串在布尔判断中通常为真,所以开关并不会按预期关闭;要写 checked="&#123;&#123;false&#125;&#125;" 才是布尔值。

简易双向绑定:model:value ​

为什么需要:输入框输入后,希望 data 自动更新,不用每次手动取 e.detail.value。
怎么想:小程序提供 model:value="&#123;&#123;数据名&#125;&#125;",目前只能用在 input 和 textarea 组件中。
怎么做:如上例 <input type="text" model:value="&#123;&#123;message&#125;&#125;" />,下面再用 &#123;&#123;message&#125;&#125; 展示,输入时页面文字会跟着变。
易错点:不要把它当成所有组件通用;转写稿的综合练习里,搜索框正是用 model:value 收集关键词,随后在事件中通过 this.data.keyword 读取。注意小程序读 data 要写 this.data.keyword,不能像 Vue 那样直接 this.keyword。

条件渲染:wx:if / wx:else 与 hidden ​

为什么需要:登录与未登录、加载中与加载完成,页面结构要随状态切换。
怎么想:wx:if 根据表达式值决定是否渲染内容;wx:else 必须紧跟在 wx:if 后面,不能单独使用。hidden 是组件属性,值为真时隐藏。
怎么做:笔记代码:

xml
<!-- 条件渲染 wx:if 和 wx:else -->
<view class="welcome">
  <text wx:if="{{isLogin}}">大师兄</text>
  <text wx:else>游客</text>你好:
</view>

<!-- 条件渲染 hidden -->
<view class="loading">
  <!-- 可以单独使用 -->
  <!-- <text wx:if="{{!loaded}}">正在加载...</text> -->
  <text hidden="{{loaded}}">正在加载...</text>
</view>
javascript
Page({
  data: {
    isOpen: true,
    message: 'hello world!',
    isLogin: true,
    loaded: false
  }
})

易错点:hidden 的隐藏效果依赖 [hidden] { display: none; };wx:if 与 wx:else 要成对相邻。转写稿搜索框练习里,搜索历史标题用 wx:if 判断数组长度是否大于 0,注意不要用“空数组等于空数组”来判断,空数组是对象,比较内存地址并不相等,应判断 length。

列表渲染:wx:for、wx:key 与自定义变量名 ​

为什么需要:学生列表、搜索历史等重复结构不能一项项手写。
怎么想:wx:for 根据数组重复渲染;默认 index 是索引,item 是单元值;wx:key 是列表项唯一标识,不写 &#123;&#123;&#125;&#125;。
怎么做:笔记代码:

xml
<!-- 列表渲染 -->
<view class="students">
  <view class="item">
    <text>序号</text>
    <text>姓名</text>
    <text>年龄</text>
    <text>性别</text>
    <text>级别</text>
  </view>
  <view class="item">
    <text>1</text>
    <text>贺洋</text>
    <text>20</text>
    <text>男</text>
    <text>菜鸟</text>
  </view>
</view>
<!-- 简单数组 -->
<view class="history">
  <text>小米</text>
</view>
javascript
Page({
  data: {
    isOpen: true,
    message: 'hello world!',
    isLogin: true,
    loaded: false,
    students: [
      {id: 1, name: '贺洋', age: 20, gender: '男', level: '菜鸟'},
      {id: 2, name: '唐刚', age: 18, gender: '女', level: '笨鸟'},
      {id: 3, name: '常超', age: 20, gender: '女', level: '老鸟'}
    ],
    history: ['苹果', '华为', 'OPPO', '三星']
  }
})

实际写列表时,students 可用 wx:for="&#123;&#123;students&#125;&#125;" wx:key="id",对象单元只写属性名;history 是简单类型,推荐 wx:key="*this"。需要改索引名用 wx:for-index,改单元名用 wx:for-item。

易错点:wx:key 不写 &#123;&#123;&#125;&#125;;简单数组不要写 item 当 key,转写稿里反复提醒“简单数据用 *this 表示元素本身”。另外,给 wx:for 的数组追加数据必须通过 setData 更新;转写稿搜索框练习中,直接写 push 并把返回值赋给数组是错误示范,因为 push 返回的是数组长度,不是数组本身,可用展开运算符生成新数组再 setData。

内置 API 是什么 ​

小程序把网络请求、本地存储、拍照录音、界面提示等能力,封装在全局对象 wx 下。需要什么功能,就调用对应的 wx.xxx。学习时不必背完所有 API,重点是掌握调用方式、回调结构和常见坑。

网络请求与合法域名 ​

页面要展示后端书单,就需要发起请求。思路是:在事件处理函数中调用 wx.request,在 success 里用 this.setData 更新页面数据。语法如下:

javascript
wx.request({
  url: '这里是接口的地址',
  method: '这里是请求的方法',
  data: '请求时提交的数据',
  header: {/* 请求头信息 */},
  success: () => {/* 成功的回调 */},
  fail: () => {/* 失败的回调 */},
  complete: () => {/* 成功或失败的回调 */}
})

演示结构:

xml
<button class="button" size="mini" type="primary">查询书单</button>
<view class="books">
  <view class="item">
    <text>序号</text>
    <text>名称</text>
    <text>作者</text>
    <text>出版社</text>
    <text>操作</text>
  </view>
  <view class="item">
    <text>1</text>
    <text>西游记</text>
    <text>吴承恩</text>
    <text>人民文学出版社</text>
    <text>删除</text>
  </view>
</view>
javascript
Page({
  data: {
    books: []
  },
  // 调用数据接口的方法
  getBooks() {
    // 调用小程序的 API 发起请求
    wx.request({
      url: 'https://hmajax.itheima.net/api/books',
      method: 'GET',
      data: {
        creator: 'zhangsan'
      },
      success: (result) => {
        // 更新数据,渲染页面
        this.setData({ books: result.data.data })
      }
    })
  }
})

调试时如果换了未配置的域名,请求会失败。开发阶段可在“本地设置”勾选不校验合法域名等选项,先跑通练习;真机预览通常还要开启调试模式。上线必须配置合法域名,不能把练习设置当成上线方案。易错点:this.data.books 才能取数据;success 里要用箭头函数或提前保存 this。

界面交互:加载框与轻提示 ​

请求有等待过程,用户需要感知,所以用 wx.showLoading 显示加载框,wx.hideLoading 隐藏;请求成功后可 wx.showToast 给轻提示。showLoading 的 title 是文字,mask 防止触摸穿透;showToast 的 mask 同样防穿透,duration 控制显示时长,icon: 'none' 可去掉图标、显示更多文字。轻提示会自动消失,默认约 1.5 秒。

推导上,加载框只负责显示,不会知道请求何时结束,所以必须手动隐藏。更合理的是把 hideLoading 放到 complete,成功失败都收尾;成功时用 showToast 覆盖加载框。注意 showLoading 和 showToast 同时只能显示一个,hideLoading 也可能把 showToast 隐藏,真机上容易出现“成功提示一闪而过”。

本地存储 ​

网页端常用 localStorage,小程序对应 wx.setStorageSync、wx.getStorageSync、wx.removeStorageSync、wx.clearStorageSync。优点是复杂类型不必 JSON.stringify / JSON.parse,存数组取出来仍是数组。

xml
<!-- 本地存储 -->
<view class="storage">
  <button size="mini" type="primary">存数据</button>
  <button size="mini" type="primary">读数据</button>
  <button size="mini" type="primary">删数据</button>
  <button size="mini" type="primary">清数据</button>
</view>

同步 API 以 Sync 结尾,代码按顺序执行,写法简单;异步版本要传对象并在 success 中取结果,嵌套多时不便维护。易错点:API 名称很像,要认准 set/get/remove/clear + Storage + Sync;删除指定 key 用 removeStorageSync,清空全部才用 clearStorageSync。

API 的三种调用风格 ​

异步 API 通过 success、fail、complete 回调获取结果;同步 API 以 Sync 结尾;部分异步 API 支持 Promise,可配合 async/await。看到文档写“支持 Promise 风格调用”,就可以用返回值接结果,减少回调嵌套。

相册/拍照 ​

选择图片用 wx.chooseMedia,count 控制数量,mediaType 控制图片或视频。返回的 tempFiles 是数组,取 tempFilePath 作为临时路径渲染。

javascript
Page({
  async chooseImage() {
    // 读取结果
    const result = await wx.chooseMedia({
      count: 1, // 只选取一张图片
      mediaType: ['image'] // 只选取图片
    })
    console.log(result)
  }
})

易错点:mediaType 是数组;返回固定为数组,即使只选一张也要取下标;临时路径不是已上传服务器地址。

搜索历史小练习 ​

xml
<!-- 搜索历史 -->
<view class="history">
  <view class="search-bar">
    <input type="text" />
    <text class="label">搜索</text>
  </view>
  <view class="title">
    历史搜索 <text class="icon-delete">x</text>
  </view>
  <view class="keywords">
    <navigator url="/pages/test/index">小米</navigator>
    <navigator url="/pages/test/index">苹果</navigator>
    <navigator url="/pages/test/index">华为</navigator>
  </view>
</view>

实现思路:输入框用简易双向绑定收集 keyword,点击搜索先 trim 去首尾空格,再做非空判断;空内容用 showToast({ icon: 'none' }) 提示并 return。追加时不要写 this.setData({ keywords: this.data.keywords.push(keyword) }),因为 push 返回的是数组长度;应使用展开运算符生成新数组,并把新词放前面。去重可用 new Set 后配合 Array.from 或展开转回数组。清空时把 keywords 设为 []。条件渲染判断历史标题和列表是否显示,应判断 keywords.length > 0,不能写“空数组 === 空数组”,因为数组是引用类型。

事件对象:回调不能传参,那数据怎么带过去 ​

先回顾语法:小程序里监听事件写 bind:事件类型=事件回调,比如 bindtap="onSearch"。这里有个限制——事件回调不支持传参数,你不能在模板里写 bindtap="onSearch('关键字')"。可业务上我们又确实需要把模板里的数据(用户输入的内容、当前点击的是第几项)带到回调里去处理,怎么办?

小程序的答案是事件对象:事件回调函数的第 1 个参数就是事件对象,模板在触发事件时产生的信息都挂在它身上。所以思路是:不要想着“把参数传进去”,而是“回调被调用时,从事件对象里把信息取出来”。

最稳妥的第一步不是猜字段,而是先打印:

javascript
Page({
  eventHandler(ev) {
    // 查看事件对象
    console.log(ev)
  }
})

把 ev 打印出来,看它到底带了哪些字段,再决定取哪一个。这也是我们后面所有事件调试的通用手法:先确认“事件有没有触发”,再确认“数据在不在里面”。

配合本节演示结构,搜索框就是最好的练手对象:

xml
<!-- 搜索框 -->
<view class="search-bar">
  <input type="text" placeholder="输入搜索关键字" />
</view>
<!-- 页面主体 -->
<view class="page-body">
  <scroll-view scroll-y class="aside">
    <view wx:for="{{6}}" wx:key="*this" class="item"></view>
  </scroll-view>
  <scroll-view scroll-y refresher-enabled class="content">
    <view wx:for="{{6}}" wx:key="*this" class="item"></view>
  </scroll-view>
</view>

做法上,可以给搜索按钮绑 bindtap,在回调里先 console.log 验证触发,再取值。取值有两种常见路径:一是从事件对象里取;二是输入框用简易双向绑定 model:value 直接把值同步到 data,回调里只需要读 this.data.keyword。

易错点有三个:第一,别在模板里给回调写括号传参,写了也不会按你预期执行;第二,小程序里读数据必须带 data 这一级,是 this.data.keyword,不是 this.keyword;第三,改完数据要 setData 才会更新视图,而且往数组里追加时别写成 this.setData({ list: list.push(x) })——push 的返回值是数组长度,不是数组,正确写法是展开:this.setData({ list: [...list, x] })。

组件事件:scroll-view 的触底与下拉刷新 ​

tap 这类事件绝大部分组件都能监听,可以理解为通用事件;但有些事件只属于某个组件,这就是组件事件。典型代表是 scroll-view:

  • bind:scrolltolower:滚动内容到达底部或最右侧时触发
  • bind:refresherrefresh:执行下拉操作时触发
  • refresher-enabled:启用自定义下拉刷新

为什么需要它?因为“滚动到底加载更多”“下拉刷新最新数据”这类时机,只有滚动容器自己知道,靠通用事件是拿不到的。怎么想?先认组件,再看这个组件文档里有哪些 bind: 开头的事件,需要哪个能力就把对应属性打开。

本节演示代码:

javascript
Page({
  data: {
    isPulling: false
  },
  onScrollPulling() {
    // 请求最新的数据
		wx.request({
      url: 'https://hmajax.itheima.net/api/books',
      data: {creator: 'zhangsan'},
      success: (result) => {},
      complete: () => {
        // 停止下拉刷新
        this.setData({isPulling: false})
      }
    })
  }
})

注意 complete 的位置:它不管请求成功还是失败都会执行,所以“停止下拉刷新”这种收尾动作放这里最合适。如果只写在 success 里,一旦请求失败,下拉状态就一直停不下来。

易错点:笔记正文写的是 refresher-enable,而示例 XML 里是 refresher-enabled,以示例代码为准,属性名少个字母能力就不生效;另外下拉状态是自己在 data 里维护的(isPulling),请求结束必须手动复位。实际调试时还可以在请求前 wx.showLoading、在 complete 里 wx.hideLoading 并复位状态,必要时给 loading 加 mask,避免用户在加载期间反复触发。

表单组件事件:change 与 submit ​

  • change:表单数据发生改变时触发(input 不支持)
  • submit:表单提交时触发,button 按钮必须指定 form-type 属性

为什么单独讲?因为表单数据分散在 input、radio-group、checkbox-group、picker 里,一个个监听很啰嗦,form 提供了一次性收集的能力。

xml
<!-- 用户信息 -->
<view class="register">
  <form>
    <view class="form-field">
      <label for="">姓名:</label>
      <view class="field">
        <input type="text" placeholder="请输入您的姓名" />
      </view>
    </view>
    <view class="form-field">
      <label for="">性别:</label>
      <view class="field">
        <radio-group>
          <radio value="男" checked />男
          <radio value="女" />女
        </radio-group>
      </view>
    </view>
    <view class="form-field">
      <label for="">爱好:</label>
      <view class="field">
        <checkbox-group>
          <checkbox value="写代码" checked />写代码
          <checkbox value="睡大觉" />睡大觉
        </checkbox-group>
      </view>
    </view>
    <view class="form-field">
      <label for="">籍贯:</label>
      <view class="field">
        <picker mode="region">请选择籍贯</picker>
      </view>
    </view>
    <button size="mini" type="primary">保存</button>
  </form>
</view>

怎么做:外层用 form 包住,按钮上加 form-type="submit",然后给 form 绑 bindsubmit,在回调里通过事件对象拿到整份表单数据——具体字段还是用 4.1 的办法,先 console.log(ev) 看清楚。change 则用来监听 radio-group、checkbox-group、picker 的变化。

易错点:button 忘了写 form-type,submit 根本不触发;在 input 上绑 change 是无效的;radio、checkbox 要绑在 radio-group、checkbox-group 上,绑在单个选项上拿不到整组结果。

课件图示 ​

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

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

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

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

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

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

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

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

代码示例 ​

示例 1:模板语法三件套(数据绑定 / 条件渲染 / 列表渲染) ​

pages/demo/index.wxml

xml
<!-- ① 数据绑定 -->
<view class="binding">
  <!-- 开关组件:注意 checked 一定要用 {{}} -->
  <switch checked="{{isOpen}}" />
  <!-- 双向数据绑定:只能用在 input / textarea -->
  <input type="text" model:value="{{message}}" />
  <view class="message">{{message}}</view>
</view>

<!-- ② 条件渲染:wx:if / wx:else -->
<view class="welcome">
  <text wx:if="{{isLogin}}">大师兄</text>
  <text wx:else>游客</text>你好:
</view>

<!-- ② 条件渲染:hidden -->
<view class="loading">
  <text hidden="{{loaded}}">正在加载...</text>
</view>

<!-- ③ 列表渲染:数组单元是对象,wx:key 写属性名 -->
<view class="students">
  <view class="item">
    <text>序号</text>
    <text>姓名</text>
    <text>年龄</text>
    <text>性别</text>
    <text>级别</text>
  </view>
  <view class="item" wx:for="{{students}}" wx:key="id">
    <text>{{item.id}}</text>
    <text>{{item.name}}</text>
    <text>{{item.age}}</text>
    <text>{{item.gender}}</text>
    <text>{{item.level}}</text>
  </view>
</view>

<!-- ③ 列表渲染:数组单元是简单类型,wx:key 用 *this -->
<view class="history">
  <text wx:for="{{history}}" wx:key="*this" class="tag">{{item}}</text>
</view>

pages/demo/index.js

javascript
Page({
  data: {
    isOpen: true,
    message: 'hello world!',
    isLogin: true,
    loaded: false,
    students: [
      { id: 1, name: '贺洋', age: 20, gender: '男', level: '菜鸟' },
      { id: 2, name: '唐刚', age: 18, gender: '女', level: '笨鸟' },
      { id: 3, name: '常超', age: 20, gender: '女', level: '老鸟' }
    ],
    history: ['苹果', '华为', 'OPPO', '三星']
  }
})

pages/demo/index.wxss

css
.binding, .welcome, .loading, .students, .history { padding: 20rpx; }
.item { display: flex; }
.item text { flex: 1; padding: 10rpx 0; }
.tag { margin-right: 20rpx; }
/* hidden 属性在自定义组件/view 上不生效,必须自己写这条样式 */
[hidden] { display: none; }

要点与易错点

  • checked="false" 传的是字符串 'false',布尔结果为真,开关是打开的;必须写 checked="&#123;&#123;false&#125;&#125;" 才是布尔假。
  • 小程序中内容绑定和属性绑定都要写 &#123;&#123;&#125;&#125;,这与 Vue 不同。
  • model:value="&#123;&#123;message&#125;&#125;" 是目前唯一的简易双向绑定语法,仅支持 input 和 textarea。
  • wx:else 不能单独使用,必须紧跟在 wx:if(或 wx:elif)后面。
  • hidden 的语义是"值为真时隐藏",所以 hidden="&#123;&#123;loaded&#125;&#125;" 表示加载完就藏起来;并且 hidden 依赖 [hidden] { display: none; } 才能生效。
  • wx:key 不加 &#123;&#123;&#125;&#125;:数组单元是对象时直接写属性名(id);是简单类型时写 *this。

示例 2:网络请求 + 加载框/轻提示 + 本地存储 ​

pages/books/index.wxml

xml
<view class="toolbar">
  <button size="mini" type="primary" bind:tap="getBooks">查询书单</button>
  <button size="mini" bind:tap="saveData">存数据</button>
  <button size="mini" bind:tap="getData">读数据</button>
  <button size="mini" bind:tap="removeData">删数据</button>
</view>

<view class="books">
  <view class="item">
    <text>序号</text>
    <text>名称</text>
    <text>作者</text>
    <text>出版社</text>
  </view>
  <view class="item" wx:for="{{books}}" wx:key="id">
    <text>{{item.id}}</text>
    <text>{{item.name}}</text>
    <text>{{item.author}}</text>
    <text>{{item.publisher}}</text>
  </view>
</view>

pages/books/index.js

javascript
Page({
  data: {
    books: []
  },

  // 调用数据接口
  getBooks() {
    // 请求发出前:显示加载框(mask 防止用户连点)
    wx.showLoading({ title: '疯狂加载中', mask: true })

    wx.request({
      url: 'https://hmajax.itheima.net/api/books',
      method: 'GET',
      data: { creator: 'zhangsan' },
      success: (result) => {
        // 更新数据,渲染页面
        this.setData({ books: result.data.data })
        // 轻提示会直接覆盖掉 loading,无需再手动 hideLoading
        wx.showToast({ title: '查询成功', icon: 'none' })
      },
      fail: () => {
        // 失败才需要手动关掉加载框,否则 loading 会一直挡着
        wx.hideLoading()
        wx.showToast({ title: '查询失败', icon: 'none' })
      }
    })
  },

  // 存数据:复杂类型不需要 JSON.stringify
  saveData() {
    wx.setStorageSync('list', this.data.books)
    wx.showToast({ title: '保存成功', icon: 'none' })
  },

  // 读数据:复杂类型不需要 JSON.parse
  getData() {
    const list = wx.getStorageSync('list')
    console.log(list)
    this.setData({ books: list })
  },

  // 删数据
  removeData() {
    wx.removeStorageSync('list')
    wx.showToast({ title: '已删除', icon: 'none' })
  }
})

pages/books/index.wxss

css
.toolbar { display: flex; padding: 20rpx; }
.item { display: flex; padding: 10rpx 20rpx; }
.item text { flex: 1; }

要点与易错点

  • 调用 wx.request 时注意 this 指向,回调里要用箭头函数,否则 this.setData 会报错;也可以提前 const that = this。
  • wx.showLoading 只负责显示,不会自动关闭,必须在合适的时机调用 wx.hideLoading。
  • 隐藏 loading 不要写在 complete 里:showLoading 和 showToast 底层共用同一个提示框,hideLoading 会把刚弹出的 toast 一起隐藏掉(模拟器看不太出来,真机上是"一闪而过")。推荐做法:success 里用 showToast 直接覆盖,fail 里再 hideLoading。
  • mask: true 会加一层透明蒙层,防止请求过程中用户反复点击触发多次请求。
  • wx.setStorageSync / wx.getStorageSync 直接存取数组和对象,不需要 JSON.stringify / JSON.parse;读出来的 list 一定要判断是否为空,否则 setData 后列表会渲染成空。
  • 本地存储的 API 名称很长,注意区分 set / get / remove + Storage + Sync 三段,别选错提示项。

示例 3:搜索历史(收集关键词 + 去重 + 清空) ​

pages/search/index.wxml

xml
<view class="history">
  <view class="search-bar">
    <!-- 双向绑定收集关键词 -->
    <input type="text" model:value="{{keyword}}" placeholder="输入搜索关键字" />
    <text class="label" bind:tap="onSearch">搜索</text>
  </view>

  <!-- 有数据才渲染标题 -->
  <view class="title" wx:if="{{keywords.length > 0}}">
    历史搜索
    <text class="icon-delete" bind:tap="onClear">x</text>
  </view>

  <view class="keywords">
    <navigator wx:for="{{keywords}}" wx:key="*this" url="/pages/test/index">{{item}}</navigator>
  </view>
</view>

pages/search/index.js

javascript
Page({
  data: {
    keyword: '',   // 搜索框内容
    keywords: []   // 搜索历史列表
  },

  // 点击搜索按钮
  onSearch() {
    // 1. 取数据并去掉首尾空格
    const keyword = this.data.keyword.trim()

    // 2. 非空判断:有问题直接退出
    if (keyword === '') {
      wx.showToast({ title: '请输入搜索关键字', icon: 'none' })
      return
    }

    // 3. 新关键词放最前面 + 用 Set 去重
    const list = [keyword, ...this.data.keywords]
    const arr = Array.from(new Set(list))

    // 4. 更新视图,同时清空搜索框
    this.setData({ keywords: arr, keyword: '' })
  },

  // 清空历史
  onClear() {
    this.setData({ keywords: [] })
  }
})

pages/search/index.wxss

css
.history { padding: 20rpx; }
.search-bar { display: flex; align-items: center; }
.search-bar input { flex: 1; border: 1rpx solid #ddd; padding: 10rpx; }
.label { padding-left: 20rpx; color: #1989fa; }
.title { margin: 30rpx 0 20rpx; display: flex; justify-content: space-between; }
.keywords navigator { display: inline-block; padding: 8rpx 20rpx; margin: 0 20rpx 20rpx 0; background: #f2f2f2; }

要点与易错点

  • 事件监听语法是 bind:tap="回调名",回调名不加括号、不传参;需要的数据通过 this.data.xxx 或事件对象 ev 拿。
  • 追加数组不要写成 this.setData({ keywords: this.data.keywords.push(keyword) }):push 的返回值是数组长度,会把 keywords 变成一个数字。正确写法是展开运算符 [keyword, ...this.data.keywords] 拼出新数组。
  • 去重推荐 Array.from(new Set(list)) 或 [...new Set(list)],注意 new Set 返回的是 Set 对象,要转回数组再 setData。
  • 判断空数组不能用 keywords === []:每次写的空数组在内存里都是不同地址。应使用 keywords.length > 0。
  • wx:key="*this" 这里不能写 item,*this 表示数组元素本身。
  • wx.showToast 默认带成功对勾图标且只能显示 7 个左右字符,文案较长时要设 icon: 'none'。

常见错误 ​

1. 布尔属性写成字符串,开关永远为真

  • 现象:<switch checked="false" /> 页面上开关是打开的,isOpen 明明是 false。
  • 原因:属性值没加 &#123;&#123;&#125;&#125; 时被当成字符串 "false",非空字符串转布尔为 true。
  • 修法:任何要传布尔值/变量/表达式的地方统一 checked="&#123;&#123;false&#125;&#125;"、hidden="&#123;&#123;loaded&#125;&#125;",记住小程序里"内容和属性都靠 &#123;&#123;&#125;&#125;"。

2. 用 push 的返回值去 setData,列表变成数字

  • 现象:搜索历史追加一条后,keywords 变成 1、2 这样的数字,页面渲染崩掉。
  • 原因:Array.prototype.push 返回的是新数组长度,不是数组本身;this.setData({ keywords: this.data.keywords.push(kw) }) 等于把长度赋给了它。
  • 修法:用展开运算符一步到位 this.setData({ keywords: [kw, ...this.data.keywords] });若坚持 push,先 push 再单独 setData({ keywords: this.data.keywords })。

3. 取数据漏了 data 这一层

  • 现象:console.log(this.keyword) 输出 undefined,输入框内容拿不到。
  • 原因:习惯 Vue 的 this.xxx,小程序里所有状态都挂在 this.data 下。
  • 修法:读用 this.data.keyword,写必须走 this.setData({...}),直接 this.keyword = x 不会触发视图更新。

4. wx:key 写法不对

  • 现象:控制台警告 wx:key 无效,列表更新后错位、选中态乱窜。
  • 原因:把 key 当成插值写成了 wx:key="&#123;&#123;item.id&#125;&#125;",或对象数组里仍写 *this。
  • 修法:wx:key 不加 &#123;&#123;&#125;&#125;;对象数组写属性名 wx:key="id",简单类型数组写 wx:key="*this"。

5. showLoading 与 showToast 互相打架

  • 现象:真机上"查询成功"的提示一闪而过,甚至看不见;或者加载框一直挂着不消失。
  • 原因:两者共用同一个底层弹层,hideLoading 也会把 showToast 一起隐藏;把 hideLoading 放在 complete 里,就必然在成功提示弹出后立刻把它干掉。
  • 修法:showLoading 写在请求前;success 里直接 showToast(它会自动覆盖并定时消失,不用再 hideLoading);只在 fail 里调 hideLoading。

6. 判断空数组用 === []

  • 现象:wx:if="&#123;&#123;keywords === []&#125;&#125;" 永远为假,标题该隐藏时隐藏不掉。
  • 原因:数组是引用类型,两个空数组在内存里是不同对象,比较永远不相等。
  • 修法:用 keywords.length === 0 / !keywords.length,渲染判断写成 wx:if="&#123;&#123;keywords.length&#125;&#125;"。

7. 异步 API 当同步用

  • 现象:const res = wx.chooseMedia({...}); console.log(res) 拿到 undefined;wx.getStorage 直接取值也拿不到。
  • 原因:带 success/fail/complete 的都是异步 API,结果只能从回调里出;只有以 Sync 结尾的才是同步返回。
  • 修法:能用同步版就用同步版(getStorageSync);异步 API 优先用 async/await(文档标注"支持 Promise 风格调用"的),或老实写 success 回调。

8. 开发阶段请求被域名校验拦住

  • 现象:换了个没备案的接口,请求直接失败,Network 里看不到响应。
  • 原因:小程序默认只允许请求已配置的合法域名。
  • 修法:练习阶段在"详情 → 本地设置"勾选"不校验合法域名、TLS 版本以及 HTTPS 证书",真机预览时同时打开调试模式;上线前必须把域名配到后台,这个开关只对开发有效。

练习 ​

练习 1|搜索历史的健壮性 在课堂搜索历史的基础上补齐四个边界:输入为空或全空格时给 wx.showToast({ icon: 'none' }) 提示并 return;同一关键词重复搜索不重复插入;新关键词插到列表最前面;搜索成功后清空输入框。要求全部用 setData 驱动,禁止直接改 this.data。

练习 2|历史记录持久化 把搜索历史用 wx.setStorageSync 存到本地,页面 onLoad 时用 wx.getStorageSync 读回来渲染;新增和删除后都要同步更新本地存储。最后用调试器的 Storage 面板验证:刷新编译后历史仍在,点"清空"后 Storage 里的键也被删掉(用 removeStorageSync,不要用 clearStorageSync)。

练习 3|书单页的加载体验 给查询书单加上完整的交互反馈:请求前 showLoading 带 mask: true 防连点,成功 showToast 提示条数,失败 showToast 提示错误并 hideLoading,最终数据用 setData 渲染成列表(记得 wx:key)。要求在真机上验证成功提示能完整显示约 1.5 秒。

练习 4|表单事件与数据收集 用 form + button form-type="submit" 收集姓名、性别、爱好、籍贯,提交时在 bindsubmit 的事件对象里打印 ev.detail.value。观察:input 用 model:value 双向绑定后是否还需要从表单里取;radio-group / checkbox-group 的 change 事件返回值与 submit 返回值有何不同。

练习 5|Promise 风格改造 用 wx.chooseMedia 选一张图并渲染到页面上,分别用回调写法和 async/await 写法各实现一次,比较两种写法的嵌套层数。再追问:如果选完图后要紧接着发一次上传请求、再提示成功,两种写法各长什么样?

原视频对照 ​

#视频时长
1网络请求-不校验合法域名04:51
2界面交互-加载框和提示框11:44
3界面交互-提示框注意事项03:44
4本地存储API10:20
5API特征-同步和异步07:36
6API特征-Promise异步09:11
7综合练习-搜索框18:33

小结 ​

  1. 模板语法层面最容易踩的坑只有两类:该加 &#123;&#123;&#125;&#125; 的地方没加(布尔属性、表达式),不该加的地方加了(wx:key)。
  2. 小程序的数据流是单向的:读 this.data.xxx,写 this.setData({...}),任何绕过 setData 的赋值都不会反映到视图上。
  3. 数组更新不要依赖 push/unshift 的返回值,展开运算符 + 新数组是最不容易出错、也最方便调整顺序的写法。
  4. 引用类型判空一律看 length,别拿 []、{} 做等值比较。
  5. 交互提示 API 三者共用一层,showToast 会覆盖 showLoading,hideLoading 会误伤 showToast——把 hideLoading 只放在 fail 分支是更稳的写法。
  6. 小程序 API 有三种调用风格(回调、Sync 同步、Promise),见到 Sync 优先用,见到"支持 Promise 风格调用"优先 async/await,能显著减少回调嵌套。
  7. 本地存储直接支持对象和数组,不需要 JSON.stringify / JSON.parse,但 clearStorageSync 会连别人的键一起清掉,慎用。
  8. 开发期"不校验合法域名"只是练习期的便利开关,上线前域名该配还得配;同时模拟器和真机在弹层时长、动画细节上存在差异,涉及交互反馈的功能务必真机验证。