# 小程序框架
# 1. 逻辑层
# 1.1注册小程序
# App(Object)
App() 函数用来注册一个小程序。接受一个 Object 参数,
App() 必须在 app.js 中调用,且只能调用一次。
Object参数说明
属性 | 类型 | 描述 | 触发时机 |
---|---|---|---|
onLaunch | Function | 生命周期回调—监听小程序初始化 | 小程序初始化完成时触发(全局只触发一次) |
onShow | Function | 生命周期回调—监听小程序显示 | 小程序启动,或从后台进入前台显示时触发 |
onHide | Function | 生命周期回调—监听小程序隐藏 | 小程序从前台进入后台时触发 |
onError | Function | 错误监听函数 | 当小程序发生脚本错误,或者 api 调用失败时触发,会带上错误信息 |
onPageNotFound | Function | 页面不存在监听函数 | 小程序要打开的页面不存在时触发,会带上页面信息回调该函数 |
其他 | 不限制 | 开发者可自由添加任意的 function 或数据到 Object 参数中,用this 可访问 |
前台、后台定义
小程序启动后,用户能够看到当前界面,此时小程序处于前台状态, 当用户通过右上角椭圆按钮关闭小程序或者离开宿主app时,小程序并没有立刻终止运行,而是进入了后台状态,此时会触发 onHide 回调事件。
当用户再次进入宿主app或再次打开小程序,小程序又会从后台切换至前台,此时会触发onShow 回调事件; 如果用户长时间没有打开小程序,或者系统资源紧张,小程序可能被销毁,此时小程序会完全退出。
示例代码
App({
onLaunch: function(options) {
console.log("launch 参数",options)
},
onShow: function(options) {
console.log("onShow 参数",options)
},
onHide: function() {
},
onError: function(error) {
console.log("错误信息:",error)
},
globalData: ''
})
# onLaunch(Object)
小程序初始化完成时触发,全局仅触发一次。
Object 参数说明
字段 | 类型 | 说明 |
---|---|---|
path | String | 打开小程序的路径 |
query | Object | 打开小程序的 query 字段,可通过分享或者唤起协议中配置 |
referrerInfo | Object | 由另一个小程序或其他 App 进入小程序时,返回此字段 |
referrerInfo.appId | String | 来源小程序的 appId,详见下方说明 |
referrerInfo.extraData | Object 其他来源传过来的数据 |
# onShow(Object)
小程序启动,或从后台进入前台时触发,每次切换到前台均会触发。
Object 参数说明
与 onLaunch 一致
# onHide()
小程序从前台进入后台时触发,每次切换到后台均会触发
# onError(String error)
小程序发生脚本错误,或者 api 调用失败时触发。
参数说明
名称 | 类型 | 说明 |
---|---|---|
error | String | 包含堆栈的错误信息 |
# onPageNotFound(Object)
要打开的目标页面不存在时触发,经常用于捕获路由跳转的目标页面不存在情况。
参数说明
名称 | 类型 | 说明 |
---|---|---|
path | String | 不存在的页面的路径 |
query | Object | 打开不存在得页面的 query 参数 |
isEntryPage | Boolean | 是否本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面) |
开发者可以在 onPageNotFound 回调中进行重定向处理,但必须在回调中同步处理,异步处理无效。
示例代码
App({
onPageNotFound(res) {
jd.redirectTo({
url: 'pages/index/index.fxml'
})
}
})
注意
- 如果开发者没有添加 onPageNotFound 监听,当跳转的目标页面不存在时,将由宿主APP接管处理;
- 请确保 onPageNotFound 回调中重定的目标页面存在,否则将由宿主APP接管处理,并且不再回调 onPageNotFound,避免调用死循环。
# getApp(Object)
全局方法,getApp() 函数可以用来获取到小程序 App 实例,多用于页面中调用,获取APP实例的全局数据和方法。值得注意的是,在app.js中的 APP()方法中调用时,可通过 this 直接获取到,在其他页面中用 getApp() 方法。
Object 参数说明
字段 | 类型 | 说明 |
---|---|---|
allowDefault | Boolean | 在 App 未定义时返回默认实现。当App被调用时,默认实现中定义的属性会被覆盖合并到 App 中。 |
示例代码
const APP = getApp();
console.log(APP.globalData) // 输出 global data
# 2.注册页面
# 2.1 使用 Page 构造器注册页面
Page(Object) 函数用来注册一个页面。接受一个 Object 类型参数,其指定页面的初始数据、生命周期回调、事件处理函数等。
属性 | 类型 | 描述 |
---|---|---|
data | Object | 页面的初始数据 |
onLoad | Function | 生命周期回调—页面加载时触发 |
onShow | Function | 生命周期回调—监听页面显示 |
onReady | Function | 生命周期回调—监听页面初次渲染完成 |
onHide | Function | 生命周期回调—监听页面隐藏 |
onUnload | Function | 生命周期回调—监听页面卸载 |
onPullDownRefresh | Function | 触发下拉刷新时执行 |
onReachBottom | Function | 页面触底时执行 |
onShareAppMessage | Function | 转发 |
onPageScroll | Function | 页面滚动触发事件的处理函数 |
onTabItemTap | Function | 当前是 tab 页时,点击 tab 时触发 |
其他 | Any | 开发者可以添加任意的函数或数据到 Object 参数中,在本页面的函数中用 this 可以访问 |
示例代码
//index.js
Page({
data: {
userName: ""
},
onLoad: function(e) {
this.getName();
},
onReady: function() {
},
onShow: function() {
},
onHide: function() {
},
onUnload: function() {
},
onPullDownRefresh: function() {
},
onReachBottom: function() {
},
onShareAppMessage: function () {
},
onPageScroll: function() {
},
onTabItemTap(item) {
console.log("当前点击的是:",JSON.stringify(item))
},
// Event handler.
getName: function() {
this.setData({
userName:"cortana"
})
},
customData: {
dName: 'cortana'
}
})
详细的参数含义和使用请参考 Page 参考文档 。
# 2.1.1 初始数据
data 是页面第一次渲染使用的初始数据。
data 中的数据必须是以下类型:字符串,数字,布尔值,对象,数组。
渲染层可以通过 FXML 对数据进行绑定。
示例代码
<view>{{userName}}</view>
<view>{{cover[0].url}}</view>
Page({
data: {
userName: 'cortana',
cover: [{url: 'http://xxxxx.jpg',title:"avatar"}, {url: 'http://xxxxx.jpg',title:"detail"}]
}
})
# 2.2 生命周期回调函数
生命周期的触发以及页面的路由方式详见
# onLoad(Object query)
页面加载时触发。一个页面仅会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数。
参数说明
名称 | 类型 | 说明 |
---|---|---|
query | Object | 打开当前页面路径中的参数 |
# onShow()
页面显示/切入前台时触发。
# onReady()
页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。
注意
对界面内容进行设置的 API 如 jd.setNavigationBarTitle,请在onReady之后进行。
# onHide()
页面隐藏/切入后台时触发。 如 navigateTo 或底部 tab 切换到其他页面,小程序切入后台等。
# onUnload()
页面卸载时触发。如 redirectTo 或 navigateBack 到其他页面时。
# 2.3 页面事件处理函数
# onPullDownRefresh()
监听用户下拉刷新事件。
# onReachBottom()
监听用户上拉触底事件。
# onPageScroll(Object)
监听用户滑动页面事件。
参数说明
属性 | 类型 | 说明 |
---|---|---|
scrollTop | Number | 页面在垂直方向已滚动的距离(单位px) |
# onShareAppMessage(Object)
监听用户点击页面内分享按钮(<button>
组件 open-type="share")或右上角菜单“推荐给朋友”按钮的行为,支持自定义转发内容。此事件需要 return 一个 Object,用于配置分享的内容,详见“分享内容配置”。
Object 参数说明
属性 | 类型 | 说明 |
---|---|---|
from | String | 转发事件来源。button:页面内转发按钮;menu:右上角“推荐给朋友”菜单 |
target | Object | 如果 from 值是 button,则 target 是触发这次转发事件的 button,否则为 undefined |
webViewUrl | String | 页面中包含 |
<web-view> 组件时,返回当前 <web-view> 的url |
分享内容配置
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
mpId | string | 否 | 微信小程序id,此场景用于分享到微信后,用户点击分享卡片,进入该appid对应的微信小程序,实现引流到微信小程序 |
title | string | 否 | 转发标题 |
type | number | 否 | 转发形式(0 - 微信小程序正式版 ;1 - 微信小程序开发版;2 - 微信小程序体验版; |
path | string | 否 | 小程序路径 |
mpPath | string | 否 | 微信小程序路径 |
imageUrl | string | 是 | 图片地址(小程序封面图或H5页封面) |
channel | string | 否 | 渠道(不写默认微信朋友,微信朋友圈) |
url | string | 否 | H5链接地址(H5分享填写,不填默认中间页) |
desc | string | 否 | 分享内容摘要 |
示例代码
Page({
onShareAppMessage: function (res) {
console.log(res.target);
return {
mpId: '分享的微信小程序appid',
title: '分享的标题',
type: 0,
desc: '分享的描述、摘要等',
imageUrl: 'http://pic30.finogeeks.com/20130619/9885883_210838271000_2.jpg',
path: 'page/component/index',
mpPath:'分享的微信小程序路径',
channel:'Wxfriends,Wxmoments',
url: 'https://www.finclip.com/develop/index/ao00f99475552b3131',
}
}
})
示例代码分享到微信好友后,会打开对应的正式版微信小程序,分享到朋友圈会打开url对应的H5页面。
# onTabItemTap(Object)
点击顶部、底部 tab 时触发
Object 参数说明
参数 | 类型 | 说明 |
---|---|---|
index | String | 被点击 tabItem 的序号,从 0 开始 |
pagePath | String | 被点击 tabItem 的页面路径 |
text | String | 被点击 tabItem 的按钮文字 |
示例代码
Page({
onTabItemTap(item) {
console.log("tabbar点击:",item);
}
})
# 2.4组件事件处理函数
Page 中还可以定义组件事件处理函数。在.fxml文件中,组件中加入事件绑定,当事件被触发时,就会执行 Page 中定义的事件处理函数。
示例代码
<view bindtap="getUsername">获取用户名</view>
Page({
getUsername: function() {
console.log("点击了获取用户名")
}
})
# 2.5 使用 Component 构造器构造页面
基础库 1.6.3 开始支持
Page 构造器适用于简单的页面。但对于复杂的页面, Page 构造器可能并不好用。
此时,可以使用 Component 构造器来构造页面。 Component 构造器的主要区别是:方法需要放在 methods: { } 里面。
代码示例
Component({
data: {
text: "This is page data."
},
methods: {
onLoad: function(options) {
// 页面创建时执行
},
onPullDownRefresh: function() {
// 下拉刷新时执行
},
// 事件响应函数
viewTap: function() {
// ...
}
}
})
这种创建方式非常类似于 自定义组件 ,可以像自定义组件一样使用 behaviors 等高级特性。具体细节请阅读 Component 构造器 章节。
# Page.route
到当前页面的路径,类型为 String。
示例代码
<view bindtap="getCurrentRoute"> 点击查看当前页面路由 </view>
Page({
getCurrentRoute: function() {
console.log("当前页面route为:",this.route)
}
})
# Page.prototype.setData(Object data, Function callback)
setData 函数用于将数据用异步的方式从逻辑层发送到视图层,同时改变对应的 this.data 的值(同步)。
Object 参数说明
字段 | 类型 | 必填 | 描述 |
---|---|---|---|
data | Object | 是 | 本次要改变的数据 |
callback | Function | 否 | setData 引起的界面更新渲染完毕后的回调函数 |
Object 以 key: value 的形式表示,将 this.data 中的 key 对应的值改变成 value。
注意
- 直接修改 this.data 无法改变页面的状态的。
- 仅支持设置可 JSON 化的数据。
- 单次设置的数据量不宜过大,不能超过 1024k。
- 请不要手动把 data 中任何一项的 value 设为 undefined 。
示例代码
<!--index.fxml-->
<view>{{title}}</view>
<button bindtap="changeTitle"> 字符串类型的改变 </button>
<view>{{num}}</view>
<button bindtap="changeNum"> 数值类型的改变</button>
<view>{{array[0].name}}</view>
<button bindtap="changeArray">数组类型的改变 </button>
<view>{{object.name}}</view>
<button bindtap="changeObject">对象类型的改变 </button>
//index.js
Page({
data: {
title: '我是title',
num: 0,
array: [{name: 'cortana'}],
object: {
text: 'init data'
}
},
changeText: function() {
this.setData({
title: '新标题'
})
},
changeNum: function() {
this.setData({
num: this.data.num +1
})
},
changeArray: function() {
this.setData({
'array[0].name':'Mary'
})
},
changeObject: function(){
this.setData({
'object.name': 'Mary'
});
}
})
# 3. 页面配置
每一个小程序页面也可以使用.json文件来对本页面的窗口表现进行配置。
页面的配置只能设置 app.json 中部分 window 配置项的内容,页面中配置项会覆盖 app.json 的 window 中相同的配置项,可配置的选项如下:
属性 | 类型 | 默认值 | 描述 |
---|---|---|---|
navigationBarBackgroundColor | HexColor | #000000 | 导航栏背景颜色,如 #000000 |
navigationBarTextStyle | String | white | 导航栏标题颜色,仅支持 black、white |
navigationBarTitleText | String | 导航栏标题文字内容 | |
navigationBarTitleFixed | Boolean | false | 标题是否固定,设置为 true 则加载H5时,标题不随H5标题变更;设置为 false则会随着H5的title变更 |
backgroundColor | HexColor | #ffffff | 窗口的背景色 |
backgroundTextStyle | String | dark | 下拉 loading 的样式,仅支持 dark、light |
disableScroll | Boolean | false | 设置为 true 则页面整体不能上下滚动;仅在页面配置中有效, |
示例 my.json 如下:
{
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "个人中心",
"backgroundColor": "#eeeeee",
"backgroundTextStyle": "light"
}
# 3.1 模块化
# 3.1.1 文件作用域
在.js 文件中声明的变量和方法只在当前文件中有效;不同的文件中可以声明相同名字的变量和方法。
通过全局函数 getApp() 可以获取全局的应用实例,如果需要全局的数据可以在 App() 中设置,如:
// app.js
App({
globalData: "cortana"
})
# 3.1.2 模块化
可以将一些公共的代码抽离成为一个单独的 js 文件,作为一个模块。模块只有通过 module.exports 或者 exports 才能对外暴露接口。
小程序目前暂不支持「直接引入 node_modules」,需要通过 构建 npm 完成引入工作,需要时也可直接复制代码到小程序的目录中,再进行使用。
// util.js
function getDate() {
return new Date().toLocaleTimeString()
}
module.exports.getDate = getDate
exports.getDate = getDate
在需要使用这些模块的文件中,使用 require(path) 将公共代码引入
var util = require('util.js');
Page({
getDate: function() {
let d = util.getDate();
console.log(d);
}
})
提示
值得注意的是,require 引入模块时,需要使用相对路径。
# 4. 视图层
框架的视图层由 FXML 与 FTSS 编写,基础单元是组件。
- FXML 用于描述页面的结构,类似于 HTML ;
- FTSS 用于描述组件和页面的样式,是 css 的子集; fts 是小程序的一套脚本语言,基础语法同javascript,结合 FXML,可以创建出页面的结构。
组件 (Component) 是视图的基本组成单元,类似于HTML页面的各种标签,如div、span、img等
# 4.1 FXML
FXML 是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。
以下是一些简单的示例:
数据绑定
<!--fxml-->
<view> hello {{name}} </view>
// page.js
Page({
data: {
name: 'cortana'
}
})
列表渲染
<!--fxml-->
<view ft:for="{{array}}"> {{item}} </view>
// page.js
Page({
data: {
array: ["苹果", "香蕉", "橘子", "西瓜"]
}
})
条件渲染
<!--fxml-->
<view ft:if="{{type == 1}}"> 类型 1 </view>
<view ft:elif="{{view == 2'}}"> 类型 2 </view>
<view ft:else="{{view == 3}}"> 类型 3 </view>
// page.js
Page({
data: {
type: 1
}
})
模板
<!--fxml-->
<template name="cat">
<view>
age: {{name}}, age: {{age}}
</view>
</template>
<template is="cat" data="{{...cat1}}"></template>
// page.js
Page({
data: {
cat1: {name: 'blue', age: '2'},
}
})
事件
<view bindtap="getDate"> {{date}} </view> //点击事件 bindtap
Page({
data: {
date: ""
},
getDate: function(e) {
this.setData({
date: new Date()
})
}
})
# 4.2 FTSS
与 CSS FTSS 扩展的特性有:
- 尺寸单位
- 样式导入
尺寸单位
rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
设备 | rpx换算px (屏幕宽度/750) | px换算rpx (750/屏幕宽度) |
---|---|---|
iPhone5 | 1rpx = 0.42px | 1px = 2.34rpx |
iPhone6 | 1rpx = 0.5px | 1px = 2rpx |
iPhone6 Plus | 1rpx = 0.552px | 1px = 1.81rpx |
注意
- 开发FinClip小程序时设计师可以用 iPhone6 作为视觉稿的标准。
- 在较小的屏幕上不可避免的会有一些毛刺,请在开发时尽量避免这种情况。
样式导入
使用@import语句可以导入外联样式表,@import后跟需要导入的外联样式表的相对路径,用;表示语句结束。
示例代码
/** common.ftss **/
.small-p {
padding:5px;
}
/** app.ftss **/
@import "common.ftss";
.middle-p {
padding:15px;
}
内联样式
框架组件上支持使用 style、class 属性来控制组件的样式。
style:静态的样式统一写到 class 中。style 接收动态的样式,在运行时会进行解析,请尽量避免将静态的样式写进 style 中,以免影响渲染速度。
<view style="color:{{color}};" />
class:用于指定样式规则,其属性值是样式规则中类选择器名(样式类名)的集合,样式类名不需要带上.,样式类名之间用空格分隔。
<view class="normal_view" />
选择器
目前支持的选择器有:
选择器 | 样例 | 样例描述 |
---|---|---|
.class | .intro | 选择所有拥有 class="intro" 的组件 |
#id | #firstname | 选择拥有 id="firstname" 的组件 |
element | view | 选择所有 view 组件 |
element, element | view, checkbox 选择所有文档的 view 组件和所有的 checkbox 组件 | |
::after | view::after | 在 view 组件后边插入内容 |
::before | view::before | 在 view 组件前边插入内容 |
全局样式与局部样式
定义在 app.ftss 中的样式为全局样式,作用于每一个页面。在 page 的 FTSS 文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖 app.ftss 中相同的选择器。
# 4.3 FTS
FTS(是小程序的一套脚本语言,结合 FXML,可以构建出页面的结构。
注意
FTS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 FTS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 FTS 的运行环境和其他 JavaScript 代码是隔离的,FTS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 FTS 函数不能作为组件的事件回调。
以下是一些使用 FTS 的简单示例:
页面渲染
<!--fxml-->
<fts module="m1">
var msg = "hello world";
module.exports.message = msg;
</fts>
<view> {{m1.message}} </view>
页面输出
hello world
数据处理
// page.js
Page({
data: {
array: [1, 2, 3, 4, 5, 1, 2, 3, 4]
}
})
<!--fxml-->
<!-- 下面的 getMax 函数,接受一个数组,且返回数组中最大的元素的值 -->
<fts module="m1">
var getMax = function(array) {
var max = undefined;
for (var i = 0; i < array.length; ++i) {
max = max === undefined ?
array[i] :
(max >= array[i] ? max : array[i]);
}
return max;
}
module.exports.getMax = getMax;
</fts>
<!-- 调用 fts 里面的 getMax 函数,参数为 page.js 里面的 array -->
<view> {{m1.getMax(array)}} </view>
页面输出
5
# 4.4 FTS 响应事件
背景
当小程序需要实现频繁用户交互的效果时,如果采用常规实现方法,如:
页面有 2 个元素 A 和 B,用户在 A 上做 touchmove 手势,要求 B 也跟随移动,movable-view 就是一个典型的例子。一次 touchmove 事件的响应过程为:
i、touchmove 事件从视图层(View)抛到逻辑层(Service)
ii、逻辑层(Service)处理 touchmove 事件,再通过 setData 来改变 B 的位置
一次 touchmove 的响应需要经过 2 次的逻辑层和渲染层的通信以及一次渲染,通信的耗时比较大。
此外 setData 渲染也会阻塞其它脚本执行,导致了整个用户交互的动画过程会有延迟,交互的实际表现不会很理想。
解决方案
基于以上原因,可以使用 FTS 函数用来响应小程序事件,然后在视图层(View)处理 dom 样式,实现比较好的效果。
目前只能响应内置组件的事件,不支持自定义组件事件。
FTS 函数的除了纯逻辑的运算,还可以通过封装好的 ComponentDescriptor 实例来访问以及设置组件的 class 和样式,对于交互动画,设置 style 和 class 能满足绝大多数需求了。
FTS 函数的例子如下:
var event = function(event, ownerInstance) {
// 获取组件实例
var instance = ownerInstance.selectComponent('.some-component')
instance.setStyle({
color: 'red',
"font-size": '18rpx'
})
instance.setClass('other-class')
return false // 不往上冒泡
}
其中入参 event 是小程序事件对象基础上多了 event.instance 来表示触发事件的组件的 ComponentDescriptor 实例。
ownerInstance 表示的是触发事件的组件所在的组件的 ComponentDescriptor 实例,如果触发事件的组件是在页面内的,则 ownerInstance 表示的是页面实例。
ComponentDescriptor 目前支持的 API 如下:
方法 | 参数 | 描述 |
---|---|---|
selectComponent | selector 对象 | 返回组件的 ComponentDescriptor 实例。 |
selectAllComponents | selector 对象数组 | 返回组件的 ComponentDescriptor 实例数组。 |
setStyle | Object/string | 设置组件样式,支持rpx。设置的样式优先级比组件 fxml 里面定义的样式高。不能设置最顶层页面的样式。 |
addClass/removeClass/hasClass | string | 设置组件的 class。设置的 class 优先级比组件 fxml 里面定义的 class 高。不能设置最顶层页面的 class。 |
callMethod | (funcName:string, args:object) | 调用当前组件/页面在逻辑层(App Service)定义的函数。funcName表示函数名称,args表示函数的参数。 |
getComputedStyle | Array <string> | 指定样式名列表,返回节点对应样式名的当前值。 |
getBoundingClientRect | 无 | 返回节点的尺寸信息。 |
使用方法
FXML 定义事件:
<fts module="event" src="./event.fts"></fts>
<view bindtouchmove="{{event.touchmove}}" class="movable"></view>
注意:FTS 函数必须用 {{}} 括起来。
文件 event.ftx 里面定义并导出函数:
module.exports = {
touchmove: function(event, instance) {
console.log('log event')
},
otherEvent: function(event, instance) {
console.log('log event')
}
}
# 4.5 简易双向绑定
在 FXML 中,普通的属性的绑定是单向的。例如:
<input value="{{value}}" />
如果使用 this.setData({ value: 'leaf' }) 来更新 value ,this.data.value 和输入框的中显示的值都会被更新为 leaf ;但如果用户修改了输入框里的值,却不会同时改变 this.data.value 。
如果需要在用户输入的同时改变 this.data.value ,需要借助简易双向绑定机制。此时,可以在对应项目之前加入 model: 前缀:
<input model:value="{{value}}" />
这样,如果输入框的值被改变了, this.data.value 也会同时改变。同时, FXML 中所有绑定了 value 的位置也会被一同更新, 数据监听器 也会被正常触发。
在开发者工具中预览效果
用于双向绑定的表达式有如下限制:
<input model:value="值为 {{value}}" />
<input model:value="{{ a + b }}" />
都是非法的;
<input model:value="{{ a.b }}" />
这样的表达式目前暂不支持。
# 在自定义组件中传递双向绑定
双向绑定同样可以使用在自定义组件上。如下的自定义组件:
// custom-component.js
Component({
properties: {
myValue: String
}
})
<!-- custom-component.FXML -->
<input model:value="{{myValue}}" />
这个自定义组件将自身的 myValue 属性双向绑定到了组件内输入框的 value 属性上。这样,如果页面这样使用这个组件:
<custom-component model:my-value="{{pageValue}}" />
当输入框的值变更时,自定义组件的 myValue 属性会同时变更,这样,页面的 this.data.pageValue 也会同时变更,页面 FXML 中所有绑定了 pageValue 的位置也会被一同更新。
# 在自定义组件中触发双向绑定更新
自定义组件还可以自己触发双向绑定更新,做法就是:使用 setData 设置自身的属性。例如:
// custom-component.js
Component({
properties: {
myValue: String
},
methods: {
update: function() {
// 更新 myValue
this.setData({
myValue: 'leaf'
})
}
}
})
如果页面这样使用这个组件:
<custom-component model:my-value="{{pageValue}}" />
当组件使用 setData 更新 myValue 时,页面的 this.data.pageValue 也会同时变更,页面 FXML 中所有绑定了 pageValue 的位置也会被一同更新。
# 4.6 基础组件
框架为开发者提供了一系列基础组件,开发者可以通过组合这些基础组件进行快速开发。详细介绍请参考组件文档。
什么是组件
- 组件是视图层的基本组成单元。
- 一个组件通常包括 开始标签 和 结束标签,属性 用来修饰这个组件,内容 在两个标签之内。
<tagname property="value">
Content goes here ...
</tagname>
注意
所有组件与属性都是小写,以连字符-连接
属性类型
类型 | 描述 | 注解 |
---|---|---|
Boolean | 布尔值 | 组件写上该属性,不管是什么值都被当作 true;只有组件上没有该属性时,属性值才为false。如果属性值为变量,变量的值会被转换为Boolean类型 |
Number | 数字 | 1, 2.5 |
String | 字符串 | "string" |
Array | 数组 | [ 1, "string" ] |
Object | 对象 | { key: value } |
EventHandler | 事件处理函数名 "handlerName" 是 Page 中定义的事件处理函数名 | |
Any | 任意属性 |
公共属性
所有组件都有以下属性
属性名 | 类型 | 描述 | 注解 |
---|---|---|---|
id | String | 组件的唯一标示 | 保持整个页面唯一 |
class | String | 组件的样式类 | 在对应的 FTSS 中定义的样式类 |
style | String | 组件的内联样式 | 可以动态设置的内联样式 |
hidden | Boolean | 组件是否显示 | 所有组件默认显示 |
data-* | Any | 自定义属性 | 组件上触发的事件时,会发送给事件处理函数 |
bind* / catch* | EventHandler | 组件的事件 详见事件 |
特殊属性
几乎所有组件都有各自定义的属性,可以对该组件的功能或样式进行修饰,请参考各个组件的定义。
# 4.7 获取界面上的节点信息
# FXML节点信息
节点信息查询 API 可以用于获取节点属性、样式、在界面上的位置等信息。
最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。
示例代码
const query = ft.createSelectorQuery()
query.select('#the-id').boundingClientRect(function(res){
res.top // #the-id 节点的上边界坐标(相对于显示区域)
})
query.selectViewport().scrollOffset(function(res){
res.scrollTop // 显示区域的竖直滚动位置
})
query.exec()
上述示例中, #the-id 是一个节点选择器,与 CSS 的选择器相近但略有区别,请参见 SelectorQuery.select 的相关说明。
在自定义组件或包含自定义组件的页面中,推荐使用 this.createSelectorQuery 来代替 ft.createSelectorQuery ,这样可以确保在正确的范围内选择节点。
FXML节点布局相交状态
节点布局相交状态 API 可用于监听两个或多个组件节点在布局位置上的相交状态。这一组API常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。
这一组API涉及的主要概念如下。
- 参照节点:监听的参照节点,取它的布局区域作为参照区域。如果有多个参照节点,则会取它们布局区域的 交集 作为参照区域。页面显示区域也可作为参照区域之一。
- 目标节点:监听的目标,默认只能是一个节点(使用 selectAll 选项时,可以同时监听多个节点)。
- 相交区域:目标节点的布局区域与参照区域的相交区域。
- 相交比例:相交区域占参照区域的比例。
- 阈值:相交比例如果达到阈值,则会触发监听器的回调函数。阈值可以有多个。 以下示例代码可以在目标节点(用选择器 .target-class 指定)每次进入或离开页面显示区域时,触发回调函数。
示例代码
Page({
onLoad: function(){
ft.createIntersectionObserver().relativeToViewport().observe('.target-class', (res) => {
res.id // 目标节点 id
res.dataset // 目标节点 dataset
res.intersectionRatio // 相交区域占目标节点的布局区域的比例
res.intersectionRect // 相交区域
res.intersectionRect.left // 相交区域的左边界坐标
res.intersectionRect.top // 相交区域的上边界坐标
res.intersectionRect.width // 相交区域的宽度
res.intersectionRect.height // 相交区域的高度
})
}
})
以下示例代码可以在目标节点(用选择器 .target-class 指定)与参照节点(用选择器 .relative-class 指定)在页面显示区域内相交或相离,且相交或相离程度达到目标节点布局区域的20%和50%时,触发回调函数。
示例代码
Page({
onLoad: function(){
ft.createIntersectionObserver(this, {
thresholds: [0.2, 0.5]
}).relativeTo('.relative-class').relativeToViewport().observe('.target-class', (res) => {
res.intersectionRatio // 相交区域占目标节点的布局区域的比例
res.intersectionRect // 相交区域
res.intersectionRect.left // 相交区域的左边界坐标
res.intersectionRect.top // 相交区域的上边界坐标
res.intersectionRect.width // 相交区域的宽度
res.intersectionRect.height // 相交区域的高度
})
}
})
注意
与页面显示区域的相交区域并不准确代表用户可见的区域,因为参与计算的区域是“布局区域”,布局区域可能会在绘制时被其他节点裁剪隐藏(如遇祖先节点中 overflow 样式为 hidden 的节点)或遮盖(如遇 fixed 定位的节点)。
在自定义组件或包含自定义组件的页面中,推荐使用 this.createIntersectionObserver 来代替 ft.createIntersectionObserver ,这样可以确保在正确的范围内选择节点。
# 4.8 响应显示区域变化
显示区域尺寸
显示区域指小程序界面中可以自由布局展示的区域。在默认情况下,小程序显示区域的尺寸自页面初始化起就不会发生变化。但以下两种方式都可以改变这一默认行为。
在手机上启用屏幕旋转支持
从小程序基础库版本 1.5.33 开始,小程序在手机上支持屏幕旋转。使小程序中的页面支持屏幕旋转的方法是:在 app.json 的 window 段中设置 "pageOrientation": "auto" ,或在页面 json 文件中配置 "pageOrientation": "auto" 。
以下是在单个页面 json 文件中启用屏幕旋转的示例。
代码示例
{
"pageOrientation": "auto"
}
如果页面添加了上述声明,则在屏幕旋转时,这个页面将随之旋转,显示区域尺寸也会随着屏幕旋转而变化。
从小程序基础库版本 1.5.33 开始, pageOrientation 还可以被设置为 landscape ,表示固定为横屏显示。
在 iPad 上启用屏幕旋转支持
从小程序基础库版本 1.5.33 开始,在 iPad 上运行的小程序可以支持屏幕旋转。使小程序支持 iPad 屏幕旋转的方法是:在 app.json 中添加 "resizable": true 。
代码示例
{
"resizable": true
}
如果小程序添加了上述声明,则在屏幕旋转时,小程序将随之旋转,显示区域尺寸也会随着屏幕旋转而变化。
注意
在 iPad 上不能单独配置某个页面是否支持屏幕旋转。
Media Query
有时,对于不同尺寸的显示区域,页面的布局会有所差异。此时可以使用 media query 来解决大多数问题。
代码示例
.my-class {
width: 40px;
}
@media (min-width: 480px) {
/* 仅在 480px 或更宽的屏幕上生效的样式规则 */
.my-class {
width: 200px;
}
}
屏幕旋转事件
有时,仅仅使用 media query 无法控制一些精细的布局变化。此时可以使用 js 作为辅助。
在 js 中读取页面的显示区域尺寸,可以使用 selectorQuery.selectViewport 。
页面尺寸发生改变的事件,可以使用页面的 onResize 来监听。对于自定义组件,可以使用 resize 生命周期来监听。回调函数中将返回显示区域的尺寸信息。(从基础库版本 1.5.33 开始支持。)
代码示例
Page({
onResize(res) {
res.size.windowWidth // 新的显示区域宽度
res.size.windowHeight // 新的显示区域高度
}
})
# 4.9 页面路由
在小程序中所有页面的路由全部由小程序框架进行管理。
页面栈
框架以栈的形式维护了当前所有页面。 当发生路由切换的时候,页面栈的表现如下:
路由方式 | 页面栈表现 |
---|---|
初始化 | 新页面入栈 |
打开新页面 | 新页面入栈 |
页面重定向 | 当前页面出栈,新页面入栈 |
页面返回 | 页面不断出栈,直到目标返回页 |
Tab | 切换 页面全部出栈,只留下新的 Tab 页面 |
重加载 | 页面全部出栈,只留下新的页面 |
getCurrentPages()
getCurrentPages() 函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。
注意
- 不要尝试手动修改页面栈,会导致路由以及页面状态错误。
- 不要在 App.onLaunch 的时候调用 getCurrentPages(),此时 page 还没有生成。
路由方式
对于路由的触发方式以及页面生命周期函数如下:
路由方式 | 触发时机 | 路由前页面 | 路由后页面 |
---|---|---|---|
初始化 | 小程序打开的第一个页面 | onLoad, onShow | |
打开新页面 | 调用 API ft.navigateTo 或使用组件 <navigator open-type="navigateTo"/> | onHide | onLoad, onShow |
页面重定向 | 调用 API ft.redirectTo 或使用组件 <navigator open-type="redirectTo"/> | onUnload | onLoad, onShow |
页面返回 | 调用 API ft.navigateBack 或使用组件 <navigator open-type="navigateBack"> 或用户按左上角返回按钮 | onUnload | onShow |
Tab | 切换 调用 API ft.switchTab 或使用组件<navigator open-type="switchTab"/> 或用户切换 Tab | 各种情况请参考下表 | |
重启动 | 调用 API ft.reLaunch 或使用组件 <navigator open-type="reLaunch"/> | onUnload | onLoad, onShow |
Tab 切换对应的生命周期(以 A、B 页面为 Tabbar 页面,C 是从 A 页面打开的页面,D 页面是从 C 页面打开的页面为例):
当前页面 | 路由后页面 | 触发的生命周期(按顺序) |
---|---|---|
A | A | Nothing happend |
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() |
提示
- navigateTo, redirectTo 只能打开非 tabBar 页面。
- switchTab 只能打开 tabBar 页面。
- reLaunch 可以打开任意页面。
- 页面底部的 tabBar 由页面决定,即只要是定义为 tabBar 的页面,底部都有 tabBar。
- 调用页面路由带的参数可以在目标页面的 onLoad 中获取。
# 4.10 动画
# 界面动画的常见方式
在小程序中,通常可以使用 CSS 渐变 和 CSS 动画 来创建简易的界面动画。
在开发者工具中预览效果
动画过程中,可以使用 bindtransitionend bindanimationstart bindanimationiteration bindanimationend 来监听动画事件。
事件名 | 含义 |
---|---|
transitionend | CSS 渐变结束或 wx.createAnimation 结束一个阶段 |
animationstart | CSS 动画开始 |
animationiteration | CSS 动画结束一个阶段 |
animationend | CSS 动画结束 |
注意:这几个事件都不是冒泡事件,需要绑定在真正发生了动画的节点上才会生效。
同时,还可以使用 wx.createAnimation 接口来动态创建简易的动画效果。(新版小程序基础库中推荐使用下述的关键帧动画接口代替。)
# 关键帧动画
还有一种更友好的动画创建方式,用于代替旧的 wx.createAnimation 。它具有更好的性能和更可控的接口。
在页面或自定义组件中,当需要进行关键帧动画时,可以使用 this.animate 接口:
this.animate(selector, keyframes, duration, callback)
参数说明
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
selector | String | 是 | 选择器(同 SelectorQuery.select 的选择器格式) | |
keyframes | Array | 是 | 关键帧信息 | |
duration | Number | 是 | 动画持续时长(毫秒为单位) | |
callback | function | 否 | 动画完成后的回调函数 |
keyframes 中对象的结构
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
offset | Number | 否 | 关键帧的偏移,范围[0-1] | |
ease | String | linear | 否 | 动画缓动函数 |
transformOrigin | String | 否 | 基点位置,即 CSS transform-origin | |
backgroundColor | String | 否 | 背景颜色,即 CSS background-color | |
bottom | Number/String | 否 | 底边位置,即 CSS bottom | |
height | Number/String | 否 | 高度,即 CSS height | |
left | Number/String | 否 | 左边位置,即 CSS left | |
width | Number/String | 否 | 宽度,即 CSS width | |
opacity | Number | 否 | 不透明度,即 CSS opacity | |
right | Number | 否 | 右边位置,即 CSS right | |
top | Number/String | 否 | 顶边位置,即 CSS top | |
matrix | Array | 否 | 变换矩阵,即 CSS transform matrix | |
matrix3d | Array | 否 | 三维变换矩阵,即 CSS transform matrix3d | |
rotate | Number | 否 | 旋转,即 CSS transform rotate | |
rotate3d | Array | 否 | 三维旋转,即 CSS transform rotate3d | |
rotateX | Number | 否 | X 方向旋转,即 CSS transform rotateX | |
rotateY | Number | 否 | Y 方向旋转,即 CSS transform rotateY | |
rotateZ | Number | 否 | Z 方向旋转,即 CSS transform rotateZ | |
scale | Array | 否 | 缩放,即 CSS transform scale | |
scale3d | Array | 否 | 三维缩放,即 CSS transform scale3d | |
scaleX | Number | 否 | X 方向缩放,即 CSS transform scaleX | |
scaleY | Number | 否 | Y 方向缩放,即 CSS transform scaleY | |
scaleZ | Number | 否 | Z 方向缩放,即 CSS transform scaleZ | |
skew | Array | 否 | 倾斜,即 CSS transform skew | |
skewX | Number | 否 | X 方向倾斜,即 CSS transform skewX | |
skewY | Number | 否 | Y 方向倾斜,即 CSS transform skewY | |
translate | Array | 否 | 位移,即 CSS transform translate | |
translate3d | Array | 否 | 三维位移,即 CSS transform translate3d | |
translateX | Number | 否 | X 方向位移,即 CSS transform translateX | |
translateY | Number | 否 | Y 方向位移,即 CSS transform translateY | |
translateZ | Number | 否 | Z 方向位移,即 CSS transform translateZ |
# 示例代码
this.animate('#container', [
{ opacity: 1.0, rotate: 0, backgroundColor: '#FF0000' },
{ opacity: 0.5, rotate: 45, backgroundColor: '#00FF00'},
{ opacity: 0.0, rotate: 90, backgroundColor: '#FF0000' },
], 5000, function () {
this.clearAnimation('#container', { opacity: true, rotate: true }, function () {
console.log("清除了#container上的opacity和rotate属性")
})
}.bind(this))
this.animate('.block', [
{ scale: [1, 1], rotate: 0, ease: 'ease-out' },
{ scale: [1.5, 1.5], rotate: 45, ease: 'ease-in', offset: 0.9},
{ scale: [2, 2], rotate: 90 },
], 5000, function () {
this.clearAnimation('.block', function () {
console.log("清除了.block上的所有动画属性")
})
}.bind(this))
调用 animate API 后会在节点上新增一些样式属性覆盖掉原有的对应样式。如果需要清除这些样式,可在该节点上的动画全部执行完毕后使用 this.clearAnimation 清除这些属性。
this.clearAnimation(selector, options, callback)
参数说明
属性 | 类型 | 默认值 | 必填 | 说明 |
---|---|---|---|---|
selector | String | 是 | 选择器(同 SelectorQuery.select 的选择器格式) | |
options | Object | 否 | 需要清除的属性,不填写则全部清除 | |
callback | Function | 否 | 清除完成后的回调函数 |
# 高级的动画方式
在一些复杂场景下,上述的动画方法可能并不适用。
WXS 响应事件 的方式可以通过使用 WXS 来响应事件的方法来动态调整节点的 style 属性。通过不断改变 style 属性的值可以做到动画效果。同时,这种方式也可以根据用户的触摸事件来动态地生成动画。
连续使用 setData 来改变界面的方法也可以达到动画的效果。这样可以任意地改变界面,但通常会产生较大的延迟或卡顿,甚至导致小程序僵死。此时可以通过将页面的 setData 改为 自定义组件 中的 setData 来提升性能。
# 4.11 初始渲染缓存
# 初始渲染缓存工作原理
小程序页面的初始化分为两个部分。
- 逻辑层初始化:载入必需的小程序代码、初始化页面 this 对象(也包括它涉及到的所有自定义组件的 this 对象)、将相关数据发送给视图层。
- 视图层初始化:载入必需的小程序代码,然后等待逻辑层初始化完毕并接收逻辑层发送的数据,最后渲染页面。 在启动页面时,尤其是小程序冷启动、进入第一个页面时,逻辑层初始化的时间较长。在页面初始化过程中,用户将看到小程序的标准载入画面(冷启动时)或可能看到轻微的白屏现象(页面跳转过程中)。
启用初始渲染缓存,可以使视图层不需要等待逻辑层初始化完毕,而直接提前将页面初始 data 的渲染结果展示给用户,这可以使得页面对用户可见的时间大大提前。它的工作原理如下:
在小程序页面第一次被打开后,将页面初始数据渲染结果记录下来,写入一个持久化的缓存区域(缓存可长时间保留,但可能因为小程序更新、基础库更新、储存空间回收等原因被清除);
在这个页面被第二次打开时,检查缓存中是否还存有这个页面上一次初始数据的渲染结果,如果有,就直接将渲染结果展示出来;
如果展示了缓存中的渲染结果,这个页面暂时还不能响应用户事件,等到逻辑层初始化完毕后才能响应用户事件。 利用初始渲染缓存,可以:
快速展示出页面中永远不会变的部分,如导航栏;
预先展示一个骨架页,提升用户体验;
展示自定义的加载提示;
# 支持的组件
在初始渲染缓存阶段中,复杂组件不能被展示或不能响应交互。
目前支持的内置组件:
<view />
<text />
<button />
<image />
<scroll-view />
<rich-text />
自定义组件本身可以被展示(但它们里面用到的内置组件也遵循上述限制)。
# 静态初始渲染缓存
若想启用初始渲染缓存,最简单的方法是在页面的 json 文件中添加配置项 "initialRenderingCache": "static" :
{
"initialRenderingCache": "static"
}
如果想要对所有页面启用,可以在 app.json 的 window 配置段中添加这个配置:
{
"window": {
"initialRenderingCache": "static"
}
}
添加这个配置项之后,在手机中预览小程序首页,然后杀死小程序再次进入,就会通过初始渲染缓存来渲染首页。
注意:这种情况下,初始渲染缓存记录的是页面 data 应用在页面 FXML 上的结果,不包含任何 setData 的结果。
例如,如果想要在页面中展示出“正在加载”几个字,这几个字受到 loading 数据字段控制:
<view wx:if="{{loading}}">正在加载</view>
这种情况下, loading 应当在 data 中指定为 true ,如:
// 正确的做法
Page({
data: {
loading: true
}
})
而不能通过 setData 将 loading 置为 true :
// 错误的做法!不要这么做!
Page({
data: {},
onLoad: function() {
this.setData({
loading: true
})
}
})
换而言之,这种做法只包含页面 data 的渲染结果,即页面的纯静态成分。
# 在初始渲染缓存中添加动态内容
有些场景中,只是页面 data 的渲染结果会比较局限。有时会想要额外展示一些可变的内容,如展示的广告图片 URL 等。
这种情况下可以使用“动态”初始渲染缓存的方式。首先,配置 "initialRenderingCache": "dynamic" :
{
"initialRenderingCache": "dynamic"
}
此时,初始渲染缓存不会被自动启用,还需要在页面中调用 this.setInitialRenderingCache(dynamicData) 才能启用。其中, dynamicData 是一组数据,与 data 一起参与页面 FXML 渲染。
Page({
data: {
loading: true
},
onReady: function() {
this.setInitialRenderingCache({
loadingHint: '正在加载' // 这一部分数据将被应用于界面上,相当于在初始 data 基础上额外进行一次 setData
})
}
})
<view wx:if="{{loading}}">{{loadingHint}}</view>
从原理上说,在动态生成初始渲染缓存的方式下,页面会在后台使用动态数据重新渲染一次,因而开销相对较大。因而要尽量避免频繁调用 this.setInitialRenderingCache ,如果在一个页面内多次调用,仅最后一次调用生效。
注意:
- this.setInitialRenderingCache 调用时机不能早于 Page 的 onReady 或 Component 的 ready 生命周期,否则可能对性能有负面影响。
- 如果想禁用初始渲染缓存,调用 this.setInitialRenderingCache(null) 。