微信小程序组件间关系relations的应用:实现一个吸顶组件(附源码)
需求分析:正常情况下,我们在微信小程序里要写一个吸顶效果的话,思路很简单,就是先通过小程序页面的onPageScroll方法,获取页面在垂直方向已滚动的距离(单位px),然后判断页面垂直滚动距离大于吸顶的容器距离页面顶部的高度,再去改变position的值为sticky就可以了。当然,这是最简单的情况,真实的业务场景可能,需要不是固定在顶部,而是距离顶部有个距离,或者一个页面有多个需要吸顶的容器,或者,需要吸顶的容器是动态的,可以增加,也可以减少,那么这时候,我们每个吸顶容器都要单独写一段代码去控制它的话,是不是就特别麻烦,而且页面看起来也会非常的乱,这时候,把吸顶操作封装成一个组件就会非常优雅了。
那么需求确定了,我么你要怎样封装这个组件呢?这里我们需要注意几个小点:
-
1、因为组件内部无法获取页面垂直滑动距离,所以我们需要在
sticky(我们后面要封装的吸顶组件的名字)
吸顶组件上通过属性scroll-top
,在使用这个组件的页面传入scroll-top(页面在垂直方向已滚动的距离)
的值。 -
2、有时候我们不一定是
吸顶
,可能是是距离顶部10px的距离吸顶
,所以我们还需要外部传给吸顶组件
一个top值,表示组件在距离顶部多少距离(默认距离当然是0px)吸顶
-
3、如果我们这个组件的内部存在多个吸顶容器的时候,第一个容器的吸顶距离是top,第二个吸顶容器的吸顶距离就是自身的top+第一个容器的高度,第三个。。。第四个。。。。,而且这个容器的个数可能是动态增加(减少)的,所以在吸顶组件里,需要监听到容器个数的变化事件,然后再动态几个每个吸顶组件距离顶部的高度,去完成吸顶。这时候子组件
sticky-item
和父组件sticky
之间的通信是相对比较复杂的,这时候,我们就会用到小程序的组件间关系relations
这里我结合子组件sticky-item
和父组件sticky
用图片的方式说一下小程序的组件间关系relations
,
- 这里大家要注意的是什么是
当父组件与子组件的关系被建立
的时候,就是每次子组件sticky-item
插入(移除)父组件sticky
的时候,就是父组件与子组件的关系改变
的时候,就会触发下面对应的方法
选项 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | String | 是 | 目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant |
linked | Function | 否 | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
linkChanged | Function | 否 | 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 |
unlinked | Function | 否 | 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 |
target | String | 否 | 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 |
具体大家还可以看下小程序的官方文档:组件间关系relations
最终我们希望实现完的组件在页面的使用效果是这样的(这样用起来就十分优雅了呀):
<sticky scrollTop="{{scrollTop}}">
<sticky-item top="10">
<view slot="header">吸顶菜单</view>
<view slot="body">内容部分</view>
</sticky-item>
<sticky-item top="20">
<view slot="header">吸顶菜单</view>
<view slot="body">内容部分</view>
</sticky-item>
</sticky>
需求分析完了,下面直接看代码吧(注释都在代码里),讨论的话评论区直接滴滴吧
sticky
Component({
relations: {
'../sticky-item/index': {
type: 'child',
linked() {
this.checkSupportCssSticky().then((isSupportCssSticky) => {
if (!isSupportCssSticky) {
this.updateStickyItemsSizeData();
}
}).catch(err => {
console.error(err);
});
},
linkChanged() {
this.checkSupportCssSticky().then((isSupportCssSticky) => {
if (!isSupportCssSticky) {
this.updateStickyItemsSizeData();
}
}).catch(err => {
console.error(err);
});
},
unlinked() {
this.checkSupportCssSticky().then((isSupportCssSticky) => {
if (!isSupportCssSticky) {
this.updateStickyItemsSizeData();
}
}).catch(err => {
console.error(err);
});
}
}
},
properties: {
/**
* 吸顶容器实现模式
* js - 使用js实现
* css - 使用css实现,若不支持css,则回滚到js模式
*/
mode: {
type: String,
value: 'css',
options: ['js', 'css']
},
/**
* 页面垂直滚动的距离
*/
scrollTop: Number,
},
observers: {
/**
* 监听页面滚动,实时更新吸顶容器位置
*/
'scrollTop': function () {
this.checkSupportCssSticky().then((isSupportCssSticky) => {
if (!isSupportCssSticky) {
this.updateStickyItemsPosition();
}
}).catch(err => {
console.error(err);
});
}
},
methods: {
/**
* 更新所有sticky-item组件的position属性
*/
updateStickyItemsPosition() {
const stickyItemNodes = this.getStickyItemNodes();
for (let stickyItemNode of stickyItemNodes) {
stickyItemNode.updateStickyItemPosition(this.data.scrollTop);
}
},
/**
* 更新所有sticky-item组件的基础数据
*/
updateStickyItemsSizeData() {
const stickyItemNodes = this.getStickyItemNodes();
stickyItemNodes.forEach((item, index) => {
item.updateStickyItemBaseData(index);
});
},
/**
* 获取所有的sticky-item组件
* @return {Object} sticky-item组件集合
*/
getStickyItemNodes() {
return this.getRelationNodes('../sticky-item/index');
},
/**
* 检测当前webview内核是否支持css设置sticky
* @return {Boolean} css是否支持设置sticky
*/
checkSupportCssSticky() {
return new Promise((resolve) => {
const stickyItemNodes = this.getStickyItemNodes();
if (stickyItemNodes.length === 0) {
resolve(false);
}
// 根据position判断是否支持position:sticky
wx
.createSelectorQuery()
.in(stickyItemNodes[0])
.select('.sticky-item-header')
.fields({
computedStyle: ['position']
})
.exec((res) => {
if (res[0] === null) {
resolve(false);
} else {
resolve(res[0].position === 'sticky');
}
});
});
},
}
});
sticky-item
import nodeUtil from '../utils/node-util';
Component({
options: {
multipleSlots: true
},
relations: {
'../sticky/index': {
type: 'parent'
}
},
properties: {
/**
* 吸顶容器吸顶后距离视窗 顶部的距离
*/
top: {
type: Number,
value: 0
}
},
data: {
/**
* 显示模式
*/
mode: undefined,
/**
* 当前sticky-item的索引值
*/
index: undefined,
/**
* sticky-item是否固定到页面顶部
*/
isFixedTop: false,
/**
* sticky-item组件距离页面顶部的高度
*/
stickyItemTop: 0,
/**
* sticky-item组件自身的高度
*/
stickyItemHeight: 0,
/**
* sticky-item组件包装高度
*/
stickyItemWrapperHeight: undefined
},
lifetimes: {
ready: function () {
// 设置显示模式
const parent = this.getParentComponent();
const mode = parent.data.mode;
this.setData({
mode
});
}
},
methods: {
/**
* 更新sticky-item组件的position属性
* 判断sticky-item组件是否固定到顶部
* @param {Number} scrollTop 页面垂直滚动距离
*/
updateStickyItemPosition(scrollTop) {
const parent = this.getParentComponent();
const {
index,
stickyItemTop,
stickyItemHeight,
top
} = this.data;
const isFixedTop = scrollTop > stickyItemTop - top && scrollTop < stickyItemHeight + stickyItemTop - top;
// 避免频繁setData
if (this.data.isFixedTop === isFixedTop) {
return;
}
if (isFixedTop) {
// 触发吸附事件
parent.triggerEvent('onSticky', {
index
});
} else {
// 触发脱落事件
parent.triggerEvent('onUnsticky', {
index
});
}
this.setData({
isFixedTop
});
},
/**
* 更新sticky-item组件的基础数据
* @param {Number} index 当前sticky-item的索引值
*/
async updateStickyItemBaseData(index) {
// 设置索引值
this.setData({
index
});
// 从父级组件获取页面垂直滚动距离
const parent = this.getParentComponent();
const scrollTop = parent.data.scrollTop;
/**
* 设置sticky-item组件距页面顶部的距离
* 和sticky-item组件的高度
*/
const stickyItemNodeRect = await nodeUtil.getNodeRectFromComponent(this, '.sticky-item');
this.setData({
stickyItemTop: stickyItemNodeRect.top + scrollTop,
stickyItemHeight: stickyItemNodeRect.height
});
// 设置sticky-item-header外层容器高度
const stickyItemHeaderNodeRect = await nodeUtil.getNodeRectFromComponent(this, '.sticky-item-header');
this.setData({
stickyItemWrapperHeight: stickyItemHeaderNodeRect.height
});
},
/**
* 获取父级组件-sticky实例
*/
getParentComponent() {
const sticks = this.getRelationNodes('../sticky/index');
if (sticks.length === 0) {
return;
}
return sticks[0];
}
}
});
- 页面使用
<w-sticky scrollTop="{{scrollTop}}"
bindonSticky="onSticky"
bindonUnsticky="onUnsticky"
data-id="1">
<text style="padding: 32rpx;display: block;">
在某些场景下sticky-item的内容会增加/减少,如:上拉加载更多,
此时需要主动调用组件的updateStickyItemsSizeData()方法刷新吸顶容器的宽高数据,
否则会造成吸顶位置错乱。
示例代码
fetchData(){
// 从服务端获取数据
let data = http.fetchData()
// 设置数据
this.setData({
listData:data
})
// 刷新sticky容器
this.selectComponent('#wSticky')
.updateStickyItemsSizeData()
}
</text>
<w-sticky-item>
<view slot="header"
style="height:80rpx;background: sandybrown;text-align:center;line-height: 80rpx;">吸顶1</view>
<view slot="body">
<text style="padding: 32rpx;display: block;">
在某些场景下sticky-item的内容会增加/减少,如:上拉加载更多,
此时需要主动调用组件的updateStickyItemsSizeData()方法刷新吸顶容器的宽高数据,
否则会造成吸顶位置错乱。
示例代码
fetchData(){
// 从服务端获取数据
let data = http.fetchData()
// 设置数据
this.setData({
listData:data
})
// 刷新sticky容器
this.selectComponent('#wSticky')
.updateStickyItemsSizeData()
}
</text>
<w-sticky scrollTop="{{scrollTop}}"
bindonSticky="onSticky"
bindonUnsticky="onUnsticky"
data-id="2">
<w-sticky-item top="80">
<view slot="header"
style="background:springgreen;text-align:center;">
<w-input data-field="name"
leftText="姓名"
modelValue="张三"
disabled="{{true}}" />
<w-input data-field="name"
leftText="姓名"
modelValue="李四"
disabled="{{true}}" />
</view>
<view style="height: 500vh;"
slot="body">
<text style="padding: 32rpx;display: block;">
在某些场景下sticky-item的内容会增加/减少,如:上拉加载更多,
此时需要主动调用组件的updateStickyItemsSizeData()方法刷新吸顶容器的宽高数据,
否则会造成吸顶位置错乱。
示例代码
fetchData(){
// 从服务端获取数据
let data = http.fetchData()
// 设置数据
this.setData({
listData:data
})
// 刷新sticky容器
this.selectComponent('#wSticky')
.updateStickyItemsSizeData()
}
</text>
</view>
</w-sticky-item>
</w-sticky>
</view>
</w-sticky-item>
</w-sticky>
// package_layout/pages/sticky/index.js
Page({
/**
* 页面的初始数据
*/
data: {
// 页面垂直滑动的距离
scrollTop: undefined
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
// 触发吸附事件(js模式下,才会执行 )
onSticky(e) {
console.log(e)
},
// 触发脱落事件(js模式下,才会执行 )
onUnsticky(e) {
console.log(e)
},
//因为组件内部无法获取页面垂直滑动距离,所以需要在sticky组件上通过属性scroll-top
onPageScroll(res) {
console.log(res.scrollTop)
this.setData({
scrollTop: res.scrollTop
})
}
})