本部分介绍 2 个内容: 前端数据绑定 与 事件捕获.

数据绑定让小程序的前端界面随数据变化而变化, 而事件捕获机制令小程序具备响应用户操作的能力.

pages/index文件夹下的 index.wxml, index.js 两个文件是我们本部分的主要战场. 先找到它们在哪里…

10. 数据绑定

10.1 MVC 模型

关于 MVC 模型, 在本博客的其它教程中也有提及, 这里再重复一下, 因为, 它确实很重要…

MVC 模型是一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码.

可以毫不夸张地说, MVC 模型指引着我们各种软件设计的大方向,

根据 MVC 模式思想, 我们可以把大到一个软件系统, 小到一个功能模块分解为 M (Model)、V (View) 和 C (Controller) 三个部分(层). 其中:

  • Model - 数据模型层: 使用合适的数据结构存储着软件(模块)中的数据.
  • View - 视图层: 粗浅来说, 它是用户看到的界面呈现. 更准确来说, 是软件(模块)中更靠近”用户”的那一层 (不一定是可视化的界面). View 是 Model 的表现.
  • Controller - 控制层: 实现了 Model 到 View 的映射 ( 根据 Model 来呈现 View ), 以及 View 到 Model 的反馈 ( 根据 View 层的变化来更新 Model ). 也就是说, Controller 实现了 Model 和 View 之间的双向映射 (同步).

盗个图来说, 大概就是这样:

MVC模式

10.2 建立主界面的数据模型

根据 MVC 模型的思想, 界面 ( View ) 是由数据 ( Model ) 决定的, 所以, 本节我们要建立主界面的数据模型, 并将其与上一节构建的界面关联起来.

再看一下上节做好的主界面:

最终实现效果

其中有少内容其实应该是 “活” 的, 而不是 “死” 的静态页面. 比如: 用户的关注数, 粉丝数, 发表的日记数, 以及日记列表中的内容.

这里我们来分析一下, 上述 “活” 的数据其实可以分作两类: (1) 与用户相关的信息 (2) 与日记相关的信息

因此, 我们分别把它们封装成 2 个对象, 并把它们放在主界面 Page对象的 data属性里.

打开 pages/index/index.js文件, 将其改为如下内容 ( 在原先的代码中加入了第 4 ~ 35 行 )

Code 10.2-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Page({
data: {
// 页面数据
userInfo: {
favCnt : 19,
fansCnt: 37,
diaryCnt: 188,
},
diaries: [{
title: '冰雪奇缘',
publishTime: new Date().getTime(),
cover: 'https://bailey.pinruikm.com/images/miniprogram/dr1.gif',
content: '小国阿伦黛尔因一个魔咒永远地被冰天雪地覆盖,为了寻回夏天,安娜公主和山民克里斯托夫以及他的驯鹿搭档组队出发,展开一段拯救王国的历险。',
readCnt: 19,
praiseCnt: 8,
author: 'Bailey'
},{
title: '小王子',
publishTime: new Date().getTime(),
cover: 'https://bailey.pinruikm.com/images/miniprogram/dr2.jpg',
content: '小王子从自己星球出发前往地球的过程中,所经历的各种历险。作者以小王子的孩子式的眼光,透视出成人的空虚、盲目,愚妄和死板教条,用浅显天真的语言写出了人类的孤独寂寞、没有根基随风流浪的命运。',
readCnt: 4,
praiseCnt: 3,
author: '小可爱'
},{
title: '白雪公主',
publishTime: new Date().getTime(),
cover: 'https://bailey.pinruikm.com/images/miniprogram/dr3.jpg',
content: '白雪公主受到继母皇后,逃到森林里,遇到七个小矮人的故事。',
readCnt: 12,
praiseCnt: 7,
audioPath: 'https://bailey.pinruikm.com/images/miniprogram/audio3.mp3',
audioDuration: 256,
author: '小宝贝'
}]
},
onLoad: function () {
// 页面加载时执行的代码
}
})

还记得吗, 在本教程的第一部分, 我们曾提到, 在调用页面初始化函数 Page()时需要传入一个 JavaScript 对象作为其初始化参数, 而这个对象主要包含 3 类东西: 页面数据, 生命周期回调函数 和 自定义函数. ( 忘记了的话, 回去看看吧: 第一部分: 6. 小结 )

所以, 把页面的数据模型放在 Page 的 data 属性上, 其实是微信小程序构架早就埋下的伏笔, 嘿嘿 ~

现在, 请把主界面的静态效果图和上面的代码对照着仔细看一下, 你会发现, 界面 ( View ) 和 数据 ( Model ) 有着天然的对应关系.

静下心来体会一下 MVC 分层思想在这里的体现…

严格来说, 这里的设计更符合 MVVC 模型的思想, 不过, 相对 MVC 而言, MVVC 可能对于初学者来说有些不太容易理解. 所以, 我们姑且按照 MVC 的思想来理解就好. 关键是你能明白分层设计的意义. 至于, 不严谨这个锅, 我还背得少吗 ?

