主题
数据绑定、渲染与表单(二)
本页对应 数据绑定、渲染与表单,共 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="{{false}}" 才是布尔值。
简易双向绑定:model:value
为什么需要:输入框输入后,希望 data 自动更新,不用每次手动取 e.detail.value。
怎么想:小程序提供 model:value="{{数据名}}",目前只能用在 input 和 textarea 组件中。
怎么做:如上例 <input type="text" model:value="{{message}}" />,下面再用 {{message}} 展示,输入时页面文字会跟着变。
易错点:不要把它当成所有组件通用;转写稿的综合练习里,搜索框正是用 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 是列表项唯一标识,不写 {{}}。
怎么做:笔记代码:
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="{{students}}" wx:key="id",对象单元只写属性名;history 是简单类型,推荐 wx:key="*this"。需要改索引名用 wx:for-index,改单元名用 wx:for-item。
易错点:wx:key 不写 {{}};简单数组不要写 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 上,绑在单个选项上拿不到整组结果。
课件图示








代码示例
示例 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="{{false}}"才是布尔假。- 小程序中内容绑定和属性绑定都要写
{{}},这与 Vue 不同。 model:value="{{message}}"是目前唯一的简易双向绑定语法,仅支持input和textarea。wx:else不能单独使用,必须紧跟在wx:if(或wx:elif)后面。hidden的语义是"值为真时隐藏",所以hidden="{{loaded}}"表示加载完就藏起来;并且hidden依赖[hidden] { display: none; }才能生效。wx:key不加{{}}:数组单元是对象时直接写属性名(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。 - 原因:属性值没加
{{}}时被当成字符串"false",非空字符串转布尔为true。 - 修法:任何要传布尔值/变量/表达式的地方统一
checked="{{false}}"、hidden="{{loaded}}",记住小程序里"内容和属性都靠{{}}"。
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="{{item.id}}",或对象数组里仍写*this。 - 修法:
wx:key不加{{}};对象数组写属性名wx:key="id",简单类型数组写wx:key="*this"。
5. showLoading 与 showToast 互相打架
- 现象:真机上"查询成功"的提示一闪而过,甚至看不见;或者加载框一直挂着不消失。
- 原因:两者共用同一个底层弹层,
hideLoading也会把showToast一起隐藏;把hideLoading放在complete里,就必然在成功提示弹出后立刻把它干掉。 - 修法:
showLoading写在请求前;success里直接showToast(它会自动覆盖并定时消失,不用再hideLoading);只在fail里调hideLoading。
6. 判断空数组用 === []
- 现象:
wx:if="{{keywords === []}}"永远为假,标题该隐藏时隐藏不掉。 - 原因:数组是引用类型,两个空数组在内存里是不同对象,比较永远不相等。
- 修法:用
keywords.length === 0/!keywords.length,渲染判断写成wx:if="{{keywords.length}}"。
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 | 本地存储API | 10:20 |
| 5 | API特征-同步和异步 | 07:36 |
| 6 | API特征-Promise异步 | 09:11 |
| 7 | 综合练习-搜索框 | 18:33 |
小结
- 模板语法层面最容易踩的坑只有两类:该加
{{}}的地方没加(布尔属性、表达式),不该加的地方加了(wx:key)。 - 小程序的数据流是单向的:读
this.data.xxx,写this.setData({...}),任何绕过setData的赋值都不会反映到视图上。 - 数组更新不要依赖
push/unshift的返回值,展开运算符 + 新数组是最不容易出错、也最方便调整顺序的写法。 - 引用类型判空一律看
length,别拿[]、{}做等值比较。 - 交互提示 API 三者共用一层,
showToast会覆盖showLoading,hideLoading会误伤showToast——把hideLoading只放在fail分支是更稳的写法。 - 小程序 API 有三种调用风格(回调、
Sync同步、Promise),见到Sync优先用,见到"支持 Promise 风格调用"优先async/await,能显著减少回调嵌套。 - 本地存储直接支持对象和数组,不需要
JSON.stringify/JSON.parse,但clearStorageSync会连别人的键一起清掉,慎用。 - 开发期"不校验合法域名"只是练习期的便利开关,上线前域名该配还得配;同时模拟器和真机在弹层时长、动画细节上存在差异,涉及交互反馈的功能务必真机验证。