likes
comments
collection
share

微信小程序组件间关系relations的应用:实现一个吸顶组件(附源码)

作者站长头像
站长
· 阅读数 15

微信小程序组件间关系relations的应用:实现一个吸顶组件(附源码)

需求分析:正常情况下,我们在微信小程序里要写一个吸顶效果的话,思路很简单,就是先通过小程序页面的onPageScroll方法,获取页面在垂直方向已滚动的距离(单位px),然后判断页面垂直滚动距离大于吸顶的容器距离页面顶部的高度,再去改变position的值为sticky就可以了。当然,这是最简单的情况,真实的业务场景可能,需要不是固定在顶部,而是距离顶部有个距离,或者一个页面有多个需要吸顶的容器,或者,需要吸顶的容器是动态的,可以增加,也可以减少,那么这时候,我们每个吸顶容器都要单独写一段代码去控制它的话,是不是就特别麻烦,而且页面看起来也会非常的乱,这时候,把吸顶操作封装成一个组件就会非常优雅了。

那么需求确定了,我么你要怎样封装这个组件呢?这里我们需要注意几个小点:

  • 1、因为组件内部无法获取页面垂直滑动距离,所以我们需要在sticky(我们后面要封装的吸顶组件的名字)吸顶组件上通过属性scroll-top,在使用这个组件的页面传入 scroll-top(页面在垂直方向已滚动的距离)的值。

  • 2、有时候我们不一定是吸顶,可能是是距离顶部10px的距离吸顶,所以我们还需要外部传给吸顶组件一个top值,表示组件在距离顶部多少距离(默认距离当然是0px)吸顶

  • 3、如果我们这个组件的内部存在多个吸顶容器的时候,第一个容器的吸顶距离是top,第二个吸顶容器的吸顶距离就是自身的top+第一个容器的高度,第三个。。。第四个。。。。,而且这个容器的个数可能是动态增加(减少)的,所以在吸顶组件里,需要监听到容器个数的变化事件,然后再动态几个每个吸顶组件距离顶部的高度,去完成吸顶。这时候子组件sticky-item和父组件sticky之间的通信是相对比较复杂的,这时候,我们就会用到小程序的组件间关系relations

微信小程序组件间关系relations的应用:实现一个吸顶组件(附源码)

这里我结合子组件sticky-item和父组件sticky用图片的方式说一下小程序的组件间关系relations

微信小程序组件间关系relations的应用:实现一个吸顶组件(附源码)

微信小程序组件间关系relations的应用:实现一个吸顶组件(附源码)

  • 这里大家要注意的是什么是 当父组件与子组件的关系被建立 的时候,就是每次子组件sticky-item插入(移除)父组件sticky的时候,就是 父组件与子组件的关系改变 的时候,就会触发下面对应的方法
选项类型是否必填描述
typeString目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant
linkedFunction关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后
linkChangedFunction关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后
unlinkedFunction关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后
targetString如果这一项被设置,则它表示关联的目标节点所应具有的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
    })
  }

})