感兴趣的同学可以百度一下 MVC 与 MVVC, 自寻一点烦恼…

10.3 数据绑定 ( Data Bind )

现在我们有了设计好的主界面 ( View ) 和对应的数据模型 ( Model ), 怎么把他们关联起来呢?

这就是控制层 ( Controller ) 该干的事了, 只是这控制层的代码, 微信小程序运行环境已经实现了, 我们只需要按照小程序开发的套路进行所谓的 “数据绑定” 即可.

其实, 就是告诉小程序, 数据该安放在界面中的哪个地方.

微信小程序开发文档里, 根据不同的情况, 把我们现在要做的数据到界面的映射工作分作了 3 种方式:

别忙着去看上面链接的文档, 先看我表演完再去…

10.3.1 绑定关注/粉丝/日记数

对于关注数、粉丝数 和 日记数, 它们都是 “离散 (单个)” 的数据值, 分别对应页面数据 data 中 userInfo 属性的 favCnt, fansCnt 和 diaryCnt

妈耶~ 好绕! 换个说法:

关注数、粉丝数 和 日记数分别对应着 data 中的userInfo.favCntuserInfo.fansCntuserInfo.diaryCnt

对于这种情况, 我们可以直接使用{ { x } }的形式来进行数据绑定 ( 注意下面代码中的第 7, 11, 15 行 )

这是pages/index/index.wxml中代码的一个局部, 相信你可以找到它们在哪里.

Code 10.3-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<view class="flex align-center p-t">
<view class="m-r">
<open-data class="avatar" type="userAvatarUrl"></open-data>
</view>
<view class="flex space-around w-full">
<view class="text-center">
<view>{{userInfo.favCnt}}</view>
<view class="text-xs">关注</view>
</view>
<view class="text-center">
<view>{{userInfo.fansCnt}}</view>
<view class="text-xs">粉丝</view>
</view>
<view class="text-center">
<view>{{userInfo.diaryCnt}}</view>
<view class="text-xs">日记</view>
</view>
</view>
</view>

保存, 刷新一下, 咦? 界面上也没什么变化嘛!

呵呵, 你去 pages/index/index.js里改一下第 5 行, 把 favCnt : 19 改成 favCnt : 999 再保存一下试试…

嘿嘿, 知道什么是数据绑定了吧!

10.3.2 绑定日记列表(列表渲染)

要绑定日记列表数据, 就有点点麻烦了, 因为它是一个数组 ( diaries, 回头看一下 Code 10.2-1 的第 9 ~ 35 行 )

这回就要用到微信小程序文档中所说的列表渲染了, 说白了, 就是要 “循环” 遍历输出, 可以使用 wx:for来处理.

在继续之前, 我们先找到 pages/index/index.wxml 中的日记列表部分的代码.

你会发现, 其实多条日记项的代码其实是差不多一个模式, 既然要 “循环” 输出, 我们留一项较复杂的即可 ( 带”音频播放器”的那一项):

Code 10.3-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- 日记列表部分 -->
<view>
<view class="diary-item">
<view class="flex p-sm align-start m-t-xs bg-white b-b">
<view class="flex column align-center m-r">
<view class="text-xl text-bold">29</view>
<view class="text-xs">4月</view>
</view>
<view class="flex-sub flex column">
<view class="flex justify w-full text-sm">
<view class="text-gray">
<text>12 次浏览</text> ·
<text>7 人点赞</text>
</view>
<view class="text-link">♪ Baily</view>
</view>
<view class="flex justify align-start w-full m-tb-xs">
<image class="book-cover" src="https://bailey.pinruikm.com/images/miniprogram/dr3.jpg" mode="aspectFill"></image>
<view class="flex-sub m-lr-sm">
<view class="text-md">白雪公主</view>
<view class="text-gray text-sm m-t-xxs">
<text class="text-ellipsis line-2">白雪公主受到继母皇后,逃到森林里,遇到七个小矮人的故事。</text>
</view>
</view>
</view>
<view class="audio flex align-center m-t-xs">
<view class="icon-play ic-xxl"></view>
<view class="flex-sub m-l">
<view class="flex text-xs">
<text>0:00</text>
<slider class="flex-sub" activeColor="#666" block-size="12"/>
<text>4:16</text>
</view>
</view>
</view>
<view class="footer">
<view class="icon-heart"> 点赞</view>
<view class="icon-fav"> 关注</view>
<view class="icon-comment"> 评论</view>
</view>
</view>
</view>
</view>
</view>

小心哟~ 最好使用折叠代码的小技巧, 准确找到配对的 <view></view> 再动手, 要不然… 嘿嘿, 你就凉凉了~

当然, 如果你的代码没像我上面这样漂亮地缩进, 那么, 我祝你好运!

