第3章 小程序开发基础 本章将从架构层面介绍小程序的开发框架,包括视图层描述语言WXML(WeiXin Markup Language,微信标记语言)和WXSS(WeiXin Style Sheets,微信样式表),以及基于JavaScript 的逻辑层等。 本章学习目标: 了解小程序的生命周期与页面的生命周期。 了解小程序框架的基本功能。 了解接口和组件。 熟悉小程序注册和页面注册的方法。 熟悉模板,样式导入,模块化的操作方法。 掌握数据绑定的用法。 掌握列表渲染和条件渲染的使用方法。 掌握事件的绑定与处理方法。 掌握小程序样式文件的使用方法。 3.1认识小程序的生命周期 【任务要求】 修改示例项目中的app.js文件,要求当小程序在执行以下操作时在调试器的Console面板中输出对应的信息。 (1) 当小程序第一次启动时,输出当前系统的时间。 (2) 当小程序被放置到后台时,输出“小程序已被隐藏”。 (3) 当小程序从后台被唤醒时,输出进入的场景值。 【任务分析】 本次任务主要涉及对小程序生命周期中三种状态的处理,一个是第一次启动,然后是隐藏和显示。其中比较重要的一点是当小程序从后台被唤醒时,携带的包含场景值的参数,它可以用来帮助开发者更好地响应用户的操作。 【任务操作】 (1) 打开示例项目的app.js文件,在onLaunch()函数内部的开始处,新增如下所示的两行代码。 //引用util文件夹中的公共函数 const util = require('utils/util.js') //将当前系统时间格式化输出 console.log(util.formatTime(new Date(Date.now()))) (2) 在app.js文件的globalData参数前,新增如下用于处理小程序被隐藏和唤醒事件的函数。 onShow:function(opts) { //输出opts中表示场景值的scene变量 console.log(opts.scene) }, onHide:function() { console.log('小程序已被隐藏') }, 完成后的app.js文件内容结构大致如下。 App({ onLaunch:function(opts) { const util = require('utils/util.js') console.log(util.formatTime(new Date(Date.now()))) // 展示本地存储能力 … }, onShow:function(opts) { console.log(opts.scene) }, onHide:function() { console.log('小程序已被隐藏') }, globalData: { userInfo: null } }) 微信小程序实用教程 第 3 章小程序开发基础 注意: 代码中加粗部分内容仅作阅读重点提示用,无其他特殊含义。下同。 (3) 保存文件,编译项目,在Console面板中查看小程序启动时输出的时间信息,如图31所示。 图31小程序启动时的输出 可以看到,小程序的第一次启动触发了onLaunch()函数和onShow()函数。 (4) 在工具栏中单击“切后台”按钮,让小程序进入后台状态,然后在模拟器中选择“1011: 扫描二维码”,唤醒小程序到前台。在这个过程中观察到的输出如图32所示。 图32小程序转入后台和切换前台时输出 【相关知识】 小程序主要有前台和后台两种运行状态。当小程序处于前台时,它可以调用所有的API(Application Programming Interface,应用程序编程接口),为用户提供服务; 当用户单击小程序运行页面左上角“关闭”按钮,或者按了设备Home键离开微信,小程序就转入了后台,此时小程序只能调用部分API,并随时可能被销毁。小程序只有在进入后台一定时间后,或者系统资源占用过高时,才会被真正销毁。当用户再次进入微信或再次打开小程序,又会从后台变为前台状态。 场景值表示的是小程序是通过什么途径从后台切换到前台的。目前小程序支持的场景值说明见表31。 表31小程序场景值说明 场景值ID 说明 场景值ID 说明 场景值ID 说明 1001 发现栏小程序主入口,“最近使用”列表(基础库2.2.4版本起包含“我的小程序”列表) 1032 手机相册选取一维码 1064 微信连WiFi状态栏 1005 顶部搜索框的搜索结果页 1034 微信支付完成页 1067 公众号文章广告 1006 发现栏小程序主入口搜索框的搜索结果页 1035 公众号自定义菜单 1068 附近小程序列表广告 1007 单人聊天会话中的小程序消息卡片 1036 App分享消息卡片 1069 移动应用 1008 群聊会话中的小程序消息卡片 1037 小程序打开小程序 1071 钱包中的银行卡列表页 1011 扫描二维码 1038 从另一个小程序返回 1072 二维码收款页面 1012 长按图片识别二维码 1039 摇电视 1073 客服消息列表下发的小程序消息卡片 1013 手机相册选取二维码 1042 添加好友搜索框的搜索结果页 1074 公众号会话下发的小程序消息卡片 1014 小程序模板消息 1043 公众号模板消息 1077 摇周边 1017 前往体验版的入口页 1044 带shareTicket的小程序消息卡片 详情 1078 连WiFi成功页 1019 微信钱包 1045 朋友圈广告 1079 微信游戏中心 1020 公众号profile页相关小程序列表 1046 朋友圈广告详情页 1081 客服消息下发的文字链 续表 场景值ID 说明 场景值ID 说明 场景值ID 说明 1022 聊天顶部置顶小程序入口 1047 扫描小程序码 1082 公众号会话下发的文字链 1023 安卓系统桌面图标 1048 长按图片识别小程序码 1084 朋友圈广告原生页 1024 小程序profile页 1049 手机相册选取小程序码 1089 微信聊天主界面下拉,“最近使用”栏(基础库2.2.4版本起包含“我的小程序”栏) 1025 扫描一维码 1052 卡券的适用门店列表 1090 长按小程序右上角菜单唤出最近使用历史 1026 附近小程序列表 1053 搜一搜的结果页 1091 公众号文章商品卡片 1027 顶部搜索框搜索结果页“使用过的小程序”列表 1054 顶部搜索框小程序快捷入口 1092 城市服务入口 1028 我的卡包 1056 音乐播放器菜单 1095 小程序广告组件 1029 卡券详情页 1057 钱包中的银行卡详情页 1096 聊天记录 1030 自动化测试下打开小程序 1058 公众号文章 1097 微信支付签约页 1031 长按图片识别一维码 1059 体验版小程序绑定邀请页 1099 页面内嵌插件 1102 公众号profile页服务预览 注意: 由于Android系统限制,目前还无法获取到按 Home 键退出到桌面,然后从桌面再次进小程序的场景值,对于这种情况,会保留上一次的场景值。 监听小程序生命周期变化的函数见表32。 表32小程序生命周期函数 属性 类型 描述 触 发 时 机 onLaunch() Function 生命周期回调——监听小程序初始化 小程序初始化完成时(全局只触发一次) onShow() Function 生命周期回调——监听小程序显示 小程序启动或从后台进入前台显示时 onHide() Function 生命周期回调——监听小程序隐藏 小程序从前台进入后台时 其中,onLaunch()函数的回调参数说明见表33。 表33onLaunch()函数回调参数说明 属性 类型 说明 path String 启动小程序的路径 scene Number 启动小程序的场景值 query Object 启动小程序的 query 参数 续表 属性 类型 说明 shareTicket String 当其他用户通过分享的小程序卡片打开小程序时,该字段包含转发的信息 referrerInfo Object 来源信息。从另一个小程序、公众号或App进入小程序时返回。否则返回{} onShow()函数的回调参数说明见表34。 表34onShow函数回调参数说明 属性 类型 说明 path String 小程序切前台的路径 scene Number 小程序切前台的场景值 query Object 小程序切前台的query参数 ShareTicket String 当其他用户通过分享的小程序卡片打开小程序时,该字段包含转发的信息 referrerInfo Object 来源信息。从另一个小程序、公众号或App进入小程序时返回。否则返回{} referrerInfo的结构说明见表35。 表35referrerInfo结构说明 属性 类型 说明 AppID String 来源小程序、公众号或App的AppID extraData Object 来源小程序传过来的数据,scene=1037或1038时支持 当从表36中的场景进入小程序时,返回的referrerInfo才是有效的。 表36返回有效referrerInfo的场景 场景值 场景 AppID含义 1020 公众号profile页相关小程序列表 来源公众号 1035 公众号自定义菜单 来源公众号 1036 App分享消息卡片 来源App 1037 小程序打开小程序 来源小程序 1038 从另一个小程序返回 来源小程序 1043 公众号模板消息 来源公众号 3.2认识小程序页面的生命周期 【任务要求】 为当前示例项目的首页和查看日志页添加监听页面生命周期变化的函数,并在对应的生命周期处理函数中编写向调试器Console面板输出页面状态变化的代码。在首页跳转到日志页面时,需要携带名为message1,值为Hello以及名为message2,值为Logs的两个参数,并在日志页面输出获取到的参数。完成后在两个页面之间跳转,观察控制台输出,理解小程序页面生命周期的变化。 【任务分析】 除了小程序本身的生命周期外,小程序的每个页面也有自己的生命周期。相较于小程序的生命周期来说,每个页面的生命周期包含加载、准备、显示、隐藏和卸载这几个阶段。本次任务使用现有的两个页面之间的跳转,通过观察对应生命周期函数的输出来理解页面生命周期的变化。 【任务操作】 (1) 打开示例项目,在index页面的index.js文件中,修改onLoad函数,使其可以在调试器中输出运行的信息。新增用于监听页面生命周期变化的onShow()、onReady()、onHide()和onUnload()函数。同时修改bindViewTap()函数中的URL部分,使其带着参数message1和message2跳转到日志页面。完成后的index.js文件内容大致如下。 //index.js Page({ data: { … }, //事件处理函数 bindViewTap: function() { wx.navigateTo({ url: '../logs/logs?message1=Hello&message2=Logs' }) }, onLoad: function () { console.log("首页加载") if (app.globalData.userInfo) { … } }, getUserInfo: function(e) { … }, onShow: function () { console.log("首页显示") }, onReady: function () { console.log("首页渲染完成") }, onHide: function () { console.log("首页隐藏") }, onUnload: function () { console.log("首页卸载") } }) (2) 打开logs.js,在onLoad()函数中新增一行代码用于输出首页传递过来的参数信息。同样新增用于监听页面生命周期变化的onShow(),onReady(),onHide()和onUnload()函数。完成后的logs.js函数大致如下。 //logs.js Page({ data: { … }, onLoad: function (query) { console.log("日志页加载", query) this.setData({ … }) }) }, onShow: function () { console.log("日志页显示") }, onReady: function () { console.log("日志页渲染完成") }, onHide: function () { console.log("日志页隐藏") }, onUnload: function () { console.log("日志页卸载") } }) (3) 保存文件,编译项目。在首页上单击用户头像跳转到日志页面,观察如图33所示调试器的输出。 图33页面生命周期函数调用示例 【相关知识】 小程序页面的生命周期,会经历加载、显示、渲染完成、隐藏和卸载这几个过程,涉及的生命周期回调函数说明见表37。 表37页面生命周期回调函数 函数名 描述 onLoad() 页面加载时触发。一个页面只会调用一次,可以在onLoad()的参数中获取打开当前页面路径中的参数 onShow() 页面显示/切入前台时触发 onReady() 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互 onHide() 页面隐藏/切入后台时触发。如navigateTo或底部Tab切换到其他页面,小程序切入后台等 onUnload() 页面卸载时触发。如redirectTo或navigateBack到其他页面时 在小程序中,所有页面的路由以栈的形式维护。当发生页面切换时,页面栈的变化见表38的说明。 表38各种情况下页面栈的表现 路 由 方 式 页面栈表现 初始化 新页面入栈 打开新页面 新页面入栈 页面重定向 当前页面出栈,新页面入栈 页面返回 页面不断出栈,直到目标返回页 Tab切换 页面全部出栈,只留下新的 Tab 页面 重加载 页面全部出栈,只留下新的页面 在上述路由方式中,每种情况下的路由触发方式以及对应页面响应的生命周期函数见表39。 表39路由触发时机和页面生命周期函数 路由方式 触 发 时 机 路由前页面 路由后页面 初始化 小程序打开的第一个页面 onLoad(), onShow() 打开新页面 调用wx.navigateTo接口或使用组件 onHide() onLoad(), onShow() 页面重定向 调用wx.redirectTo接口或使用组件 onUnload() onLoad(), onShow() 页面返回 调用wx.navigateBack接口或使用组件或用户单击左上角返回按钮 onUnload() onShow() Tab切换 调用wx.switchTab接口或使用组件或用户切换Tab 参考表310 重启动 调用wx.reLaunch接口或使用组件 onUnload onLoad(), onShow() Tab切换时对应页面的生命周期变化见表310(以A、B页面为tabBar页面,C是从A页面打开的页面,D页面是从C页面打开的页面为例)。 表310Tab切换页面的生命周期变化 当 前 页 面 路由后页面 触发的生命周期(按顺序) A A A B A. onHide(), B.onLoad(), B.onShow() A B(再次打开) A. onHide(), B.onShow() C A C. onUnload(), A.onShow() C B C. onUnload(), B.onLoad(), B.onShow() D B D. onUnload(), C.onUnload(), B.onLoad(), B.onShow() D(从转发进入) A D. onUnload(), A.onLoad(), A.onShow() D(从转发进入) B D. onUnload(), B.onLoad(), B.onShow() 关于页面的路由,需要注意以下几点。 (1) navigateTo(),redirectTo()只能打开非tabBar页面。 (2) switchTab()只能打开tabBar页面。 (3) reLaunch()可以打开任意页面。 (4) 页面底部的tabBar由页面决定,即只要是定义为tabBar的页面,底部都有tabBar。 tabBar部分可以参见2.1节内容,涉及的接口可以参见14.7节的内容,组件可以参考9.1节的内容。 在本次任务中,可以看到,加载首页的时候依次执行的函数是onLoad(),onShow()和onReady()。在单击首页头像后,首页被隐藏,首页的onHide()函数被执行,然后跳转到日志页面。日志页面的onLoad()函数运行,同时也获取到了首页传递过来的参数message1和message2。紧接着,日志页面的onShow()和onReady()函数被执行。 在index.js文件中,为要跳转到的logs页面地址添加了message1和message2两个参数。路径携带参数的格式要求为: 参数与路径之间使用“?”分隔,参数键与参数值之间用“=”相连,不同参数用“&”分隔,例如'path?key=value&key2=value2'。 结合小程序的生命周期和页面的生命周期,在小程序运行的过程中,各个生命周期函数的调用顺序可参考如图34所示说明。 图34小程序和页面生命周期变化 3.3概览MINA框架 【任务要求】 在pages目录下新建一个Chapter_3目录,在Chapter_3目录下新建一个名为mina的页面。要求在页面上显示文本“This is a text”和一个Change Text按钮,并实现单击按钮后将文本更改为“This is another text”的功能。 【任务分析】 MINA是小程序使用的框架,其核心是一个响应的数据绑定系统。本次任务通过实现一个简单的单击按钮更改文字的功能,来体会框架中视图层和逻辑层的联系。 【任务操作】 (1) 打开示例项目,在app.json文件的pages数组中,新增第一项"pages/Chapter_3/mina/mina"。保存文件,编译项目,可以看到开发者工具已经建好了mina页面,模拟器中正在运行的也是mina页面。完成后的目录结构如图35所示。 图35新建mina页面后的项目目录结构 (2) 打开mina.js文件,在Page函数的页面初始数据处,添加一个名为text,值为“This is a text”的初始数据。完成后的mina.js文件内容大致结构如下。 // pages/Chapter_3/mina/mina.js Page({ /** * 页面的初始数据 */ data: { text:"This is a text" }, … }) (3) 打开mina.wxml文件,删除原文件内容,新增用于显示mina.js文件中text变量的代码,同时增加一个按钮,在按钮的属性中,为单击事件绑定一个名为changeText()的函数。完成后的mina.wxml文件内容如下。 {{text}} (4) 回到mina.js文件,需要为按钮单击事件的处理函数changeText()编写代码逻辑,实现更改文字的功能。完成后的mina.js文件内容结构大致如下。 // pages/Chapter_3/mina/mina.js Page({ /** * 页面的初始数据 */ data: { text:"This is a text" }, … /** * 用户单击右上角分享 */ onShareAppMessage: function () { }, changeText:function(){ this.setData({ text:"This is another text" }) } }) (5) 保存所有文件,编译项目,在模拟器中查看mina页面表现,单击按钮,观察文字是否被改变。正常情况下,页面表现应如图36所示。 图36mina页面单击按钮前(左)和单击按钮后(右) 【相关知识】 MINA框架(后文简称为“框架”)是微信小程序的开发框架。框架的目标是通过尽可能简单、高效的方式让开发者可以在微信中开发具有原生应用体验的服务。 框架中,整个小程序的内容分为两个层次: 视图层(View)和逻辑层(App Service)。视图层是小程序的“外观”,它规定了小程序页面的结构和样式,决定了小程序内容的展示; 逻辑层是小程序的“内涵”,它控制了小程序的生命周期和数据处理等。除了处理本地的业务逻辑之外,在开发小程序的过程中逻辑层往往还需要通过HTTP同远程服务器进行数据交流。 框架提供了自己的视图层描述语言规范: WXML和WXSS,以及基于JavaScript的逻辑层,并在视图层与逻辑层间提供了数据传输和事件系统,可以让开发者将重点放在数据与视图上。 整个框架中最核心的部分就是视图层和逻辑层之间的数据绑定和事件响应系统。视图层中的信息通过事件绑定携带参数向逻辑层传递,逻辑层的数据通过数据绑定向视图层传递。框架可以让数据与视图非常简单地保持同步。当作数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。 在本次任务中,通过框架将逻辑层(mina.js文件)数据中的text与视图层(mina.wxml文件)的text进行了绑定,所以在页面一打开的时候会显示“This is a text”。当单击按钮的时候,触发了按钮的单击事件,视图层会发送单击事件的处理函数changeText()给逻辑层,逻辑层找到并执行对应的函数。changeText()函数触发后,逻辑层执行setData的操作,将data中的text从“This is a text”变为“This is another text”,因为该数据和视图层已经绑定了,从而视图层上显示的内容会自动改变为“This is another text”。 框架除了最为核心的数据绑定和事件响应系统外,还管理了整个小程序的页面路由,可以做到页面间的无缝切换,并给予页面完整的生命周期。开发者需要做的只是将页面的数据、方法、生命周期函数注册到框架中,其他的一切复杂的操作都交由框架处理。有关页面生命周期和路由的介绍,可以参见3.2节内容。 框架提供了一套基础的组件,这些组件自带微信风格的样式以及特殊的逻辑,开发者可以通过组合基础组件,创建出强大的微信小程序。大量的预置组件如文本组件、轮播组件等使开发者能够专注业务逻辑的编写,快速开发出需要的小程序。 框架还提供了丰富的微信原生API,使用这些API可以方便地调用微信提供的能力,如获取用户信息、本地存储、支付等。 3.4逻辑层 小程序开发框架的逻辑层使用JavaScript引擎为小程序提供开发者JavaScript代码的运行环境以及微信小程序的特有功能。 逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。开发者写的所有代码最终将会打包成一份JavaScript文件,并在小程序启动的时候运行,直到小程序销毁。这一行为类似Service Worker,所以逻辑层也称为App Service。 在JavaScript的基础上,微信小程序还增加了一些新的功能,以方便小程序的开发。 (1) 增加App()和Page()方法,进行程序和页面的注册。 (2) 增加getApp()和getCurrentPages()方法,分别用来获取App实例和当前页面栈。 (3) 提供丰富的API,如微信用户数据、扫一扫、支付等微信特有能力。 (4) 每个页面有独立的作用域,并提供模块化能力。 需要注意的是,小程序框架的逻辑层并非运行在浏览器中,因此JavaScript在Web中的一些能力都无法使用,如window、document等。 3.4.1注册程序 【任务要求】 新建一个临时的项目,要求在建立的时候,不勾选任何启动模板,直接建立一个完全空白的项目,然后手动新建必要的app.js以及app.json文件。除了需要在app.js文件中注册小程序所有的生命周期函数以外,还需要注册错误监听函数以及页面不存在监听函数,同时,还需要在app.js中设置一个名为globalData,值为“This is global data”字符串的变量。在app.json文件中,注册一个名为demo的页面,并让该页面显示globalData的值。 【任务分析】 注册程序,意即让开发者工具知道当前的目录和文件是一个小程序并将其作为小程序进行处理的步骤。之前的任务,我们一直都是使用基于“普通快速启动模板”(详见1.3节)建立的小程序项目。本次任务,需要自己手动从一个空白的目录建立小程序,并熟悉声明注册一个小程序的过程,体会哪些文件是小程序必需的。 【任务操作】 (1) 新建一个名为Blank_Project的项目,具体操作如图37所示。注意不要勾选图中的“建立普通快速启动模板”复选框。 图37新建空白小程序项目 (2) 新建app.js文件和app.json文件。app.js的文件内容如下。 //app.js App({ onLaunch: function (options) { console.log("小程序启动",options) }, onShow: function (options) { console.log("小程序显示",options) }, onHide: function (options) { console.log("小程序隐藏",options) }, onError: function (msg) { console.log("小程序发生错误",msg) }, onPageNotFound:function(msg){ console.log("小程序要打开的页面不存在",msg) }, globalData: 'This is global data' }) app.json文件内容如下。 { "pages": [ "Demo/demo" ] } 保存文件,编译项目,完成后的项目目录结构如图38所示。 图38Blank_Project项目目录 (3) 打开demo.js文件,在第一行使用getApp()函数获取小程序实例,在data中,将小程序实例中的globalData数据赋给页面的message变量。随后打开demo.wxml文件,在标签中使用{{message}}来显示demo.js中message变量的值。完成后的demo.js文件大致如下。 // Demo/demo.js const appInstance = getApp() Page({ /** * 页面的初始数据*/ data: { message:appInstance.globalData }, … }) demo.wxml文件内容如下。 {{message}} (4) 编译运行,可以看到在模拟器中显示出了demo页面的内容(如图39所示),调试区输出了小程序生命周期的相关内容(如图310所示)。 图39模拟器页面输出 图310调试区输出 【相关知识】 在本次任务中,最为重要的便是app.js文件中的App(Object)函数。App()函数用来注册一个小程序。接受一个Object参数,用于指定小程序的生命周期回调等。App()必须在app.js中调用,必须调用且只能调用一次,不然会出现无法预期的后果。 Object参数说明见表311。 表311App函数参数说明 属性 类型 描述 触 发 时 机 onLaunch() Function 生命周期回调—监听小程序初始化 小程序初始化完成时(全局只触发一次) onShow() Function 生命周期回调—监听小程序显示 小程序启动或从后台进入前台显示时 onHide() Function 生命周期回调—监听小程序隐藏 小程序从前台进入后台时 onError() Function 错误监听函数 小程序发生脚本错误,或者API调用失败时触发,会带上错误信息 onPageNotFound() Function 页面不存在监听函数 小程序要打开的页面不存在时触发,会带上页面信息回调该函数 其他 Any 开发者可以添加任意的函数或数据到Object参数中,用this可以访问 其中,有关小程序生命周期函数的相关内容可以参考3.1节。 onError(String error)函数会在小程序发生脚本错误或API调用报错时触发。string error表示错误信息,包含堆栈信息。 onPageNotFound(Object)函数会在小程序要打开的页面不存在时触发。其参数说明见表312。 表312onPageNotFound函数参数说明 属性 类型 说明 path String 不存在页面的路径 query Object 打开不存在页面的 query 参数 isEntryPage Boolean 是否本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面) 一个简单的使用示例如下。 App({ onPageNotFound(res) { wx.redirectTo({ url: 'pages/...' }) // 如果是 tabBar 页面,请使用 wx.switchTab } }) 当小程序页面不存在时,需要注意以下几点。 (1) 开发者可以在回调中进行页面重定向,但必须在回调中同步处理,异步处理(例如setTimeout异步执行)无效。 (2) 若开发者没有处理页面不存在的情况,当跳转页面不存在时,将打开微信客户端原生的页面不存在提示页面。 (3) 如果回调中又重定向到另一个不存在的页面,将打开微信客户端原生的页面不存在提示页面,并且不再第二次回调。 getApp(Object)是一个全局函数,可以用来获取到小程序App实例。一个简单的使用getApp函数的示例如下。 // other.js const appInstance = getApp() console.log(appInstance.globalData)// I am global data 使用getApp()函数需要注意以下两点。 (1) 不要在定义于App()内的函数中调用getApp(),使用this就可以得到App实例。 (2) 通过getApp()获取实例之后,不要私自调用生命周期函数。 3.4.2注册页面 【任务要求】 打开之前的示例项目(非上个任务的空白项目),在pages/Chapter_3文件夹下新建一个名为page的文件夹,并在里面新建一个名为page的页面,要求如下。 (1) 当页面显示时,在调试器的Console面板输出当前页面的路径; (2) 添加页面的初始数据,分别包括文本、数字、JSON对象和数组四种数据类型; (3) 在页面上显示所有的初始数据,同时添加4个按钮用于修改其内容; (4) 添加一个按钮用于新增一个数据并在页面上显示出来; (5) 设置自定义分享的标题,将当前页面分享给他人。 完成后的页面示例如图311所示。 图311初始页面(左)和单击按钮后页面(右) 【任务分析】 对页面的相关处理,可以说是在开发小程序的时候打交道最多的操作了。同注册程序需要一个App()函数一样,注册页面也需要一个Page()函数。通过设置Page()函数中的参数,可以实现对页面生命周期的处理,监听页面的事件和处理组件事件。同时,注册程序里面还有一个非常重要的用于逻辑层和视图层数据同步的机制。这些都是十分重要的。 【任务操作】 (1) 打开示例项目,在app.json文件的pages数组中,新增第一项"pages/Chapter_3/page/page"。保存文件,编译项目,让开发者工具自动生成page页面所需的文件。 (2) 打开page.wxml,将其中的内容替换成如下代码。 {{text}} {{num}} {{array[0].text}} {{object.text}} {{newField.text}} (3) 打开page.js,将其中的内容替换成如下代码。 // pages/Chapter_3/page/page.js Page({ data: { text: 'init data', num: 0, array: [{ text: 'init data' }], object: { text: 'init data' } }, onShow(){ console.log(this.route) }, changeText() { // this.data.text = 'changed data' // 不要直接修改 this.data // 应该使用 setData this.setData({ text: 'changed data' }) }, changeNum() { // 或者,可以修改 this.data 之后马上用 setData 设置修改了的字段 this.data.num = 1 this.setData({ num: this.data.num }) }, changeItemInArray() { // 对于对象或数组字段,可以直接修改一个其下的子字段,这样做通常比修改整个对象或数组更好 this.setData({ 'array[0].text': 'changed data' }) }, changeItemInObject() { this.setData({ 'object.text': 'changed data' }) }, addNewField() { this.setData({ 'newField.text': 'new data' }) }, onShareAppMessage(res){ if (res.from === 'menu') { // 来自右上角转发菜单 console.log(res.target) } return { title: '注册页面示例', path: this.route } } }) (4) 保存所有文件,编译项目并运行。可以看到页面的表现和按钮的作用如图311所示。观察调试器的Console面板,可以看到如图312所示输出的当前页面路径信息。 图312onShow函数中使用this.route输出当前页面路径 (5) 在模拟器区单击右上角的“菜单”按钮,在弹出的选项中选择“转发”(如图313所示),可以看到带自定义标题的用当前页面截图生成的转发卡片(如图314所示),同时可以看到在Console面板中多了一行如图315所示的输出。 图313“转发”选项 图314转发卡片预览 图315输出转发信息 【相关知识】 Page(Object)函数用来注册一个页面。它接受一个Object类型参数,用于指定页面的初始数据、生命周期回调、事件处理函数等。其中,Object参数内容说明见表313。 表313Page函数参数说明 属性 类型 描述 data Object 页面的初始数据 onLoad() Function 生命周期回调—监听页面加载 onShow() Function 生命周期回调—监听页面显示 onReady() Function 生命周期回调—监听页面初次渲染完成 onHide() Function 生命周期回调—监听页面隐藏 onUnload() Function 生命周期回调—监听页面卸载 onPullDownRefresh() Function 监听用户下拉动作 onReachBottom() Function 页面上拉触底事件的处理函数 onShareAppMessage() Function 用户单击右上角“转发”菜单 onPageScroll() Function 页面滚动触发事件的处理函数 onResize() Function 页面尺寸改变时触发,详见响应显示区域变化 onTabItemTap() Function 当前是tab页时,单击tab时触发 其他 Any 开发者可以添加任意的函数或数据到Object参数中,在页面的函数中用this可以访问 其中,data是页面第一次渲染使用的初始数据。页面加载时,data将会以JSON字符串的形式由逻辑层传至渲染层,因此data中的数据必须是可以转成JSON的类型: 字符串,数字,布尔值,对象,数组。在渲染层的wxml文件中,可以使用“{{var}}”的方式绑定数据。一个简单的示例片段如下。 {{text}} {{array[0].msg}} //example.js Page({ data: { text: 'init data', array: [{msg: '1'}, {msg: '2'}] } }) 在上面这个示例片段中,最终页面上会输出字符串init data和1。 对页面的生命周期函数的介绍和页面的路由跳转,请参见表37的相关内容。此处需要单独提到的一点是,onLoad()函数带有一个Object类型的名为query的参数,该参数包含打开当前页面路径中的参数信息。例如,一个页面通过在路径中使用“url/?key=value”的方式携带了参数跳转到当前页面,那么在当前页面的onLoad函数加载时,便可以在其query参数中获取到以“key : value”形式存储的JSON数据对象。 onPullDownRefresh函数用于监听用户的下拉刷新事件。需要注意的是,下拉刷新的功能并不是默认启用的,如果要允许用户使用下拉刷新,可以在app.json的window选项中或页面配置中设置enablePullDownRefresh的值为true。也可以使用wx.startPullDownRefresh触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。当处理完数据刷新后,调用wx.stopPullDownRefresh可以停止当前页面的下拉刷新。 onReachBottom()函数用于监听用户上拉触底事件。开发者可以在app.json的window选项中或页面配置中设置触发距离onReachBottomDistance,设置好后,用户滑到距离页面底部指定距离时,便会执行onReachBottom函数中的内容。用户在触发距离内滑动期间,本事件只会被触发一次。 onPageScroll(Object)用于监听用户滑动页面事件。其Object参数说明见表314。 表314onPageScroll函数参数说明 属性 类型 说明 scrollTop Number 页面在垂直方向已滚动的距离(单位为px) 需要注意的是,只在需要的时候才在page中定义此方法,不要定义空方法,以减少不必要的事件派发对渲染层和逻辑层通信的影响。同时需要避免在onPageScroll中过于频繁地执行setData等引起逻辑层到渲染层通信的操作,尤其是每次传输大量数据时,这样会影响通信耗时。 onShareAppMessage(Object) 用于监听用户单击页面内“转发”按钮( (3) 打开该目录下的WXML.js文件,将里面的内容修改为如下代码。 // pages/Chapter_3/WXML/WXML.js Page({ data: { array:[1] }, addNewMultiplier:function(){ this.setData({ array:[this.data.array[this.data.array.length-1]+1] }) } }) (4) 保存所有文件,编译运行。在模拟器中单击“下一组”按钮,观察运行效果。 【相关知识】 WXML是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。它包含数据绑定、列表渲染、条件渲染、模板、事件和引用这几个部分的功能。 1. 数据绑定 WXML中的动态数据,均来自于对应Page()函数的data对象。如何将data中的数据送到前端页面中去显示,这就涉及数据绑定的问题。 1) 简单绑定 数据绑定使用Mustache语法(双大括号)将变量括起来,可以作用于以下四种情况。 (1) 内容。 直接在页面上显示数据内容,一个简单的示例如下。 {{ message }} Page({ data: { message: 'Hello MINA!' } }) 将变量message用{{ }}括起来,即表示需要使用data中的数据来显示。该示例会在页面上显示“Hello MINA1”字样。 (2) 组件属性。 用后端变量来设置前端部分组件的属性。注意由双大括号括起来的变量需要在属性的双引号内。一个简单的示例如下。 Page({ data: { id: 0 } }) 该示例表示为view标签新增一个值为“item0”的id属性。 (3) 控制属性。 用后端变量来控制前端组件的显示效果。由双大括号括起来的变量需要在属性的双引号内。一个简单的示例如下。 Hello MINA Page({ data: { condition: true } }) 该示例表示判断的逻辑条件成立,“Hello MINA”的字样会被渲染显示到屏幕上。如果condition的值为false,则页面上什么也不会显示。有关wx:if的用法可以参见后文条件渲染部分内容。 (4) 关键字。 主要用于逻辑判断。具体指“true”和“false”这两个关键字,分别是boolean类型的true和false,表示真和假。一个简单的示例如下。 勾选框不被选中 勾选框被选中 图317在WXML中设置关 键字控制复选框状态 该示例表示复选框处于选中和未选中的两种状态。显示的效果如图317所示。 需要特别注意的是,设置复选框未被选中时不能直接写checked="false",因为这样的方式会将“false”当作字符串看待,转成boolean类型后代表真值。 2) 运算 可以在{{ }}内进行简单的运算,支持如下几种方式。 (1) 三元运算。 可以在双大括号内进行三元运算,一个简单的示例如下。 该示例表示会根据条件表达式flag的情况来决定hidden属性的值是true还是false,进而决定是否要在页面上显示“Hidden”字符串。 (2) 算术运算。 在双大括号内,可以进行基本的算术运算,会直接显示运算后的结果。一个简单的示例如下。 {{a + b}} + {{c}} + d Page({ data: { a: 1, b: 2, c: 3 } }) 该示例会在页面上显示“3 + 3 + d”。其中,第一个3来自a+b的运算结果,第二个3来自变量c的值,d因为没有被包含在双大括号内,作为一个字符原样输出。 (3) 逻辑判断。 可以在双大括号内进行逻辑运算,返回boolean类型的true或者false,可以用于某些属性的控制。一个简单的示例如下。 {{length}} 该示例表示,如果变量length的值大于5,则显示length的值,否则不显示。 (4) 字符串运算。 可以在双大括号内做字符串的拼接运算。一个简单的示例如下。 {{"hello" + " " +name}} Page({ data: { name: 'MINA' } }) 该示例会在页面上显示出拼接好的字符串“hello MINA”。 (5) 数据路径运算。 对于数组和JSON对象类型的数据,在双大括号内也可以通过索引的方式取其值。一个简单的示例如下。 {{object.key}} {{array[0]}} Page({ data: { object: { key: 'Hello ' }, array: ['MINA','!'] } }) 该示例最终会在页面上显示“Hello MINA”。因为{{array[0]}}是取array这个数组的第一个元素,因此“!”并不会被输出。 3) 组合 可以在双大括号内直接进行组合,构成新的数组或者对象。 (1) 数组。可以将data中的数据在WXML中组合成为一个新的数组。一个简单的示例如下。 {{item}} Page({ data: { zero: 0 } }) 该示例表示将data中的变量zero加到WXML的数组中去,组成数组[0, 1, 2, 3, 4],最后在页面上输出0,1,2,3,4。有关示例里面用到的wx:for的用法,可以参考后文列表渲染的相关内容。 (2) 对象。 在双大括号里面,可以对对象进行组合、展开等操作。几个简单的示例如下。 Page({ data: { a: 1, b: 2 } }) 最终组合成的对象是{for: 1, bar: 2}。 也可以使用扩展运算符“...”来将一个对象展开: Page({ data: { obj1: { a: 1, b: 2 }, obj2: { c: 3, d: 4 } } }) 最终组合成的对象是{a: 1, b: 2, c: 3, d: 4, e: 5}。 如果对象的key和value相同,也可以间接地表达。例如: Page({ data: { foo: 'my-foo', bar: 'my-bar' } }) 最终组合成的对象是{foo: 'myfoo', bar:'mybar'}。 上述几种情况可以随意组合,但是如有存在变量名相同的情况,后面的会覆盖前面,例如: Page({ data: { obj1: { a: 1, b: 2 }, obj2: { b: 3, c: 4 }, a: 5 } }) 最终组合成的对象是{a: 5, b: 3, c: 6}。 注意: 如果双大括号和引号之间有空格,则表达式最终将会被解析成为字符串。例如: {{item}} 等同于: {{item}} 2. 列表渲染 1) wx:for 在组件上使用wx:for控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。默认情况下,数组当前项的下标变量名为index,数组当前项的变量名为item。一个简单的示例如下。 {{index}}: {{item.message}} Page({ data: { array: [{ message: 'foo', }, { message: 'bar' }] } }) 该示例最终会在页面上输出0: foo和1: bar两行结果。 当然,也可以使用wx:foritem来指定数组当前元素的变量名,使用wx:forindex来指定数组当前下标的变量名。例如,前面的例子也可以这样写: {{idx}}: {{itemName.message}} 也可以将wx:for用在标签上,以渲染一个包含多节点的结构块。例如: {{index}}: {{item}} 标签本身不含有任何的默认样式,也不会在页面上有具体的展现,仅仅是作为设计WXML页面的结构而出现。这个例子最终会渲染出6个view组件,每两个view组件为一组,分别输出数组里每一项的序号和值。 如果wx:for的值为一个字符串,那么该字符串将被解析成为字符串数组。例如: {{item}} 等同于: {{item}} 2) wx:key 如果列表中项目的位置会动态改变或者有新的项目添加到列表中,与此同时还希望列表中已有的项目保持自己的特征和状态(如 中的输入内容,的选中状态),这个时候需要使用wx:key来指定列表中项目的唯一的标识符。有关的介绍可以参见7.4节和7.10节的内容。 wx:key的值以如下两种形式提供。 (1) 字符串。代表在wx:for循环的数组中某一项的某个属性,该属性的值需要是列表中唯一的字符串或数字,且不能动态改变。 (2) 保留关键字*this。代表在wx:for循环中的某一项本身,这种表示需要这一项本身是一个唯一的字符串或者数字。 当数据改变触发渲染层重新渲染的时候,框架会校正带有key的组件,让它们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。一个使用wx:key的示例如下。 {{item.id}} {{item}} // pages/Chapter_3/WXKEY/WXKEY.js Page({ data: { objectArray: [ { id: 5, unique: 'unique_5' }, { id: 4, unique: 'unique_4' }, { id: 3, unique: 'unique_3' }, { id: 2, unique: 'unique_2' }, { id: 1, unique: 'unique_1' }, { id: 0, unique: 'unique_0' }, ], numberArray: [1, 2, 3, 4] }, switch(e) { const length = this.data.objectArray.length for (let i = 0; i < length; ++i) { const x = Math.floor(Math.random() * length) const y = Math.floor(Math.random() * length) const temp = this.data.objectArray[x] this.data.objectArray[x] = this.data.objectArray[y] this.data.objectArray[y] = temp } this.setData({ objectArray: this.data.objectArray }) }, addToFront(e) { const length = this.data.objectArray.length this.data.objectArray = [{ id: length, unique: 'unique_' + length }].concat(this.data.objectArray) this.setData({ objectArray: this.data.objectArray }) }, addNumberToFront(e) { this.data.numberArray = [this.data.numberArray.length + 1].concat(this.data.numberArray) this.setData({ numberArray: this.data.numberArray }) } }) 该示例会在页面上显示若干个开关组件,可以更改部分开关组件的状态(如图318所示),在单击“重新排序”或者是“在列表顶端新增一项”之后,原有的开关状态并不会被改变,而是会一直保持(如图319所示)。 图318设定开关状态 图319插入新的元素或者是重新排序后 一般情况下,如果不提供wx:key,调试器会给出一个警告。如果自己明确知道该列表是静态的,或者不必关注其顺序,可以选择忽略。 3. 条件渲染 1) wx:if wx:if是一个控制属性,用于控制它所作用的标签是否要被渲染。使用的格式为: wx:if="{{condition}}"。condition需要是一个可以转换为Boolean类型的值或者表达式。还可以结合使用wx:elif和wx:else这两个控制属性组合成if else代码块。一个简单的示例如下。 A B C 和前面的wx:if类似,如果需要一次性判断多个组件标签,可以使用一个 标签将多个组件包装起来,并在上边使用 wx:if 控制属性。例如: view1 view2 同样地,仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。 2) wx:if对比hidden 考虑到wx:if之中的模板也可能包含数据绑定,所以当wx:if的条件值切换时,框架会对wx:if包含的代码块进行销毁或者是重新进行局部渲染。同时wx:if也是惰性的。这意味着,如果初始渲染条件为false,那么框架什么也不会做。框架只有在wx:if的条件第一次变成真的时候才开始渲染wx:if控制的代码块内容。 相比之下,hidden就简单得多,组件始终会被渲染,只是简单地控制显示与隐藏。 一般来说,wx:if有更高的切换消耗而hidden有更高的初始渲染消耗。因此,如果需要频繁切换,用hidden更好,如果在运行时条件不大可能改变则使用wx:if较好。 4. 事件 事件是视图层到逻辑层的通信方式,它可以将用户的行为反馈到逻辑层进行处理。事件一般绑定在组件上,当设定监听的事件被触发时,视图层会将携带了id, dataset, touches等信息的事件对象发送到逻辑层中,此时框架就会执行逻辑层中对应的事件处理函数,来响应用户的操作。 1) 事件的使用方式 以bindtap这样的一个监听用户单击事件的使用方式举一个简单的例子。 Click me! 在相应的Page定义中写上相应的事件处理函数,参数是event。 Page({ tapEvent(event) { console.log(event) } }) 在调试器的Console面板中可以看到输出的信息大致如下。 { "type": "tap", "timeStamp": 895, "target": { "id": "tapTest", "dataset": { "hi": "WeChat" } }, "currentTarget": { "id": "tapTest", "dataset": { "hi": "WeChat" } }, "detail": { "x": 53, "y": 14 }, "touches": [ { "identifier": 0, "pageX": 53, "pageY": 14, "clientX": 53, "clientY": 14 } ], "changedTouches": [ { "identifier": 0, "pageX": 53, "pageY": 14, "clientX": 53, "clientY": 14 } ] } 在这个例子中,为view组件绑定了一个值为“tapEvent”,名为“bindtap”的属性。它表示需要监听用户的单击(tap)事件,该事件由逻辑层的tapEvent函数处理。同时还在view组件里设置了事件需要传递的数据,由datahi属性给出,表示需要传递的数据名为“hi”,值为“WeChat”。在逻辑层接收到的事件对象event里,也找到了对应的数据。 2) 事件的分类 事件分为冒泡事件和非冒泡事件。冒泡事件是指: 当一个组件上的事件被触发后,该事件会向父节点传递。非冒泡事件是指: 当一个组件上的事件被触发后,该事件不会向父节点传递。一般来说,大多数组件的事件都是非冒泡事件。除了非冒泡事件,WXML里的冒泡事件见表321。 表321WXML里的冒泡事件 类型 触 发 条 件 最低版本 touchstart 手指触摸动作开始 touchmove 手指触摸后移动 touchcancel 手指触摸动作被打断,如来电提醒、弹窗 touchend 手指触摸动作结束 tap 手指触摸后马上离开 longpress 手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发了这个事件,tap事件将不被触发 1.5.0 longtap 手指触摸后,超过350ms再离开(推荐使用longpress事件代替) transitionend 会在WXSS transition或wx.createAnimation动画结束后触发 animationstart 会在一个WXSS animation动画开始时触发 animationiteration 会在一个WXSS animation一次迭代结束时触发 animationend 会在一个WXSS animation动画完成时触发 touchforcechange 在支持3D Touch的iPhone设备,重按时会触发 1.9.90 3) 事件的绑定和冒泡 事件绑定的写法同组件的属性写法一样,均以(key,value)键值对的形式。 key以bind或catch开头,然后跟上事件的类型,如bindtap、catchtouchstart。自基础库版本1.5.0起,在非原生组件中,bind和catch后可以紧跟一个冒号,其含义不变,如bind:tap、catch:touchstart。使用bind绑定的事件不会阻止冒泡事件向上冒泡,而使用catch绑定的事件可以阻止冒泡事件向上冒泡。 value是一个字符串,指的是对应的Page中定义的同名函数。如果没有在Page中定义该函数,在事件被触发时,调试器会报错。 有关冒泡和阻止冒泡,可以参考下面这个简单的例子。 outer view middle view inner view 在上面的这个例子中,单击inner view会先后调用handleTap3和handleTap2(因为tap事件会冒泡到middle view,而middle view阻止了tap事件冒泡,不再向父节点传递),单击middle view只会触发handleTap2,单击outer view会触发handleTap1。 4) 事件的捕获阶段 自基础库版本1.5.0起,触摸类事件支持捕获阶段,也可以理解为监听事件的发生。捕获阶段位于冒泡阶段之前,且在捕获阶段中,事件到达节点的顺序与冒泡阶段恰好相反。需要在捕获阶段监听事件时,可以采用capturebind、capturecatch关键字。其中,capturecatch将中断捕获阶段和取消冒泡阶段。 一个使用事件捕获阶段的简单示例如下。 outer view inner view 在该示例中,如果单击了inner view,则会先后触发handleTap2、handleTap4、handleTap3、handleTap1这四个事件。 如果将上面例子中的第一个capturebindtouchstart改为capturecatchtouchstart,由于capturecatch会中断后面的事件捕获以及冒泡,因此在单击inner view后将只会触发handleTap2。 5) 事件对象 如无特殊说明,当组件触发事件时,逻辑层绑定该事件的处理函数会收到一个事件对象。基础事件(BaseEvent)对象的属性说明见表322。 表322基础事件对象属性说明 属性 类型 说明 type String 事件类型 timeStamp Integer 事件生成时的时间戳,记录的是页面打开到触发事件所经过的毫秒数 target Object 触发事件的源组件的一些属性值集合 currentTarget Object 事件绑定的当前组件的一些属性值集合 由基础事件(BaseEvent)对象派生出自定义事件对象(CustomEvent)和触摸事件对象(TouchEvent),这两个对象除了具有基础事件对象的所有属性外,还具有各自的一些属性,分别见表323和表324。 表323自定义事件对象属性说明 属性 类型 说明 detail Object 额外的信息,由各个组件自己定义。比如表单提交的数据、媒体的错误信息等 表324触摸事件对象属性说明 属性 类型 说明 touches Array 触摸事件,当前停留在屏幕中的触摸点信息的数组 changedTouches Array 触摸事件,当前变化的触摸点信息的数组 对基础事件对象的target属性所包含的属性值集合的说明见表325。 表325target所包含的属性值 属性 类型 说明 id String 事件源组件的id tagName String 当前组件的类型 dataset Object 事件源组件上由data开头的自定义属性组成的集合 对基础事件对象的currentTarget属性所包含的属性值集合的说明见表326。 表326currentTarget所包含的属性值 属性 类型 说明 id String 当前组件的id tagName String 当前组件的类型 dataset Object 当前组件上由data开头的自定义属性组成的集合 有关target和currentTarget的使用,可以参考下面这个简单的例子。 outer view middle view inner view 在这个例子中,我们单击inner view,会依次触发handleTap3和handleTap2这两个处理函数。其中,handleTap3收到的事件对象的target和currentTarget都是inner,而handleTap2收到的事件对象的target是inner,currentTarget则是middle。 在target和currentTarget属性值的集合中,dataset属性表示组件中自定义数据的集合。在组件中定义的数据,会通过事件传递给逻辑层。正如上面例子中,在view组件中自定义了datahi="WeChat"的属性,那么在dataset这个属性里,就可以找到一个key为hi,value为“WeChat”的值。在组件中自定义数据的方式是: 以“data”开头,多个单词由连字符“”连接,不能有大写,如果有大写,则会自动转成小写。例如,dataelementtype,最终在event.currentTarget.dataset中会将连字符转成驼峰elementType; dataelementType,最终在event.currentTarget.dataset中会转换成elementtype。 在触摸事件对象中,touches属性是一个数组,数组的每一项是一个Touch对象,表示当前停留在屏幕上的触摸点。Touch对象的属性说明见表327。 表327Touch对象属性说明 属性 类型 说明 identifier Number 触摸点的标识符 pageX, pageY Number 距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴 clientX, clientY Number 距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴 触摸事件对象的changedTouches属性数据格式同touches,表示有变化的触摸点。如从无变有(touchstart),位置变化(touchmove),从有变无(touchend,touchcancel)。 需要单独说明的是,在Canvas中,触摸事件对象的touches属性里面携带的则是CanvasTouch对象。CanvasTouch对象的属性说明见表328。 表328CanvasTouch对象属性说明 属性 类型 说明 identifier Number 触摸点的标识符 x, y Number 距离Canvas左上角的距离,Canvas的左上角为原点,横向为X轴,纵向为Y轴 5. 模板 WXML提供模板(template)功能。开发者可以在模板中定义代码片段,然后在不同的地方调用。方便在小程序页面的部分固定结构中使用。 要使用模板,首先需要定义模板。定义模板需要使用