前面章节介绍了小程序的文件构成,那么这些文件在微信客户端是怎么协同工作的呢?在本章中将会介绍微信客户端给小程序所提供的宿主环境,下文把这个概念简称为宿主或者宿主环境。
小程序可以调用宿主环境提供的微信客户端的能力,这就使得小程序比普通网页拥有更多的能力。由于小程序会运行在不同版本的宿主环境下,因此针对各个版本的宿主环境做程序上的兼容也是在所难免的。
小程序的运行环境分为渲染层和逻辑层。前面章节介绍过WXML模板和WXSS样式在渲染层进行工作,JS脚本在逻辑层进行工作。小程序的渲染层和逻辑层分离是经过很多考虑得出来的模型,在后面章节会详细阐述这个模型背后的原理以及产生的问题。在本章会先介绍这个模型的基本工作方式。
2.3.1.1 渲染HelloWorld页面
下面来看看小程序是如何把脚本里边的data数据渲染在界面上的。
创建名称为runHelloWorld的小程序项目。
WXML模板使用view标签,其子节点用{{}}的语法绑定一个名称为msg的变量,示例代码如下:
<view>{{msg}}</view>
在JS脚本中使用this.setData方法把msg字段设置成“Hello World”,示例代码如下:
Page({
onLoad: function () {
this.setData({ msg: 'Hello World' })
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
从上面的例子可以看到3点:
(1)渲染层和数据相关
(2)逻辑层负责产生、处理数据
(3)逻辑层通过Page实例的setData方法传递数据到渲染层
关于第1点,涉及到了“数据驱动”的概念,此知识点会在后面的章节进行详细讨论,现在先看看第3点涉及的“通信模型”。
2.3.1.2 通信模型
小程序的渲染层和逻辑层分别由两个线程管理:
(1)渲染层的界面使用WebView线程进行渲染。一个小程序存在多个界面,所以渲染层存在多个WebView线程
(2)逻辑层使用JsCore线程运行JS脚本
这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型如图2-xx所示。
图2-xx 小程序的通信模型
2.3.1.3 数据驱动
通常情况下,在开发UI界面的过程中,程序代码需要维护很多变量的状态,同时还要操作对应的UI元素。但是随着界面越来越复杂,就得需要维护更多变量的状态,并且要处理很多界面上的交互事件,最终会使整个程序代码变得越来越复杂。通常界面视图和变量状态是相关联的,如果有某种方法可以让视图和状态绑定在一起(状态改变时视图也能自动同步变更),那么就可以省去繁琐的手动修改视图的工作了。
这个方法就是“数据驱动”,下面来介绍一下小程序的数据驱动的基本原理。
WXML结构实际上等价于一棵Dom树,通过一个JS对象也可以表达Dom树的结构,如图2-xx所示。
图2-xx WXML结构和JS对象均可以表示一棵Dom树
WXML可以先转成JS对象,然后再渲染出真正的Dom树,转换的过程如图2-xx所示。
图2-xx WXML结构转成JS对象后再转成Dom树
通过setData()方法把msg数据从“Hello World”更新成“Goodbye”,产生的JS对象对应的节点就会发生变化,此时可以对比前后两个JS对象得到变化的部分,然后把这个差异应用到原来的Dom树上,从而达到更新UI的目的,这就是“数据驱动”的原理,如图2-xx所示。
图2-xx 对比状态
状态更新的时候,通过对比前后JS对象变化,进而改变视图层的Dom树。
2.3.1.4 双线程下的界面渲染
小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转换成对应的JS对象,在逻辑层发生数据变更的时候,需要通过宿主环境提供的setData()方法把数据从逻辑层传递到渲染层,再经过对比前后的差异,把差异应用在原来的Dom树上,最终渲染出正确的UI界面,如图2-xx所示。
图2-xx 逻辑层传递数据到渲染层
从逻辑组成来说,一个小程序是由多个“页面”组成的“程序”。这里要区别一下“小程序”和“程序”的概念,通常情况下需要在“程序”启动或者退出的时候存储数据或者在“页面”显示或者隐藏的时候做一些逻辑处理,了解程序和页面的概念以及它们的生命周期是非常重要的。
2.3.2.1 程序
“小程序”指的是产品层面的程序,而“程序”指的是代码层面的程序实例,为了避免误解,下文采用App来代替代码层面的“程序”的概念。
2.3.2.1.1 程序构造器App()
宿主环境提供了App()构造器用来注册一个程序App,需要留意的是,App()构造器必须写在项目根目录的app.js文件中,App实例是单例对象,在其他JS脚本中可以使用宿主环境提供的getApp()方法来获取App实例。
创建名称为getAppTest的小程序项目。
JS示例代码如下:
Page({
onLoad() {
var getAppInstance = getApp();
getAppInstance.username = "我的姓名";
console.log(getAppInstance.username);
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
使用App()构造器的示例代码如下:
App({
onLaunch: function (options) { },
onShow: function (options) { },
onHide: function () { },
onError: function (msg) { },
globalData: 'I am global data'
})
App构造器接受一个Object参数,其参数的属性说明如表2-xx所示。
参数属性 | 类型 | 描述 |
---|---|---|
onLaunch | Function | 当小程序初始化完成时会触发onLaunch(全局只触发一次) |
onShow | Function | 当小程序启动或从后台进入前台显示时会触发onShow |
onHide | Function | 当小程序从前台进入后台时会触发onHide |
onError | Function | 当小程序发生脚本错误或者API调用失败时会触发onError并带上错误信息 |
其他字段 | 任意 | 可以添加任意的函数或数据到Object参数中,在App实例回调用this可以访问 |
表2-xx App构造器参数的属性
测试onLaunch回调函数。
创建名称为onLaunchTest的小程序项目。
JS示例代码如下:
App({
onLaunch() {
console.log("onLaunch run !");
}
})
启动小程序后的程序运行效果如图2-xx所示。
图2-xx 运行效果
测试onShow回调函数。
创建名称为onShowTest的小程序项目。
JS示例代码如下:
App({
onLaunch() {
console.log("onLaunch run !");
},
onShow() {
console.log("onShow run !time = " + new Date().toString());
}
})
启动小程序后的程序运行效果如图2-xx所示。
图2-xx 运行效果
点击Home菜单,如图2-xx所示。
图2-xx 点击Home菜单
显示界面如图2-xx所示。
图2-xx 显示界面
点击界面上的任何一个条目返回小程序的index界面,控制台打印效果如图2-xx所示。
图2-xx 再次运行onShow回调函数
测试onHide回调函数。
创建名称为onHideTest的小程序项目。
JS示例代码如下:
App({
onLaunch() {
console.log("onLaunch run !");
},
onShow() {
console.log("onShow run !time = " + new Date().toString());
},
onHide() {
console.log("onHide run !time = " + new Date().toString());
}
})
启动小程序后的程序运行效果如图2-xx所示。
图2-xx 运行效果
点击Home菜单,如图2-xx所示。
图2-xx 点击Home菜单
显示界面如图2-xx所示。
图2-xx 显示界面
控制台打印效果如图2-xx所示。
图2-xx 运行效果
点击界面上的任何一个条目返回小程序的index界面,控制台打印效果如图2-xx所示。
图2-xx 再次运行onShow回调函数
测试onError回调函数。
创建名称为onErrorTest的小程序项目。
JS示例代码如下:
App({
onLaunch() {
var myString = null;
console.log(myString.length);
},
onError(msg) {
console.log("msg begin");
console.log(msg);
console.log("msg end");
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
测试App级别的全局属性。
创建名称为appGlobalAttribute的小程序项目。
文件app.js示例代码如下:
App({
globalData: {
usernameAndPassword: "123_456"
}
})
文件index.js示例代码如下:
Page({
onLoad() {
console.log(getApp().globalData.usernameAndPassword);
}
})
启动小程序后的程序运行效果如图2-xx所示。
图2-xx 运行效果
其中onLaunch/onShow/onHide这三个回调函数是App实例的生命周期函数。App()构造器的其他参数的具体使用可以参考后面的章节。
2.3.2.1.2 程序的生命周期和打开场景
初次进入小程序的时候,微信客户端会初始化宿主环境,同时从网络下载或者从本地缓存中拿到小程序的代码包,把它注入到宿主环境中。初始化完毕后,微信客户端就会给App实例派发onLaunch事件,App构造器参数所定义的onLaunch方法会被调用。
进入小程序之后,用户可以点击右上角的关闭按钮,或者按手机设备的Home键离开小程序,此时的小程序并没有被直接销毁,把这种情况称为“小程序进入后台状态”,App构造器参数所定义的onHide方法会被调用。
当再次回到微信或者再次打开小程序时,微信客户端会把“后台”的小程序唤醒,把这种情况称为“小程序进入前台状态”,App构造器参数所定义的onShow方法会被调用。
可以看到,App的生命周期是由微信客户端根据用户操作主动触发的。为了避免程序上的混乱,不应该在其他代码里主动调用App实例的生命周期函数。
在微信客户端中打开小程序有很多途径:从群聊会话里打开,从小程序列表中打开,通过微信扫一扫二维码打开,从另外一个小程序打开其它的小程序等等,针对不同途径的打开方式,小程序有时需要做不同的业务处理,所以微信客户端会把打开方式传递给onLaunch和onShow回调函数的options参数。
需要留意小程序的宿主环境在迭代更新的过程中会增加不少的打开场景,因此要获取最新的场景值说明请查看官方文档:
https://mp.weixin.qq.com/debug/wxadoc/dev/framework/app-service/app.html
创建名称为getOptions的小程序项目。
JS示例代码如下:
App({
onLaunch: function (options) { console.log(options) },
onShow: function (options) { console.log(options) }
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
onLaunch和onShow函数的options参数的解释如表2-xx所示。
字段 | 类型 | 描述 |
---|---|---|
path | String | 打开小程序的页面路径 |
query | Object | 打开小程序的页面参数query |
scene | Number | 打开小程序的场景值 |
shareTicket | String | shareTicket |
referrerInfo | Object | 当场景是从另一个小程序或公众号或App打开时,返回此字段 |
referrerInfo.appId | String | 来源小程序或公众号或App的appId |
referrerInfo.extraData | Object | 来源小程序传过来的数据,scene=1037或1038时支持 |
表2-xx 回调函数onLaunch和onShow的参数解释
如表2-xx所示的场景支持返回referrerInfo.appId。
场景值 | 场景 | appId信息含义 |
---|---|---|
1020 | 公众号profile | 页相关小程序列表,返回来源公众号appId |
1035 | 公众号自定义菜单 | 返回来源公众号appId |
1036 | App分享消息卡片 | 返回来源应用appId |
1037 | 小程序打开小程序 | 返回来源小程序appId |
1038 | 从另一个小程序返回 | 返回来源小程序appId |
1043 | 公众号模板消息 | 返回来源公众号appId |
表2-xx 哪些场景支持返回referrerInfo.appId
2.3.2.1.3 小程序全局数据
小程序的JS脚本是运行在JsCore线程中,小程序的每个页面各自有一个WebView线程进行渲染,所以在小程序切换页面时,小程序逻辑层的JS脚本运行上下文依旧在同一个JsCore线程中。
App实例是单例的,因此不同页面可以直接通过App实例下的属性来共享数据。App构造器可以传递其他参数作为全局属性以达到全局共享数据的目的。示例代码如下:
// app.js
App({
globalData: 'I am global data' // 全局共享数据
})
// 其他页面脚本other.js
var appInstance = getApp()
console.log(appInstance.globalData) // 输出: I am global data
与此同时还要特别留意一点,所有页面的脚本逻辑都跑在同一个JsCore线程里,页面使用setTimeout或者setInterval定时器,然后跳转到其他页面时,这些定时器并没有被清除,需要开发者自己在页面离开的时候进行清理。
2.3.2.2 页面
一个小程序可以有很多页面,每个页面承载着不同的功能,页面之间可以互相跳转。为了叙述简洁,之后讨论所涉及的“页面”概念特指“小程序页面”。
2.3.2.2.1 文件构成和路径
小程序的一个页面由三个部分所组成:
一个页面的文件需要放置在同一个目录下,其中WXML文件和JS文件是必须存在的,JSON和WXSS文件是可选的。
页面路径需要在小程序代码根目录中的app.json文件里的pages字段进行声明,否则这个页面不会被注册到宿主环境中。例如两个页面的文件的相对路径分别为pages/index/page和pages/other/other(表示wxml/wxss/json/js四个文件),示例代码如下:
{
"pages": [
"pages/index/page", // 第一项默认为首页
"pages/other/other"
]
}
默认pages字段的第一个页面路径为小程序的首页。
为了叙述方便,下文使用page.wxml/page.wxss/page.json/page.js来分别代表特定页面的4个文件。
2.3.2.2.2 页面构造器Page()
宿主环境提供了Page()构造器用来注册一个小程序页面,Page()在页面脚本page.js中被调用,Page()的调用方式如下:
Page({
data: { text: "This is page data." },
onLoad: function (options) { },
onReady: function () { },
onShow: function () { },
onHide: function () { },
onUnload: function () { },
onPullDownRefresh: function () { },
onReachBottom: function () { },
onShareAppMessage: function () { },
onPageScroll: function () { }
})
Page构造器接受一个Object参数,其参数的属性说明如表2-xx所示。
参数属性 | 类型 | 描述 |
---|---|---|
data | Object | 页面的初始数据 |
onLoad | Function | 生命周期函数--监听页面加载,触发时机早于onShow和onReady |
onShow | Function | 生命周期函数--监听页面显示,触发事件早于onReady |
onReady | Function | 生命周期函数--监听页面初次渲染完成 |
onHide | Function | 生命周期函数--监听页面隐藏 |
onUnload | Function | 生命周期函数--监听页面卸载 |
onPullDownRefresh | Function | 页面相关事件处理函数--监听用户下拉动作 |
onReachBottom | Function | 页面上拉触底事件的处理函数 |
onShareAppMessage | Function | 用户点击右上角转发 |
onPageScroll | Function | 页面滚动触发事件的处理函数 |
其他 | Any | 可以添加任意的函数或数据,在Page实例的其他函数中用 this 可以访问 |
表2-xx Page构造器参数的属性
其中data属性是当前页面WXML模板中可以用来做数据绑定的初始数据,后面的章节会展开讨论。
onLoad/onShow/onReady/onHide/onUnload这5个回调函数是Page实例的生命周期函数,会在后面的章节展开讨论。
onPullDownRefresh/onReachBottom/onShareAppMessage/onPageScroll这4个回调函数是页面的用户行为,会在后面的章节展开讨论。
2.3.2.2.3 使用wx.navigateTo和wx.redirectTo和wx.navigateBack跳转页面
在微信小程序开发里使用wx.navigateTo和wx.redirectTo和wx.navigateBack方法用来实现页面的跳转,不过它们的应用场景和效果却存在差异。
(1)方法wx.navigateTo:该API的作用是保留当前页面,然后跳转到应用内的某个页面。此操作会创建新的页面栈,支持用户返回上一页。
特点:
示例代码:
wx.navigateTo({
url: '/pages/detail/detail?id=123'
})
(2)方法wx.redirectTo:该API的作用是关闭当前页面,然后跳转到应用内的某个页面。此操作不会保留当前页面在历史记录中。
特点:
示例代码:
wx.redirectTo({
url: '/pages/index/index'
})
(3)方法wx.navigateBack:该API的作用是关闭当前页面,然后返回到上一页面或者多级页面。
特点:
示例代码:
// 返回上一级页面
wx.navigateBack()
// 返回三级页面
wx.navigateBack({
delta: 3
})
总结如表2-xx所示。
方法 | 是否保留当前页面 | 页面栈变化 | 应用场景 |
---|---|---|---|
wx.navigateTo | 是 | 新增页面 | 列表页→详情页 |
wx.redirectTo | 否 | 替换当前页面 | 登录页→主页、表单提交成功后 |
wx.navigateBack | 否 | 减少页面(返回指定层数) | 详情页→列表页、多级返回 |
表2-xx 方法总结
在实际开发过程中,需要依据业务需求来合理选用页面跳转方式,防止出现页面栈溢出或者无法返回等问题。
2.3.2.2.4 页面的生命周期和打开参数
创建名称为的小程序项目。
文件app.js代码如下:
App({
onLaunch() {
console.log("APP onLaunch");
},
onShow() {
console.log("APP onShow");
},
onHide() {
console.log("APP onHide");
}
})
页面index的WXML文件代码如下:
<button bind:tap="goToSecondPage">go to second page</button>
<button bind:tap="showDetailPage">go to detail page</button>
页面index的JS文件代码如下:
Page({
onLoad() {
console.log("Page index onLoad");
},
onShow() {
console.log("Page index onShow");
},
onReady() {
console.log("Page index onReady");
},
onHide() {
console.log("Page index onHide");
},
onUnload() {
console.log("Page index onUnload");
},
goToSecondPage() {
console.log("goToSecondPage run");
wx.navigateTo({
url: '/pages/second/second'
});
},
showDetailPage() {
console.log("showDetail run");
wx.navigateTo({
url: '/pages/detail/detail?id=123&username=abc&password=xyz'
});
}
})
页面second的WXML文件代码如下:
<button bind:tap="test_redirectTo">redirectTo index page</button>
<button bind:tap="test_navigateBack">navigateBack index page</button>
页面second的JS文件代码如下:
Page({
onLoad() {
console.log("Page second onLoad");
},
onShow() {
console.log("Page second onShow");
},
onReady() {
console.log("Page second onReady");
},
onHide() {
console.log("Page second onHide");
},
onUnload() {
console.log("Page second onUnload");
},
test_redirectTo() {
wx.redirectTo({
url: '/pages/index/index'
});
},
test_navigateBack() {
wx.navigateBack({
delta: 1
});
}
})
页面detail的WXML文件代码如下:
<text>pages/detail/detail.wxml</text>
页面detail的JS文件代码如下:
Page({
onLoad(options) {
console.log(options.id);
console.log(options.username);
console.log(options.password);
}
})
页面初次加载的时候,微信客户端会给Page实例派发onLoad事件,Page构造器参数所定义的onLoad方法会被调用。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onLoad方法执行了。
onLoad在页面没被销毁之前只会触发1次。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onLoad方法执行了。
点击如图2-xx中的按钮。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击“后退”按钮,如图2-xx所示。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
在整个操作的过程中,由于页面index没有被销毁,所以页面index的onLoad方法只被调用1次。
页面显示之后,Page构造器参数所定义的onShow方法会被调用。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onShow方法执行了。
一般从别的页面返回到当前页面时,当前页面的onShow方法会被调用。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onShow方法执行了。
点击按钮,如图2-xx所示。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击按钮,如图2-xx所示。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onShow方法再次被执行。
在页面初次渲染完成时,Page构造器参数所定义的onReady方法会被调用。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onReady方法执行了。
onReady在页面没有被销毁之前只会触发1次。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onReady方法执行了。
点击如图2-xx中的按钮。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击“后退”按钮,如图2-xx所示。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
在整个操作的过程中,由于页面index没有被销毁,所以页面index的onReady方法只被调用1次。
onReady触发时,表示页面已经准备妥当,在逻辑层就可以和视图层进行交互了。
以上三个事件触发的时机是onLoad -> onShow -> onReady。
页面不可见时,Page构造器参数所定义的onHide方法会被调用,这种情况会在使用wx.navigateTo切换到其他页面、底部tab切换时触发。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击如图2-xx中的按钮。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面index的onHide方法执行了。
当前页面使用wx.redirectTo或wx.navigateBack返回到其他页面时,当前页面会被微信客户端销毁回收,此时Page构造器参数所定义的onUnload方法会被调用。测试一下。
先来测试wx.redirectTo方法。重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击如图2-xx中的按钮。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击按钮,如图2-xx所示。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面second的onUnload方法执行了。
再来测试wx.navigateBack方法。重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击如图2-xx中的按钮。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击按钮,如图2-xx所示。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
页面second的onUnload方法执行了。
Page的生命周期是由微信客户端根据用户操作主动触发的,那么为了避免程序上的混乱,所以不应该在其他代码中主动调用Page实例的生命周期函数。
在onLoad回调方法中可以获取当前页面的option参数,如图2-xx所示。
图2-xx 查看option内容
通过options参数可以获得参数query。query的作用是什么?先来设想这样一个场景,实现一个购物商城的小程序,需要完成一个商品列表页面和商品详情页面,点击商品列表页面中的任何一个商品就可以跳转到该商品的详情页面。当然不可能为每个商品单独去实现它的详情页面,所以只需要实现一个商品详情的pages/detail/detail(代表WXML/WXSS/JS/JSON文件)页面即可。在列表页面打开商品详情页面时把商品的id传递过去,在详情页面通过刚刚介绍的onLoad回调函数的option参数就可以获取到商品id,从而显示出对应的商品详情信息,示例代码如下:
// pages/list/list.js
// 列表页使用navigateTo跳转到详情页
wx.navigateTo({ url: 'pages/detail/detail?id=1&other=abc' })
// pages/detail/detail.js
Page({
onLoad: function (option) {
console.log(option.id)
console.log(option.other)
}
})
小程序把页面的打开路径定义成页面的URL,小程序的URL组成格式和网页的URL组成格式类似,在页面路径后面使用英文?分隔path和query部分。query部分的多个参数使用&进行分隔,参数的名字和值使用key=value键值对的形式进行声明。通过页面Page构造器里的onLoad的option参数可以拿到当前页面的打开参数,参数option的数据类型是一个Object,其键值对与页面URL上的query键值对一一对应。和网页URL一样,页面URL上的value如果涉及特殊字符(例如&字符、?字符、中文字符等,详情参考URI的RFC3986说明),需要采用UrlEncode后再拼接到页面URL上。测试一下,重启小程序项目,控制台打印结果如图2-xx所示。
图2-xx 运行效果
点击如图2-xx中的按钮。
图2-xx 点击按钮
控制台打印结果如图2-xx所示。
图2-xx 运行效果
成功通过option获取URL参数id,username,password的参数值。
2.3.2.2.5 页面的数据
小程序的页面结构由WXML进行描述,WXML可以通过数据绑定的语法绑定从逻辑层传递过来的数据字段,这里所说的数据其实就是来自于页面Page构造器的data字段,data参数是页面第一次渲染时从逻辑层传递到渲染层的数据。使用Page构造器的data参数的示例代码如下:
// page.js
Page({
data: {
text: 'init data',
array: [{ msg: '1' }, { msg: '2' }]
}
})
创建名称为dataTest1的小程序项目。
WXML示例代码如下:
<view>{{text}}</view>
<view>{{array[0].msg}}</view>
<view>{{array[1].msg}}</view>
JS示例代码如下:
Page({
data: {
text: 'init data',
array: [{ msg: '1' }, { msg: '2' }]
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
宿主环境提供的Page实例的原型中有setData函数,可以在Page实例下的方法调用this.setData方法把数据传递给渲染层,从而达到更新界面的目的。由于小程序的渲染层和逻辑层分别在两个线程中运行,所以setData传递数据实际是一个异步的过程,所以setData的第二个参数是一个callback回调,回调是在setData对界面渲染完毕之后被触发。
setData函数的一般调用格式是setData(data,callback),其中data是由多个key:value构成的Object对象。使用setData更新渲染层数据的示例代码如下:
// page.js
Page({
onLoad: function () {
this.setData({
text: 'change data'
}, function () {
// 在这次setData对界面渲染完毕后触发
})
}
})
创建名称为dataTest2的小程序项目。
WXML示例代码如下:
<view>{{text}}</view>
<view>{{array[0].msg}}</view>
<view>{{array[1].msg}}</view>
JS示例代码如下:
Page({
data: {
text: 'init data',
array: [{ msg: '1' }, { msg: '2' }]
},
onLoad: function () {
this.setData({
text: 'change data'
}, function () {
// 在这次setData对界面渲染完毕后触发
console.log("callback method run !");
})
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
实际在开发的时候,页面的data数据会涉及相当多的字段,开发者并不需要每次都将整个data字段重新设置一遍,只需要把改变的值进行设置即可,宿主环境会自动把新改动的字段合并到渲染层对应的字段中。使用setData更新渲染层数据的示例代码如下:
// page.js
Page({
data: {
a: 1, b: 2, c: 3,
d: [1, { text: 'Hello' }, 3, 4]
}
onLoad: function () {
// a需要变化时,只需要setData设置a字段即可
this.setData({ a: 2 })
}
})
创建名称为dataTest3的小程序项目。
WXML示例代码如下:
<view>{{a}}</view>
<view>{{b}}</view>
<view>{{c}}</view>
<view>{{d[0]}}</view>
<view>{{d[1].text}}</view>
<view>{{d[2]}}</view>
<view>{{d[3]}}</view>
JS示例代码如下:
Page({
data: {
a: 1, b: 2, c: 3,
d: [1, { text: 'Hello' }, 3, 4]
},
onLoad: function () {
// a需要变化时,只需要setData设置a字段即可
this.setData({ a: 2 })
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
data中的key还可以非常灵活,以数据路径的形式给出,例如:
this.setData({ "d[0]": 100 });
this.setData({ "d[1].text": 'Goodbye' });
只要保持一个原则就可以提高小程序的渲染性能:每次只设置需要改变的最小单位数据。
创建名称为dataTest4的小程序项目。
WXML示例代码如下:
<view>{{a}}</view>
<view>{{b}}</view>
<view>{{c}}</view>
<view>{{d[0]}}</view>
<view>{{d[1].text}}</view>
<view>{{d[2]}}</view>
<view>{{d[3]}}</view>
JS示例代码如下:
Page({
data: {
a: 1, b: 2, c: 3,
d: [1, { text: 'Hello' }, 3, 4]
},
onLoad: function () {
this.setData({ "d[0]": 100 });
this.setData({ "d[1].text": 'Goodbye' });
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
此外需要注意以下3点:
(1)直接修改Page实例的this.data而不调用this.setData函数是无法改变页面的状态的,还会造成数据的不一致。
(2)由于setData函数是需要两个线程的一些通信消耗,为了提高性能,每次设置的数据不应超过1024kB。
(3)不要把data中的任意一项的value设为undefined,否则可能会引起一些不可预料的bug。
2.3.2.2.6 页面的用户行为
小程序宿主环境提供了四个和页面相关的用户行为回调,在本章节中将一一进行测试。
2.3.2.2.6.1 下拉刷新onPullDownRefresh
监听用户下拉刷新事件,需要在app.json的window选项中或页面配置page.json中设置enablePullDownRefresh为true。当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
创建名称为onPullDownRefresh的小程序项目。
WXML示例代码如下:
<block wx:for="{{userinfoArray}}" wx:key="id">
<view>
{{item.id}}__{{item.username}}__{{item.password}}
</view>
</block>
JS示例代码如下:
Page({
data: {
userinfoArray: []
},
i: 1,
onReady() {
console.log("onReady");
this.getNewData();
},
onPullDownRefresh() {
console.log("onPullDownRefresh");
this.getNewData();
},
getNewData() {
console.log("getNewData");
setTimeout(() => {
var newUserinfoArrray = [];
var end = this.i + 20;
for (; this.i < end; this.i++) {
var newUserinfo = {};
newUserinfo.id = this.i;
newUserinfo.username = "username" + (this.i);
newUserinfo.password = "password" + (this.i);
newUserinfoArrray.push(newUserinfo);
}
newUserinfoArrray[newUserinfoArrray.length - 1].password = "无stop版本";
this.setData({ userinfoArray: newUserinfoArrray });
console.log(newUserinfoArrray.length);
// wx.stopPullDownRefresh({
// success: () => {
// console.log('下拉刷新结束');
// },
// fail: (err) => {
// console.error('停止下拉刷新失败:', err);
// }
// });
}, 5000);
}
})
先编译后预览,如图2-xx所示。
图2-xx 先编译后预览
在真机中预览的效果就是数据已经更新了,但加载状态一直在持续,这是错误的运行效果。
更改JS代码如下:
Page({
data: {
userinfoArray: []
},
i: 1,
onReady() {
console.log("onReady");
this.getNewData();
},
onPullDownRefresh() {
console.log("onPullDownRefresh");
this.getNewData();
},
getNewData() {
console.log("getNewData");
setTimeout(() => {
var newUserinfoArrray = [];
var end = this.i + 20;
for (; this.i < end; this.i++) {
var newUserinfo = {};
newUserinfo.id = this.i;
newUserinfo.username = "username" + (this.i);
newUserinfo.password = "password" + (this.i);
newUserinfoArrray.push(newUserinfo);
}
newUserinfoArrray[newUserinfoArrray.length - 1].password = "有stop版本";
this.setData({ userinfoArray: newUserinfoArrray });
console.log(newUserinfoArrray.length);
wx.stopPullDownRefresh({
success: () => {
console.log('下拉刷新结束');
},
fail: (err) => {
console.error('停止下拉刷新失败:', err);
}
});
}, 5000);
}
})
先编译后预览,如图2-xx所示。
图2-xx 先编译后预览
在真机中预览的效果就是数据更新结束后加载状态也取消了,这是正确的运行效果。
2.3.2.2.6.2 上拉触底onReachBottom
监听用户上拉触底事件。可以在app.json的window选项中或页面配置page.json中设置触发距离onReachBottomDistance。在触发距离内滑动期间,本事件只会被触发一次。
创建名称为onReachBottom的小程序项目。
WXML示例代码如下:
<block wx:for="{{userinfoArray}}" wx:key="id">
<view>
{{item.id}}__{{item.username}}__{{item.password}}
</view>
</block>
JS示例代码如下:
Page({
data: {
userinfoArray: []
},
i: 1,
isLoading: false,
onReady() {
console.log("onReady");
this.getNewData();
},
onReachBottom() {
console.log("onReachBottom");
this.getNewData();
},
getNewData() {
console.log("getNewData");
if (this.isLoading == false) {
console.log("未加载中,继续加载");
this.isLoading = true;
setTimeout(() => {
var newUserinfoArray = [];
var end = this.i + 50;
for (; this.i < end; this.i++) {
var newUserinfo = {};
newUserinfo.id = this.i;
newUserinfo.username = "username" + (this.i);
newUserinfo.password = "password" + (this.i);
newUserinfoArray.push(newUserinfo);
}
this.setData({ userinfoArray: [...this.data.userinfoArray, ...newUserinfoArray] });
console.log(this.data.userinfoArray.length);
this.isLoading = false;
}, 5000);
} else {
console.log("已加载中,取消加载");
}
}
})
程序运行效果是分段加载最新的数据。
2.3.2.2.6.3 页面滚动onPageScroll
监听用户滑动页面事件,参数为Object,包含scrollTop字段,表示页面在垂直方向已滚动的距离(单位px)。
创建名称为onPageScroll的小程序项目。
WXML示例代码如下:
<block wx:for="{{userinfoArray}}" wx:key="id">
<view>
{{item.id}}__{{item.username}}__{{item.password}}
</view>
</block>
JS示例代码如下:
Page({
data: {
userinfoArray: []
},
onReady() {
console.log("onReady");
this.getNewData();
},
onPageScroll(param) {
console.log("scrollTop=" + param.scrollTop);
},
getNewData() {
console.log("getNewData");
var newUserinfoArrray = [];
for (var i = 0; i < 200; i++) {
var newUserinfo = {};
newUserinfo.id = i + 1;
newUserinfo.username = "username" + (i + 1);
newUserinfo.password = "password" + (i + 1);
newUserinfoArrray.push(newUserinfo);
}
this.setData({ userinfoArray: newUserinfoArrray });
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
2.3.2.2.6.4 用户转发onShareAppMessage
只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮,在用户点击转发按钮的时候会被调用。此事件需要return一个Object,包含title和path两个字段,用于自定义转发内容。使用onShareAppMessage自定义转发字段的示例代码如下:
// page.js
Page({
onShareAppMessage: function () {
return {
title: '自定义转发标题',
path: '/page/user?id=123'
}
}
})
创建名称为onShareAppMessage的小程序项目。
页面index的WXML示例代码如下:
<view>
welcome to 小程序 !
</view>
页面index的JS示例代码如下:
Page({
onShareAppMessage: function () {
return {
title: '自定义转发标题',
path: '/pages/showtext/showtext'
}
}
})
页面showtext的WXML示例代码如下:
<text>this is:pages/showtext/showtext.wxml</text>
页面showtext的JS示例代码如下:
Page({
})
程序运行效果如图2-xx所示。
图2-xx 出现分享按钮
2.3.2.2.7 页面跳转和路由
一个小程序拥有多个页面,可以通过wx.navigateTo转到一个新的页面。如图2-xx所示,在首页使用2次wx.navigateTo方法后,页面层级会有三层,在微信中把这样的一个页面层级称为页面栈。
图2-xx 使用2次wx.navigateTo后的页面栈
2.3.2.2.7.1 测试navigateTo和navigateBack和redirectTo方法的特性
创建名称为的小程序项目。
文件app.json示例代码如下:
{
"pages": [
"pages/a/a",
"pages/b/b",
"pages/c/c",
"pages/d/d",
"pages/e/e"
],
"window": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "Weixin",
"navigationBarBackgroundColor": "#ffffff"
},
"style": "v2",
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
页面a的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">a.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
页面a的JS示例代码如下:
Page({
onLoad() {
console.log("a onLoad");
},
onUnload() {
console.log("a onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/b/b' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/b/b' });
}
})
页面b的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">b.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面b的JS示例代码如下:
Page({
onLoad() {
console.log("b onLoad");
},
onUnload() {
console.log("b onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/c/c' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/c/c' });
},
closeMethod() {
wx.navigateBack();
}
})
页面c的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">c.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
<button bind:tap="redirectToMethod_to_e" style="width:100%; margin-top: 20px;">redirectToMethod_to_e</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面c的JS示例代码如下:
Page({
onLoad() {
console.log("c onLoad");
},
onUnload() {
console.log("c onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/d/d' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/d/d' });
},
redirectToMethod_to_e() {
wx.redirectTo({ url: '/pages/e/e' });
},
closeMethod() {
wx.navigateBack();
}
})
页面d的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">d.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面d的JS示例代码如下:
Page({
onLoad() {
console.log("d onLoad");
},
onUnload() {
console.log("d onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/c/c' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/c/c' });
},
closeMethod() {
wx.navigateBack();
}
})
页面e的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">e.wxml</view>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面e的JS示例代码如下:
Page({
onLoad() {
console.log("e onLoad");
},
onUnload() {
console.log("e onUnload");
},
closeMethod() {
wx.navigateBack();
}
})
使用这5个页面[A,B,C,D,E]来测试3个API[navigateTo,navigateBack,redirectTo]的应用效果。
后续为了表述方便,会采用这样的方式描述页面栈:[pageA,pageB,pageC],其中pageA在最底,pageC在最顶。小程序宿主环境限制页面栈的最大层级为10层(请参考最新的小程序的标准),也就是当页面栈到达10层之后就没有办法再推入新的页面了。下面通过上面的示例代码(项目pageStack)测试以下几个和导航相关的API(假定当前页面栈的状态为[pageA,pageB,pageC]):
(1)使用wx.navigateTo({url:'pageD'})可以往当前页面栈中多推入一个pageD,此时页面栈变成[pageA,pageB,pageC,pageD]。
(2)使用wx.navigateBack()可以退出当前页面栈的最顶的页面,此时页面栈变成[pageA,pageB,pageC]。
(3)使用wx.redirectTo({url:'pageE'})是替换当前页面变成pageE,此时页面栈变成[pageA,pageB,pageE],当页面栈到达10层没法再新增的时候,往往就是使用redirectTo这个API进行页面跳转。
2.3.2.2.7.2 测试tabbar
创建名称为的小程序项目。
文件app.json示例代码如下:
{
"pages": [
"pages/a/a",
"pages/b/b",
"pages/c/c",
"pages/d/d",
"pages/e/e",
"pages/f/f",
"pages/g/g"
],
"tabBar": {
"list": [
{
"text": "a_page",
"pagePath": "pages/a/a"
},
{
"text": "f_page",
"pagePath": "pages/f/f"
},
{
"text": "g_page",
"pagePath": "pages/g/g"
}
]
},
"window": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "Weixin",
"navigationBarBackgroundColor": "#ffffff"
},
"style": "v2",
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
页面a的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">a.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
页面a的JS示例代码如下:
Page({
onLoad() {
console.log("a onLoad");
},
onUnload() {
console.log("a onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/b/b' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/b/b' });
}
})
页面b的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">b.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面b的JS示例代码如下:
Page({
onLoad() {
console.log("b onLoad");
},
onUnload() {
console.log("b onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/c/c' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/c/c' });
},
closeMethod() {
wx.navigateBack();
}
})
页面c的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">c.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
<button bind:tap="redirectToMethod_to_e" style="width:100%; margin-top: 20px;">redirectToMethod_to_e</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面c的JS示例代码如下:
Page({
onLoad() {
console.log("c onLoad");
},
onUnload() {
console.log("c onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/d/d' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/d/d' });
},
redirectToMethod_to_e() {
wx.redirectTo({ url: '/pages/e/e' });
},
closeMethod() {
wx.navigateBack();
}
})
页面d的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">d.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
<button bind:tap="navigateBackMethod" style="width:100%; margin-top: 20px;">navigateBackMethod</button>
<button bind:tap="redirectToMethod" style="width:100%; margin-top: 20px;">redirectToMethod</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面d的JS示例代码如下:
Page({
onLoad() {
console.log("d onLoad");
},
onUnload() {
console.log("d onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/c/c' });
},
navigateBackMethod() {
wx.navigateBack();
},
redirectToMethod() {
wx.redirectTo({ url: '/pages/c/c' });
},
closeMethod() {
wx.navigateBack();
}
})
页面e的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">e.wxml</view>
<button bind:tap="switchTab_f" style="width:100%; margin-top: 20px;">switchTab_f</button>
<button bind:tap="closeMethod" style="width:100%; margin-top: 40px;">closeMethod</button>
页面e的JS示例代码如下:
Page({
onLoad() {
console.log("e onLoad");
},
onUnload() {
console.log("e onUnload");
},
switchTab_f() {
wx.switchTab({ url: '/pages/f/f' })
},
closeMethod() {
wx.navigateBack();
}
})
页面f的WXML示例代码如下:
<text>pages/f/f.wxml</text>
页面f的JS示例代码如下:
Page({
onLoad() {
console.log("f onLoad");
},
onUnload() {
console.log("f onUnload");
}
})
页面g的WXML示例代码如下:
<text>pages/g/g.wxml</text>
页面g的JS示例代码如下:
Page({
onLoad() {
console.log("g onLoad");
},
onUnload() {
console.log("g onUnload");
}
})
小程序提供了原生的Tabbar支持,可以在app.json声明tabBar字段来定义Tabbar页面(更多详细参数请参考Tabbar官方文档)。
如下示例代码是在app.json中定义小程序底部tab:
{
"tabBar": {
"list": [
{ "text": "Tab1", "pagePath": "pageA" },
{ "text": "Tab1", "pagePath": "pageF" },
{ "text": "Tab1", "pagePath": "pageG" }
]
}
}
假定现在页面栈的状态为[pageA,pageB,pageE],在pageE中使用wx.switchTab({url:'pageF'}),此时原来的页面栈会被清空(除了已经声明为Tabbar页面的pageA之外的其他页面[pageB,pageE]会被销毁),然后会切到pageF所在的tab页面,页面栈变成[pageF],此时点击Tab1切回到pageA时,pageA不会再触发onLoad,因为pageA没有被销毁。
补充一下,wx.navigateTo和wx.redirectTo只能打开非TabBar页面,wx.switchTab只能打开Tabbar页面。
2.3.2.2.7.3 测试reLaunch方法
可以使用wx.reLaunch({url:'pageH'})重启小程序,并且打开pageH,此时页面栈为[pageH]。
创建名称为reLaunchTest的小程序项目。
页面a的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">a.wxml</view>
<button bind:tap="navigateToMethod" style="width:100%; margin-top: 20px;">navigateToMethod</button>
页面a的JS示例代码如下:
Page({
onLoad() {
console.log("a onLoad");
},
onUnload() {
console.log("a onUnload");
},
navigateToMethod() {
wx.navigateTo({ url: '/pages/h/h' });
}
})
页面h的WXML示例代码如下:
<view style="text-align: center; font-size: 40px;">h.wxml</view>
<button bind:tap="reLaunchMethod" style="width:100%; margin-top: 20px;">reLaunchMethod</button>
页面h的JS示例代码如下:
Page({
onLoad() {
console.log("h onLoad");
},
onUnload() {
console.log("h onUnload");
},
reLaunchMethod() {
wx.reLaunch({ url: '/pages/h/h' })
}
})
程序运行后在h页面可以重启小程序,重启小程序后自动进入h页面。
2.3.2.2.7.4 页面路由触发方式及页面生命周期函数的对应关系
表2-xx罗列了详细的页面路由触发方式及页面生命周期函数的对应关系。
路由方式 | 触发时机 | 路由前页面生命周期 | 路由后页面生命周期 |
---|---|---|---|
初始化 | 小程序打开的第一个页面 | onLoad, onShow | |
打开新页面 调用 | API wx.navigateTo | onHide | onLoad, onShow |
页面重定向 调用 | API wx.redirectTo | onUnload | onLoad, onShow |
页面返回 调用 | API wx.navigateBack | onUnload | onShow |
Tab | 切换 调用API wx.switchTab | 请参考表3-6 | 请参考表3-6 |
重启动 | 调用 API wx.reLaunch | onUnload | onLoad, onShow |
表2-xx 页面路由触发方式及页面生命周期函数的对应关系
2.3.2.2.7.5 tabbar及页面生命周期函数的对应关系
Tab切换对应的生命周期(以A、B页面为Tabbar页面,C是从A页面打开的页面,D页面是从C页面打开的页面为例),如表3-6所示,注意Tabbar页面初始化之后不会被销毁。
当前页面 | 路由后页面 | 触发的生命周期(按顺序) |
---|---|---|
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() |
表3-6 Tab切换对应的生命周期
一个小程序页面可以分解成多个组件,组件是小程序页面的基本组成单元。为了让开发者可以快速的进行开发,小程序宿主环境提供了一系列的基础组件。
组件是在WXML模板文件中使用的,WXML的语法和HTML语法相似,小程序使用标签名来引用一个组件,通常包含开始标签和结束标签,标签的属性用来描述该组件。组件的示例代码如下:
<!-- page.wxml -->
<image mode="scaleToFill" src="img.png"></image>
需要注意的是,所有组件名称和属性都是小写的,多个单词之间会以英文横杠"-"进行连接。
对于一些容器组件,其内容可以声明在其开始标签和结束标签之间。示例代码如下:
<!-- page.wxml -->
<view>
<image mode="scaleToFill" src="img.png"></image>
<view>
<view>1</view>
<view>2</view>
<view>3</view>
</view>
</view>
所有的组件都拥有表2-xx列举的属性,主要涉及样式和事件绑定。
属性名 | 类型 | 描述 | 其他说明 |
---|---|---|---|
id | String | 组件的唯一标识 | 保持整个页面唯一 |
class | String | 组件的样式类 | 在对应的WXSS文件中定义的样式类 |
style | String | 组件的内联样式 | 可以通过数据绑定进行动态设置的内联样式 |
hidden | Boolean | 组件是否显示 | 所有组件默认显示 |
data-* | Any | 自定义属性 | 组件上触发的事件时,会发送给事件处理函数 |
bind/catch | EventHandler | 事件 | 详情见3.5节 |
表3-7 组件共有属性
组件都拥有各自的自定义属性,可以对该组件的功能或者样式进行修饰,以image图片组件为例,其属性列表如表3-8所示。
属性名 | 类型 | 默认值 | 描述 |
---|---|---|---|
src | String | 图片资源地址 | |
mode | String | 'scaleToFill' | 图片裁剪、缩放的模式 |
lazy-load | Boolean | false | 图片懒加载。只针对page与scroll-view下的image有效 1.5.0 |
binderror | HandleEvent | 当错误发生时触发事件,事件对象event.detail = {errMsg: 'something wrong'} | |
bindload | HandleEvent | 当图片载入完毕时触发事件,事件对象event.detail = {height:'图片高度px', width:'图片宽度px'} |
表3-8 Image图片组件属性
为了不占篇幅,不在本教程中展开所有组件的属性说明,请在使用时前往如下官方文档的网址进行查阅相关组件的使用说明即可:
https://mp.weixin.qq.com/debug/wxadoc/dev/component
宿主环境提供了丰富的API,可以很方便的调用微信所提供的能力。wx.navigateTo可以保留当前页面,然后跳转到新的页面。这里的wx对象实际上就是小程序的宿主环境所提供的全局对象,几乎所有小程序的API都挂载在wx对象之下(除了Page/App等特殊的构造器),所以本教程谈到API的概念时,通常指的是wx对象下的方法。
小程序提供的API按照功能主要分为几大类:网络、媒体、文件、数据缓存、位置、设备、界面、界面节点信息还有一些特殊的开放接口,下面介绍一下API一般调用的约定:
(1)wx.on*开头的API是监听某个事件发生的API接口,接受一个Callback函数作为参数。当该事件触发时,会调用Callback函数。
(2)如未特殊约定,多数API接口为异步接口,都接受一个Object作为参数。
(3)API的Object参数一般由success、fail、complete三个回调来接收接口的调用结果。例如通过wx.request发起网络请求的示例代码如下:
wx.request({
url: 'test.php',
data: {},
header: { 'content-type': 'application/json' },
success: function (res) {
// 收到https服务成功后返回
console.log(res.data)
},
fail: function () {
// 发生网络错误等情况触发
},
complete: function () {
// 成功或者失败后触发
}
})
API接口的回调函数的详细说明如表3-9所示。
参数名字 | 类型 | 必填 | 描述 |
---|---|---|---|
success | Function | 否 | 接口调用成功的回调函数 |
fail | Function | 否 | 接口调用失败的回调函数 |
complete | Function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
表3-9 API接口回调说明
(4)wx.get*开头的API是获取宿主环境数据的接口。
(5)wx.set*开头的API是写入数据到宿主环境的接口。
还有需要注意的是,API调用大多数都是异步的,其次,有部分API会拉起微信的原生界面,此时会触发Page的onHide方法,当用户从原生界面返回到小程序时,会触发Page的onShow方法。
API的数量非常多,而且随着宿主环境的迭代更新会持续的新增API,在这里并不想一一展开叙述每一个API的含义,开发者只要了解一般调用API的技巧,再通过如下网址所提供的官方API文档进行查询即可:
https://mp.weixin.qq.com/debug/wxadoc/dev/api
2.3.5.1 什么是事件
UI界面需要和用户进行互动,例如用户可能会点击界面上的某个按钮,又或者长按某个区域,这类反馈应该通知给开发者的逻辑层,需要将对应的处理状态呈现给用户。
有些时候程序上的“行为反馈”不一定是用户主动触发的,例如在视频video播放的过程中,播放进度是会一直变化的,这种反馈也应该通知给开发者做相应的逻辑处理。
在小程序里面,把这种“用户在渲染层的行为反馈”和“组件的部分状态反馈”抽象为渲染层传递给逻辑层的“事件”,如图3-7所示。
图3-7 渲染层产生用户交互事件传递给逻辑层
下面是一个简单的处理事件的小程序的示例代码:
<view id="tapTest" data-hi="WeChat" bindtap="tapName">Click me!</view>
// page.js
Page({
tapName: function (event) {
console.log(event)
}
})
事件是通过bindtap属性绑定在组件上的,同时在当前页面的Page构造器中定义对应的事件处理函数tapName,当用户点击该view区域时,达到触发条件生成事件tap,该事件处理函数tapName会被执行,同时还会收到一个事件对象event。
2.3.5.2 事件类型和事件对象
前边介绍过触发事件是由“用户在渲染层的行为反馈”和“组件的部分状态反馈”而引起的,由于不同组件的状态不一致,所以这里不讨论组件相关的事件。组件的事件可以参考其参数说明,详情见官方文档:
https://mp.weixin.qq.com/debug/wxadoc/dev/component
常见的事件类型如表3-10所示。
类型 | 触发条件 |
---|---|
touchstart | 手指触摸动作开始 |
touchmove | 手指触摸后移动 |
touchcancel | 手指触摸动作被打断,如来电提醒,弹窗 |
touchend | 手指触摸动作结束 |
tap | 手指触摸后马上离开 |
longpress | 手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发了这个事件,tap事件将不被触发 |
longtap | 手指触摸后,超过350ms再离开(推荐使用longpress事件代替) |
transitionend | 会在WXSS transition或wx.createAnimation动画结束后触发 |
animationstart | 会在一个WXSS animation动画开始时触发 |
animationiteration | 会在一个WXSS animation一次迭代结束时触发 |
animationend | 会在一个WXSS animation动画完成时触发 |
表3-10 常见的事件类型
当事件回调函数被触发的时候,会收到一个事件对象event,event对象的详细属性如下表所示。
属性 | 类型 | 说明 |
---|---|---|
type | String | 事件类型 |
timeStamp | Integer | 页面打开到触发事件所经过的毫秒数 |
target | Object | 触发事件的组件的一些属性值集合 |
currentTarget | Object | 当前组件的一些属性值集合 |
detail | Object | 额外的信息 |
touches | Array | 触摸事件,当前停留在屏幕中的触摸点信息的数组 |
changedTouches | Array | 触摸事件,当前变化的触摸点信息的数组 |
表3-11 事件对象属性
这里需要注意的是target和currentTarget的区别,currentTarget为当前事件所绑定的组件,而target则是触发该事件的源头组件。
创建名称为的小程序项目。
WXML示例代码如下:
<view id="outer" catchtap="handleTap">
<view id="inner">点击我</view>
</view>
JS示例代码如下:
Page({
handleTap: function (evt) {
console.log("target=" + evt.target.id);
console.log("currentTarget=" + evt.currentTarget.id);
console.log("type =" + evt.type);
console.log("timeStamp=" + evt.timeStamp);
console.log("detail x=" + evt.detail.x + " y=" + evt.detail.y);
console.log("touches identifier=" + evt.touches[0].identifier);
console.log("touches force=" + evt.touches[0].force);
console.log("touches clientX=" + evt.touches[0].clientX);
console.log("touches clientY=" + evt.touches[0].clientY);
console.log("touches pageX=" + evt.touches[0].pageX);
console.log("touches pageY=" + evt.touches[0].pageY);
console.log("touches screenX=" + evt.touches[0].screenX);
console.log("touches screenY=" + evt.touches[0].screenY);
console.log("changedTouches identifier=" + evt.changedTouches[0].identifier);
console.log("changedTouches force=" + evt.changedTouches[0].force);
console.log("changedTouches clientX=" + evt.changedTouches[0].clientX);
console.log("changedTouches clientY=" + evt.changedTouches[0].clientY);
console.log("changedTouches pageX=" + evt.changedTouches[0].pageX);
console.log("changedTouches pageY=" + evt.changedTouches[0].pageY);
console.log("changedTouches screenX=" + evt.changedTouches[0].screenX);
console.log("changedTouches screenY=" + evt.changedTouches[0].screenY);
}
})
程序运行结果如图2-xx所示。
图2-xx 运行效果
关于target和currentTarget对象的详细参数如表3-12所示。
属性 | 类型 | 说明 |
---|---|---|
id | String | 当前组件的id |
tagName | String | 当前组件的类型 |
dataset | Object | 当前组件上由data-开头的自定义属性组成的集合 |
表3-12 target和currentTarget事件对象属性
关于touch和changedTouches对象的详细参数如表3-13所示。
属性 | 类型 | 说明 |
---|---|---|
identifier | Number | 触摸点的标识符 |
pageX, pageY | Number | 距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴 |
clientX, clientY | Number | 距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴 |
表3-13 touch和changedTouches对象属性
2.3.5.3 事件绑定与冒泡捕获
事件绑定的写法和组件属性一致,以key="value"的形式,其中:
(1)key以bind或者catch开头,然后跟上事件的类型,如bindtap、catchtouchstart。自基础库版本1.5.0起,bind和catch后可以紧跟一个冒号,其含义不变,如bind:tap、catch:touchstart。同时bind和catch的前面还可以加上capture-来表示捕获阶段。
(2)value是一个字符串,需要在对应的页面Page构造器中定义同名的函数,否则触发事件时在控制台会有报错信息。
bind和capture-bind的含义分别代表事件的冒泡阶段和捕获阶段,其触发的顺序如图3-8所示。
图3-8 事件捕获和冒泡触发时序
创建名称为的小程序项目。
WXML示例代码如下:
<view id="outer" bind:touchstart="handleTap1" capture-bind:touchstart="handleTap2">
outer view
<view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
inner view
</view>
</view>
JS示例代码如下:
Page({
handleTap1() {
console.log("handleTap1");
},
handleTap2() {
console.log("handleTap2");
},
handleTap3() {
console.log("handleTap3");
},
handleTap4() {
console.log("handleTap4");
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
点击innerview后的调用顺序为:handleTap2、handleTap4、handleTap3、handleTap1。
bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。
创建名称为的小程序项目。
WXML示例代码如下:
<view id="outer" bind:touchstart="handleTap1" capture-catch:touchstart="handleTap2">
outer view
<view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
inner view
</view>
</view>
JS示例代码如下:
Page({
handleTap1() {
console.log("handleTap1");
},
handleTap2() {
console.log("handleTap2");
},
handleTap3() {
console.log("handleTap3");
},
handleTap4() {
console.log("handleTap4");
}
})
程序运行效果如图2-xx所示。
图2-xx 运行效果
使用capture-catch后将只触发handleTap2,capture-catch将中断捕获阶段和取消冒泡阶段。
注意,除表3-10列举的事件类型之外的其他组件自定义事件,如无特殊声明都是非冒泡事件,例如<form/>的submit事件,<input/>的input事件,<scroll-view/>的scroll事件。
小程序的宿主环境一直在迭代更新,提供更多的能力给开发者去完成更多的事情,所以小程序会运行在不同版本的宿主环境下。为了让小程序在不同环境下都能提供相应的服务,需要了解一下在小程序中如何实现兼容的办法。
可能需要针对不同手机进行程序上的兼容,此时可以使用wx.getSystemInfo或者wx.getSystemInfoSync来获取手机品牌、操作系统版本号、微信版本号以及小程序基础库版本号等等,通过这个信息,可以针对不同平台做差异化的服务。示例代码如下:
wx.getSystemInfoSync()
/*
{
brand: "iPhone", // 手机品牌
model: "iPhone 6", // 手机型号
platform: "ios", // 客户端平台
system: "iOS 9.3.4", // 操作系统版本
version: "6.5.23", // 微信版本号
SDKVersion: "1.7.0", // 小程序基础库版本
language: "zh_CN", // 微信设置的语言
pixelRatio: 2, // 设备像素比
screenWidth: 667, // 屏幕宽度
screenHeight: 375, // 屏幕高度
windowWidth: 667, // 可使用窗口宽度
windowHeight: 375, // 可使用窗口高度
fontSizeSetting: 16 // 用户字体大小设置
}
*/
随着宿主环境的更新,新版本的宿主环境会提供一些新的API,可以通过判断此API是否存在来做程序上的兼容。示例代码如下:
If (wx.openBluetoothAdapter) {
wx.openBluetoothAdapter()
} else {
// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
wx.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
小程序还提供了wx.canIUse这个API,用于判断接口或者组件在当前宿主环境是否可用,其参数格式为:
${API}.${method}.${param}.${options}或者${component}.${attribute}.${option}
各个段的含义如下:
(1)${API}代表API名字
(2)${method}代表调用方式,有效值为return,success,object,callback
(3)${param}代表参数或者返回值
(4)${options}代表参数的可选值
(5)${component}代表组件名字
(6)${attribute}代表组件属性
(7)${option}代表组件属性的可选值
示例代码如下。
// 判断接口及其参数在宿主环境是否可用
wx.canIUse('openBluetoothAdapter')
wx.canIUse('getSystemInfoSync.return.screenWidth')
wx.canIUse('getSystemInfo.success.screenWidth')
wx.canIUse('showToast.object.image')
wx.canIUse('onCompassChange.callback.direction')
wx.canIUse('request.object.method.GET')
// 判断组件及其属性在宿主环境是否可用
wx.canIUse('contact-button')
wx.canIUse('text.selectable')
wx.canIUse('button.open-type.contact')
可以选择合适的判断方法来做小程序的向前兼容,以保证小程序在旧版本的微信客户端里也能工作正常。在不得已的情况下(小程序强依赖某个新的API或者组件时),还可以通过在小程序管理后台设置“基础库最低版本设置”来达到不向前兼容的目的。例如选择设置小程序只支持1.5.0版本以上的宿主环境,那么当运行着1.4.0版本宿主环境的微信用户打开小程序的时候,微信客户端会显示当前小程序不可用,并且提示用户应该去升级微信客户端。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。