日记项部分要绑定的东西可就多了: 发布日期, 浏览数, 点赞数, 标题, 作者, 封面图片, 日记内容, 甚至还有音频的时长…

但是, 别忘了, 日记数据是一个数组, 我们要遍历它.

修改上面这块代码 ( Code 10.3-2 ), 把它变成下面的样子:

Code 10.3-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- 日记列表部分 -->
<view>
<view class="diary-item" wx:for="{{diaries}}">
<view class="flex p-sm align-start m-t-xs bg-white b-b">
<view class="flex column align-center m-r">
<view class="text-xl text-bold">29</view>
<view class="text-xs">4月</view>
</view>
<view class="flex-sub flex column">
<view class="flex justify w-full text-sm">
<view class="text-gray">
<text>{{item.readCnt}} 次浏览</text> ·
<text>{{item.praiseCnt}} 人点赞</text>
</view>
<view class="text-link">♪ {{item.author}}</view>
</view>
<view class="flex justify align-start w-full m-tb-xs">
<image class="book-cover" src="{{item.cover}}" mode="aspectFill"></image>
<view class="flex-sub m-lr-sm">
<view class="text-md">{{item.title}}</view>
<view class="text-gray text-sm m-t-xxs">
<text class="text-ellipsis line-2">{{item.content}}</text>
</view>
</view>
</view>
<view class="audio flex align-center m-t-xs">
<view class="icon-play ic-xxl"></view>
<view class="flex-sub m-l">
<view class="flex text-xs">
<text>0:00</text>
<slider class="flex-sub" activeColor="#666" block-size="12"/>
<text>4:16</text>
</view>
</view>
</view>
<view class="footer">
<view class="icon-heart"> 点赞</view>
<view class="icon-fav"> 关注</view>
<view class="icon-comment"> 评论</view>
</view>
</view>
</view>
</view>
</view>

大家来找茬, 看看哪些地方变了?

第 3 行, wx:for开启了对 data.diaries数组的循环模式.

默认情况下, wx:for 将遍历到的每一项放在一个 item的变量里, 而顺序号( 数组下标变量 ) 则放在叫index的变量中.

所以, 注意诸如第 12 行处的数据绑定, 使用的是item.readCnt的形式, 其中 item 代码当前遍历到的那条日记数据.

如果你不想使用默认的 “item” 作为当前遍历项的名称, 那么可以加上 wx:for-item=”自定义名称”. 同样, 如果你不想使用默认的 “index” 作为当前项的下标变量名, 那么可以加上 wx:for-index=”自定义下标变量”. 例如:

1
2
3
<view wx:for="{{diaries}}" wx:for-index="idx" wx:for-item="diary">
第{{idx}}条日记: {{diary.title}}
</view>

有时, 你可能确实有必要自定义”遍历项名”和”下标变量名”, 比如… 多重循环嵌套时…

现在, 你的代码可能会被小程序开发平台”警告” ( 控制台 (Console) 中黄色的输出内容 )

大意是, 你使用了 wx:for 但是没使用 wx:key来指定循环项目的唯一标识… 嘿嘿, 又看不懂了吧…

幸运的是, 小程序开发文档中说 “如不提供 wx:key, 会报一个 warning, 如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略”.

哈哈, 我们的日记数组确实是”静态的”, 并且, 我们也不太关心”顺序”, 所以… 就由它吧 ~ 警告而已, 呵呵~

但我相信有不少同学和我一样, 有”代码洁癖”, 连警告都见不得! 如果是这样, 你可以加上 wx:key=”id” 即可. 至于是什么意思… 我来说句你可能听不懂的话吧: 小程序使用 key 指定的属性来跟踪循环项, 以使组件保持自身的状态, 并且提高列表渲染时的效率.

哈哈, 果然听不懂吧~ 不重要, 当作没看见, 以后你会懂的…

现在, 如果一切正常 ( 阿门! ), 你会看到主界面中出现了与数据对应的 3 条日记信息.

只是… 每条日记怎么都会有个”音频播放器” ?

嘿嘿, 接着看下一节…

10.3.3 条件渲染

日记数据 ( data.diaries ) 中, 只有第 3 条日记有音频信息 ( audioPath 和 audioDuration )

而目前的 index.wxml 的代码中并没有区分日记是否带有音频信息这件事情. 因此, 最终显示出的界面中每条日记都带了一个 “音频播放器”

我们来用小程序平台提供的 wx:if来处理一下它:

Code 10.3-4

1
2
3
4
5
6
7
8
9
10
<view wx:if="{{item.audioPath}}" class="audio flex align-center m-t-xs">
<view class="icon-play ic-xxl"></view>
<view class="flex-sub m-l">
<view class="flex text-xs">
<text>0:00</text>
<slider class="flex-sub" activeColor="#666" block-size="12"/>
<text>4:16</text>
</view>
</view>
</view>

( 这段代码只是 index.wxml 的一个局部, 相信你可以找到它在哪里 )

变化的只是第 1 行, 加了 wx:if="..."

这里, 你可以就把它当作 JavaScript 的 if 语句来理解, 即判断 item.audioPath是否有值? 有, 则渲染上面 1 ~ 10 行部分的东西, 否则, 直接忽略.

除了 wx:if 外, 还有 wx:elif, wx:else, 相当于 JavaScript 中的 else if 和 else. 需要的时候, 可以试用一下.

现在, 你可以看一下这 3 个文档, 并整理一下笔记了:

是不是在文档中, 还瞄到了 “模板” 和 “引用” 这两个东东? 呵呵, 能看懂就看看, 如果看不懂… 暂时放过它, 长大了你自然会懂.

11. 动态改变数据模型值

现在, 我们基本实现了界面和数据的绑定, 即: 最终呈现的界面效果是根据数据渲染出来的.

并且, 也尝试过, 如果改变数据模型值, 界面也会自动作相应变化. ( 如果还没试过, 赶快试一下吧~ )

本节我们插播一个内容, 学习如何动态改变小程序前端数据模型值.

打开 pages/index/index.js, 找到其中的onLoad函数. 它一直默默地躺在那里… 修改代码如下:

Code 11-1

1
2
3
4
5
6
onLoad: function () { // 页面加载时执行的代码
this.setData({
'userInfo.favCnt': 666,
'diaries[0].readCnt': 666
});
}

使用this.setData()方法可以改变当前页面 (Page) 的数据 (data).

其中 this指向代码运行时的”宿主”. 直白说, 这里的 this 即指代当前页面.

同时, 注意 this.setData() 方法所需的参数是一个 JavaScript 对象, 其中的每一个属性对应着一个需要改变的值.

代码中故意演示了如何改变单个值 ( userInfo.favCnt ) 和 数组中某个元素的属性值 ( diaries[0].readCnt ) 的情况, 注意体会.

保存代码, 界面刷新后, 你会发现, 关注数和第 1 篇日记的浏览数都变成了 666 .

WARNING!!! 此处有坑 !!!

在 Page 的生命周期回调函数 或 自定义函数中, 你都可以使用 this.data.xxx 的形式取到当前页面的数据值.

比如: 你可以在上面 Code 11-1代码的 第 2 行 和 第 6 行 前面 均插入 console.log( this.data.userInfo.favCnt );

控制台会先后输出 19 和 666, 即 userInfo.favCnt 改变前/后的值.

也就是说, 我们可以在 js 中使用 this.data.xxx 直接取到页面数据.

那么, 赋值呢? 如果在代码中插入this.data.userInfo.favCnt = 10086;是否会有效呢?

负责任的告诉你, 有效! 不信你可以设断点, 或 console.log( this.data.userInfo.favCnt ) 输出试试, 确实会变成 10086.

BUT!!! 界面不会跟着变, 也就是说, 如果使用直接赋值的方式, 界面不会自动与数据模型同步!! 好土!!

所以, 如果需要在 js 代码中动态改变数据值, 只能老老实实地使用 this.setData() 才会触发界面刷新.

数据绑定机制在很多前端框架中都有, 比如: Angular, VUE… 大同小异, 甚至语法都差不多.

回顾 MVC ( 或 MVVC ) , 其实真正的数据绑定应该是 “双向” 的, 即: 数据模型(Model)的变化应自动映射到界面(View)上, 相反, 界面上的变化也应自动反馈到数据模型.

但不幸的是, 微信小程序框架仅实现了所谓的”简易双向绑定“, 例如:

.wxml : <input model:value="{{name}}">

.js : Page({ data: { name: 'zhang' } });

于是, 页面初始时输入框中将显示 zhang, 随后若输入框中的值被用户更改(如: 改为 wang) , 则 data.name 将自动变成 wang.

这就是传说中的双向绑定. 看上去还不错吧 ~

呵呵, 但是话说回来, 结合我们的小程序, 如果在 index.wxml 中放入输入框: <input model:value="{{userInfo.nickName}}"> 试图对 userInfo.nickName 进行双向绑定, 你会发现微信小程序框架居然不支持 ~

官方文档是这样说的: “尚不能 data 路径” (自己看), 呵呵, 什么鬼意思… 简单说就是只能双向绑定 nickName, 而不能是 userInfo.nickName

不得不说, 这特么也太简易了点!

有些人直接把 VUE 引入到小程序中来, 从而借助 VUE 实现真正的双向绑定. 等你长大了, 你也可以试试…

现在, 我们界面中涉及的 “活数据” 还有 2 个地方没有绑定: 发布日期, 音频时长.

整理好前面的内容后, 我们再继续…

12. WeiXin Script (wxs)

到目前为止, 在主界面上, 我们还有两个东西没有绑定: 日记的发布日期和音频的时长.

观察 index.js 中的数据模型结构会发现, 之所以暂时没有绑定是因为这两个数据的值与界面上最终要呈现的样子之间有差异, 不能直接绑…

即: 发布日期是一个Date类似的数据, 而界面上呈现时需要把月和日拆开显示. 同样, 音频时长数据使用秒作为单位 ( 如: 256 表示 256秒 ), 而界面上呈现的是 4:16 的样子.

对于这种情况, 一般可有 2 种方法来处理:

(1) 预先把数据处理成最终需要的样子, 然后再绑定. 例如: 将整数 256 转换成字符串 “4:16”, 然后绑定. 你可以试试在 onLoad 中补充代码试着自己处理一下, 这里就不举例了.

(2) 在绑定的时候, 使用框架提供的 *”过滤器 (Filter)”*来处理. 例如: 一些框架中可以这样写: { { item.publishTime | date: 'D' } } , 即将 publishTime 格式化输出 2 位的日 (Day).

不幸的是, 微信小程序框架并没有提供所谓的过滤器… 差评!!

所以, 我们得自己来…

可以定义类似下面的函数来处理:

Code 12-1

1
2
3
4
5
6
7
8
9
10
function formatDate(date, pattern) {
switch(pattern) {
case 'Y': return date.getFullYear();
case 'M': return date.getMonth() + 1;
case 'D': return date.getDate();
default: return '';
}
}

// 使用方法: formatDate( new Date(), 'Y'); => 输出: 2020

“过滤器(Filter)” 一词来源于设计模式中的”过滤器模式”. 有空的话研究一下设计模式(Design Pattern) 对你长飞升上仙很有帮助.

在这里, 我们据说的过滤器主要表现为把输入的值”过滤”后输出为你所需要的值.

按照一般网站开发的做法, 我们可以把上面的函数放在一个独立的 .js 文件 ( 如: filter.js ) 里, 然后在 HTML 中引入 filter.js , 再然后, 调用其中的函数, 完成功能. 例如:

Code 12-2

1
2
3
4
5
6
7
8
9
<html>
<head>
....
<script type="text/javascript" src="filter.js"></script>
</head>
<body>
<script>document.write( formatDate(new Date(), 'Y') );</script>
</body>
</html>

呵呵, So naive !!

在小程序的 .wxml 文件中, 并不允许像上面 HTML 代码那样, 使用 <script></script>引入外部 .js 文件… 差评!!

但微信倒是搞出了一种名叫 WXS ( WeiXin Script ) 的东东 ( 一种自创的脚本语言 ), 可以让你在 .wxml 文件中引入定义在外部的 wxs 脚本.

我们来试试吧~

utils文件夹中创建一个名为filter.wxs的文件, 然后写入如下代码:

Code 12-3

1
2
3
4
5
6
7
8
9
10
11
function formatDate(date, pattern) {
switch(pattern) {
case 'Y': return date.getFullYear();
case 'M': return date.getMonth() + 1;
case 'D': return date.getDate();
default: return '';
}
}
module.exports = {
formatDate: formatDate
}

每一个 .wxs 文件是一个模块 (Module), 模块定义的函数都是私有的(private), 不能直接在模块的外面访问. 必须使用 module.exports的方式向外暴露, 模块外才可以访问.

模块化设计是一个不错的 Idea, 不过它并非微信创造出来的, 而是 ES6 (ECMAScript 6, 俗称的 JavaScript 的一个版本) 之后引入的机制.

不一定非得把 filter.wxs 文件放在 utils 文件夹里, 也可以别的地方. 但人家微信开发工具都已经帮你创建好了 utils 文件夹, 那就放在这里不香吗?

OK ~ 接下来, 我们来看怎么使用它…

pages/index/index.wxml文件第1行插入如下代码

1
<wxs module="filter" src="../../utils/filter.wxs"></wxs>

看上去很你 HTML 文档中引入外部 .js 文件的写法… 呵呵, 的确, 连作用都差不多.

然后, 在需要使用到 formatDate 函数的地方这样写:

Code 12-4

1
2
3
4
5
6
7
8
9
10
11
12
<wxs module="filter" src="../../utils/filter.wxs"></wxs>
....
<view class="diary-item">
<view wx:for="{{diaries}}" wx:key="id" class="flex p-sm align-start m-t-xs bg-white b-b">
<view class="flex column align-center m-r">
<view class="text-xl text-bold">{{filter.formatDate(item.publishTime, 'D')}}</view>
<view class="text-xs">{{filter.formatDate(item.publishTime, 'M')}}月</view>
</view>
.....
</view>
</view>
......

上面只是 index.wxml 中的一个片段, 关键是第 1, 6, 7 三行. 其它的代码只是为了帮助你找到应该修改的代码在哪里.

保存一下, 看看效果…

嘿嘿, 是不是报错了? 大概是如下图的样子:

报错

大概的意思是说, 我们的 formatDate 函数中的 date.getDate() 和 date.getMonth() 都不是函数…

别问我是怎么看出错误就出现在 formatDate 函数中的 ? 嘿嘿, 除了这里, 我们也没在别的地方写过 date.getDate() 啊 …

现在, 你是不是有股冲动, 想设个断点, 调试 (Debug) 一下, 看看到底是什么原因?

呵呵, 又天真了不是… wxs 里就不支持设断点调试! 差评!!

如果实在想对 wxs 代码进行所谓的调试, 那么只能使用 console.log() 将就一下…

这回悲催了! 我们只能靠猜了….

是我们传给 formateDate 的参数 item.publishTime 是空值吗? NO!

是因为 item.publishTime 不是 Date 类型数据, 所以没有 getDate() 和 getMonth() 方法吗? NO!!

打死你也想不到, wxs 虽然支持 Date 类型, 但是在 wxs 模块内如果你想使用 Date 类型的数据, 你必须使用 getDate() 方法来获得 Date 实例(对象). 换句话说, 你不能把一个 Date 类型的数据作为参数传递给 wxs 模块中的函数…

有图为证: ( 摘自 https://developers.weixin.qq.com/miniprogram/dev/reference/wxs/06datatype.html )

date

简直了! 碉堡了! ( 你很可能不知道我在抱怨什么, 没关系, 不重要, 我只是叨叨几句… )

说说怎么改吧…. 修改 Code 12-3 的代码, 变成下面的样子:

Code 12-5

1
2
3
4
5
6
7
8
9
10
11
12
13
function formatDate(date, pattern) {
// 假定传入参数date是从1970年1月1日00:00:00 UTC开始计算的毫秒数, 按照wxs的套路构造出Date对象
var d = getDate(date);
switch(pattern) {
case 'Y': return d.getFullYear();
case 'M': return d.getMonth() + 1;
case 'D': return d.getDate();
default: return '';
}
}
module.exports = {
formatDate: formatDate
}

关键是第 3 行, 看下第 2 行的注释.

既然这里假定传入参数是”毫秒数”, 那么假设成不成立呢?

呵呵 ~ 不成立! 自己看一下代码就知道, pages/index/index.js 中给 publishTime 的是 Date 类型值 ( 通过 new Date() 构造 )

所以, 还要继续改…

打开 pages/index/index.js找到所有的 publishTime: new Date() 替换为 publishTime: new Date().getTime()

也就是, 把日记的发布时间记录为 “毫秒数”, 而不是直接使用 Date 类型.

保存一下, 这回应该在主界面上正常看到发布日期了.

为了简便起见, 这里我们直接取了当前系统时间作为日记发表时间. 所以, 你看到的日志发布日期都是你的”今天”.

后期, 我们将会在服务器端数据库中记录真实的日记发布时间.

在 JavaScript 的世界里, 有一个很不错的处理时间型数据的工具: Moment.js , 推荐各位抽空学习一下.

对于本文这种对时间数据进行格式化的工作, Moment.js 比我们做得好多了!

那么… 为什么我们不在这里用一下 Moment.js 呢 ??

wxs 模块中, 可以使用 require 引入其它模块, 但 只能引入其它 wxs 模块 !

至少我没见过 Moment.js 有 wxs 的封装形式. 几乎就没有什么大家常用的, 知名的第三方库会封装成 .wxs 的形式.

所以, 如果要用第三方的库, 还是在 .js ( 比如: pages/index/index.js ) 中使用吧, 别扯什么 wxs

也许是我的打开方式不对, 或理解上有失偏颇.

如果你能明白我在吐槽什么, 同时你有更了的解决方案, 请告诉我! 不胜感激!

终于… 处理好了发布日期这个绑定项… 头发都掉光了…

接下来了, 我们来处理”音频时长”的绑定…

有了前面跳坑的经验, 现在处理音频时长的绑定就简单多了.

注意到, 在pages/index/index.js的数据模型中, 音频时长audioDuration是一个整数 (秒)

因此, 我们只需要在 utils/filter.wxs中补充相应的转换函数即可:

下面是最终 utils/filter.wxs的完整代码, 你可以直接复制.

Code 12-6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 取出时间中数据中的年/月/日部分
function formatDate(date, pattern) {
var d = getDate(date);
switch(pattern) {
case 'Y': return d.getFullYear();
case 'M': return d.getMonth() + 1;
case 'D': return d.getDate();
default: return '';
}
}

// 将XX秒转换为MM:SS的形式, 如: 将256秒转换为4:16
function sec2Min(num) {
if (!num) return '0:00';
var min = Math.floor(num / 60);
var sec = Math.floor(num % 60);
return min + ':' + ((sec < 10) ? ('0' + sec) : sec);
}

module.exports = {
formatDate: formatDate,
sec2Min: sec2Min
}

添加了第 12 ~ 17 行, 还有 21 行. 代码我就不解释了, 你自己研究.

然后, 接着修改 pages/index/index.wxml, 处理下面这块代码(片断):

1
2
3
4
5
6
7
8
9
10
11
12
....
<view wx:if="{{item.audioPath}}" class="audio flex align-center m-t-xs">
<view class="icon-play ic-xxl"></view>
<view class="flex-sub m-l">
<view class="flex text-xs">
<text>0:00</text>
<slider class="flex-sub" activeColor="#666" block-size="12"/>
<text>{{filter.sec2Min(item.audioDuration)}}</text>
</view>
</view>
</view>
....

关键是第 8 行进行了”音频时长”的绑定.

不得不说, 这一小节是我写得最痛苦的… 纯粹是为了介绍微信小程序框架中的所谓 Weixin Script (wxs) 而编出来的故事.

当然, 使用过滤器模式对最终输出的数据进行处理, 是一个不错的方案, 值得提倡! ( 它有点像装在水龙头上的净水器一样, 把水中的杂质滤除, 留下用户喜闻乐见的纯净 )

但在微信小程序环境下, 我们如果强行使用 wxs 来实现过滤器的话, 往往有诸多不便. 除非, 你只是对数据作简单的处理(过滤), 就像本例一样…

大多数情况下, 我宁愿使用本节开始部分提到的第1种方法来处理 ( 预先把数据处理成最终需要的样子. 例如: 将整数 256 转换成字符串 “4:16”, 直接绑定 )

下面, 来数一数这个传说中的微信 Script 的坑:

  • wxs 不是 JavaScript, 它是微信自己发明出来的脚本语言, 它比 JavaScript 弱多啦 ~

  • wxs 模块中可以引入其它模块, 但是, 引入的模块也必须是 wxs 模块, 不能是普通的 JavaScript 模块

  • 至今除了使用 console.log() , 我没发现有更好的调试 wxs 代码的方法. 腾讯是不是认为我们都是大神, 写代码都不带出错的?

    如果你发现了好的调试方法, 请不吝赐教!

  • wxs 支持的数据类型和书写语法与 JavaScript 相似, 但仅仅是相似, 而且还是与比较古老的 JavaScript 版本相似.

    不然, 你在 wxs 中用一下 let x = 4;或者 const x = 3;试试 ?

  • 建议掉坑时查阅 WXS 语法参考

13. 事件处理

前面的内容我们谈论了数据从数据模型层向视图层的流动过程, 本小节我们聊一聊数据由视图层向数据层的流动过程.

也许你会觉得奇怪, 本小节的标题不是”事件处理”吗? 怎么会扯上数据的流动问题?

这里, 我们所说的事件主要指的是用户的一些操作, 比如: 轻点 ( 在手机上叫 Tap, 类似电脑上的点击, Click ), 键盘输入等. 换个角度来看, 用户不正是通过轻点/输入等动作将信息告知你的程序吗? 而这些信息不正是我们据说的程序所处理的数据吗?

这样说来, 是不是逻辑就通了, 呵呵 ~

OK ~ 说正事… 看下图:

date

假设这是页面上的一块区域, 元素(组件) ① ~ ④ 依次嵌套在一起, 这里, 你点击了图中的 “Commit” 按钮.

考你一考: 点击事件将由哪个元素来处理?

呵呵 ~ 这不明摆着吗? 点击了按钮, 就不是由按钮来处理吗?

嘿嘿 ~ So young so naive…

事实上, 点击事件会依次被元素 ④ ③ ② ① 处理…

完整的事件处理过程由事件捕获(Capturing)和冒泡(Bubbling)两个阶段构成.

对于上图中的情况, 在捕获阶段事件由外向内传递, 当到达最里面的元素④后, 若④对此事感兴趣(监听)则进行响应, 然后”冒泡”到上一层③, 同样, ③感兴趣的话进行响应, 然后… 再冒泡到上一层…….

怎么证明呢? 来直接看代码:

Code 13-1 ( /pages/index/index.wxml 中随便找个合适的地方放入如下代码, 比如: 最后面 )

1
2
3
4
5
6
7
<view bindtap="onTap1">
<view bindtap="onTap2">
<view bindtap="onTap3">
<button bindtap="onTap4">Commit</button>
</view>
</view>
</view>

Code 13-2 ( /pages/index/index.js 中插入如下代码 )

1
2
3
4
5
6
7
8
9
// ...
{
// ...
onLoad: function() {},
onTap1: function() { console.log('1'); },
onTap2: function() { console.log('2'); },
onTap3: function() { console.log('3'); },
onTap4: function() { console.log('4'); }
}

( 关键是 5 ~ 8 行, 其它东东只是为了让你能找到代码插入的位置 )

OK~ 现在页面上应多出了一个 “Commit” 按钮, 用手戳它一下, 注意观察控制台的输出, 是不是依次输出了 4 3 2 1 ?

这能证明什么? 呵呵, 这就证明了, 事件是”由内向外”依次处理的. ( 注意看 Code 13-1 中那些 onTap 的位置, 以及它们和 Code 13-2 中 onTap 函数的对应关系 )

哟! 一不小心, 你似乎已经学会了怎么进行事件捕获和处理了… 我来帮你描述一下:

  • .wxml 中在需要捕获事件的组件(元素)中添加 bindtap 属性, 其值是事件回调函数的名称
  • .js 中添加对应的事件回调函数, 当 tap ( 轻触 ) 事件发生时, 此回调函数将被执行.

就这么简单! 但注意 2 点: (1) 事件的触发顺序 (2) 事件不止是被最里面的按钮捕获

所谓”回调函数”, 可以理解为, 此函数的不是由你的代码主动调用的, 而是当某个事件发生后, “折返回来” 调用的函数. 你的代码仅做了一个登记/注册.

上述事件处理过程也常被称作事件监听, 而事件回调函数被称作监听器 ( listener ). 这又涉及一个设计模式: 监听器模式.

在其它的技术中, 可以看到类似 addClickListener, setClickListener 之类的代码, 这回知道为什么叫 listener 了吧 ~

有时候, 我们可能在嵌套的多个组件上都进行了事件绑定, 但并不希望”事件冒泡”, 即”里层”组件的事件回调函数已经处理了此事件, 则不希望此事件还继续被”外层”组件的事件回调函数处理.

呵呵, 又看不懂了 ~

看着你小程序的主界面, 我们来举个例子…

假设我们希望用户在轻触(tap)一条日记项的时候转到日记详情页面, 于是在日记项的 view 上进行了事件绑定(bindtap), 触发后将转至详情页面. 而主界面的每一条日记项里不是还有”点赞”的图标吗? 我们同时也希望用户在轻触了”点赞”图标时触发点赞功能, 于是同样在”点赞”的 view 上进行了事件绑定(bindtap).

那么问题来了… 当用户轻触了 “点赞” 图标时会发生什么?

按前面我们讲的故事, 是不是触发了点赞功能的逻辑后, 界面将还被切换到日记详情页面? ( 自行脑补画面 )

这可不是我们想要的结果!

怎么办呢? 来~ 改把 Code 13-1 的代码改一下:

1
2
3
4
5
6
7
<view bindtap="onTap1">
<view bindtap="onTap2">
<view bindtap="onTap3">
<button catchtap="onTap4">Commit</button>
</view>
</view>
</view>

再试一次, 这回是不是只会输出 “4” 了?

哈哈, 又学会一招: catchtap可以阻止事件的正常冒泡. ( 第 4 行 )

微信提供的不同组件支持的事情不尽相同, 用到的时候记得查阅微信小程序文档.

另外, 事件回调时还可以带参数. 看例子:

.wxml 文件

1
<button bindtap="onButtonTap" data-name="zhangsan" data-age="18" data-first-name="zhang" data-lastName="san">Commit</button>

.js 文件

1
2
3
4
5
6
7
8
9
10
// 回调函数, 与上面代码的 bindtap="onButtonTap" 对应
onButtonTap: function(e) {
// 通过回调函数第0个形参可取得事件相关信息
// e.currentTarget.dataset 可取得组件中以"data-"开头的自定义属性值(数据集)
const params = e.currentTarget.dataset;
console.log( params.name ); // zhangsan
console.log( params.age ); // 18
console.log( params.firstName ); // zhang, 坑!!! .wxml文件中是 data-first-name
console.log( params.lastname ); // san, 大坑!!! .wxml文件中是 data-lastName (大写N), 此处应是 param.lastname (小写n)
},

有时候, 某事件相关参数也会通过 e.detail.xxx 传递, 注意关注微信小程序文档.

好了, 本小节我们暂时就说这么多.

事件触发后具体功能实现还需要配合其它知识才能完成, 我们将在后续部分配合实例讲解.

~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~

到目前为止的代码: daily-reading_part3.zip

14. 小结

本部分教程最重要的不是那些基础语法, 或代码怎么写的问题, 而是…

  • 充分领悟以 MVC ( MVVC ) 为代表的分层设计思想, 以及引申出的数据绑定概念.
  • 文中提到的过滤器 (filter) 模式是很常见且实用的设计模式, 理解过滤器是什么东东? 以及它所体现的设计思想.
  • 事件触发后包含了事件捕获与冒泡两个阶段, 我们借助事件监听机制来进行事件响应.

以上即是本部分要学会的”套路”, 这已经超出了微信小程序开发的范畴, 你可以在各种现有技术, 即使是未来出现的新技术中都能看到它们的影子.

以上… 希望能帮助你在今后学习别的技术时, 事半功倍, 快速上手 ~