文心雕刺

文心雕刺 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

文心雕刺 发布了文章 · 2月14日

小程序自定义下拉刷新组件(为应对头部有非滚动区的情况)

2021-03-21 update:
经过一个多月的测试实践,修复了这个组件初版的诸多不足,最重要的地方就是在ios上的自带的橡皮筋效果会影响到下拉效果,甚至某些老iphone会出现严重的抖动,还有滑动的时候如果出现类似点击的效果会导致滑动卡住的bug,这两个问题目前已经解决掉了,也是通过反复更改和测试的,由于更改内容较多,涉及css,wxml和js,暂时没有精力去更新,我正在考虑做成npm包。

2021-03-24:
现在已经将该组件发布到npm,包名为: hws-scroll

原文章:
效果动图:

image

小程序是有其下拉刷新api的,然而头部或者尾部有非滚动区域的情况下,是应对不了的,相关问题在微信开放社区已经是老生常谈了,比如:

onPullDownRefresh 下拉刷新在安卓手机上会带动头部fixed元素一起下拉的问题

又比如:

小程序下拉刷新onPullDownRefresh 问题,fixed定位下移!

像这种问题有很多,不一一列举了。

这个问题从2018年开始就不断有开发者向微信开放社区反馈,一直到现在,都没有解决,没办法,本公司项目中有头部是固定非滚动的列表页面,只得自己自定义一个。

列表页的上拉加载,参考了这篇文章中的方法,链接放在这里,大家可以移步去看一下:

浅谈微信小程序中的下拉刷新和上拉加载

回到主题,下拉刷新,我的思路是在这篇文章里找到的:

小程序开发踩坑记录(五)——模拟实现底部tabbar和下拉刷新功能(解决安卓端打开下拉刷新功能后fixed元素失效问题)

我一上来打算用该文章中的方法,来自制下拉刷新,可是我是在自定义组件中使用,不是在page中定义,在自定义组件中scroll-view的onscroll事件怎么也响应不了,也就无法获得当前页面的scrollTop。

后来我受这个启发,既然可以在tocuh事件中改变scroll-view的scrollTop,来模拟出一个下拉刷新区域的scrollTop随手指滑动而变化,进而实现其下拉显示和回弹,那我也可以使用绝对定位和相对定位,依靠改变容器的top值来实现这个功能,也就是可以将滚动容器的top初始值设置为负的刷新区域的高度,在tocuhstart和tocuhmove事件中获取手指在屏幕上的滑动距离,让容器的top和滑动距离呈正相关,当top值达到自己设置的阈值的时候停止变化,随后在tocuhend事件中发起刷新请求。

image

大体思路如上图所示

下面贴一下基本代码:


<view
  class="scroll"
  scroll-y
  style="min-height: {{pageHeight + 'px'}}; top: {{marginTop + 'rpx'}}"
  enable-back-to-top="{{true}}"
  bindtouchmove="movePull" 
  bindtouchstart="startPull" 
  bindtouchend="endPull" >
  <view class="scroll__content {{springbacking ? 'return__content' : ''}}" style="min-height: {{contentHeight + 'px'}};top: {{scrollTop + 'px'}}">
    <view class="refresh__wrap">
      <view class="refresh__center">
        <view class="refresh__icon {{refreshText === '松开刷新' ? 'rotate' : ''}}" >
          <mp-icon icon="sending" color="#999" size="{{35}}"></mp-icon>
        </view>
        <view class="tips">{{refreshText}}</view>
        <view class="times">最后更新:{{refreshTime}}</view>
      </view>
    </view>
    <slot />
    <view class="footer__line" wx:if="{{recycleList.length}}">
      <view class="scroll__loading" wx:if="{{bottomLoadingShow}}"></view>
      <text class="text {{bottomLoadingShow ? 'notline' : ''}}">{{lineText}}</text>
    </view>
  </view>
  <no-data 
    show="{{notDataShow}}"
    bind:refresh="refresh" />
</view>

重点说下,这个组件结构为外层.scroll元素,最小高度设置为pageHeight(计算方法后面会贴),position为relative,其top值设置为父级页面传入的marginTop值,就是父级页面头部非滚动元素的rpx高度,如果没有非滚动元素,则默认为0,这样的话,外层的位置就可以依赖marginTop值来确定了,滚动的时候不会把非滚动区域也算进去;
内层列表滚动区域.scroll__content元素,position为absolute最小高度contentHeight,top值scrollTop,初始-80px(刷新文案元素高度),这样就把.refresh__wrap刷新文案元素隐藏起来了。此外,设置一个默认插槽,就是列表循环内容。

pageHeight和contentHeight计算方法,用到wx.getSystemInfozhege api,比较简单:

getPageHeight() {
      const self = this;
      wx.getSystemInfo({
        success: (res) => {
          self.setData({
            pageHeight: res.windowHeight - (self.data.marginTop / 750 * res.windowWidth),
            contentHeight: (res.windowHeight - (self.data.marginTop / 750 * res.windowWidth)) + REFRESHHEIGHT + 2,
          })
        }
      })
    }

以上,marginTop是父级组件传进来的头部非滚动区域的rpx高度值(头部非滚动区域position设置为fixed),默认为0,REFRESHHEIGHT是80,就是刷新文案区域的高度px值,最后加2是保证在第一页铺不满整屏的情况下,能让onReachBottom上拉事件生效,上拉加载本文不多说了,可以直接去看上面贴出的链接:“浅谈微信小程序中的下拉刷新和上拉加载”(注意,我这里是自定义组件,onReachBottom事件同样要在父页面中写onReachBottom生命周期,来调用自定义组件中的onReachBottom方法)。

tocuhstart方法,就是获取手指开始接触屏幕的clentY值:

startPull(ev) {
  this.setData({ springbacking: false }); // 开始下拉的时候,去除.scroll的transition为0.4s的动画延迟效果,使其完全遵循手指的驱动
  this.lastTop = ev.changedTouches[0].clientY;
},

tocuhmove方法:

// MAX_MOVE_TOP 为 120 允许最大滑动距离
// MAX_SCROLL_TOP 为 20 允许.scroll__content的最大top值
movePull(ev) {
      this.nowY = ev.changedTouches[0].clientY;// 手指当前触摸位置的clentY值
      this.nowY = this.nowY - this.lastTop;// 滑动距离
      const query = wx.createSelectorQuery();
      query.select('.scroll').boundingClientRect();
      query.selectViewport().scrollOffset();
      query.exec((rect) => { // 必须是滚动高度为0即在顶部的时候触发
        if (rect[1].scrollTop <= 0 && this.nowY > 0 && this.nowY <= MAX_MOVE_TOP && this.data.recycleList.length) {
          this.setData({// 满足以上条件的,则使.scroll__content元素的top值等于-80px 加上滑动距离 nowY
            scrollTop: -REFRESHHEIGHT + this.nowY,
          })
          if(this.nowY >= 100) {
            this.setData({
              refreshText: '松开刷新',
            })
          }
        }
      })

      if(this.nowY > MAX_MOVE_TOP && this.data.scrollTop < MAX_SCROLL_TOP && this.data.recycleList.length) {
        this.setData({// 此处判断是为了解决手指滑动过快,tocuhmove得到的clentY值呈非线性变化,导致滑动距离可能上一次还是100以内,下一次直接就到300开外,无法满足上面的top变化条件,就卡住了。所以此时手动将.scroll__content的top值设置为20。
          refreshText: '松开刷新',
          scrollTop: MAX_SCROLL_TOP,
        })
      }
      ...
      // 上拉加载逻辑
      ...
    }

以上,下拉刷新都是在进入页面有数据的情况下才会触发即this.data.recycleList.length这个条件,无数据的情况下则显示no-data组件,如下图所示,直接点击刷新按钮刷新即可。
image

tocuhend事件:

endPull() {// 结束滑动的时候当.scroll__content的top值大于等于20,则可以执行刷新方法。
      if(this.data.scrollTop >= MAX_SCROLL_TOP) {
        wx.showNavigationBarLoading();
        this.refresh();
      }
      ...
      // 上拉加载逻辑
      ...
      if (this.data.scrollTop > -REFRESHHEIGHT) {
        setTimeout(() => { // 定时器是防止因为手指离开屏幕过快(类似点击事件,但又下拉了一段距离),导致的数据更新,视图未更新,而卡住的情况。
          this.setData({ // 这个springbacking为true的时候,.scroll__content元素的transition就是0.4s,回弹时候的动画效果。
            springbacking: true,
            scrollTop: -REFRESHHEIGHT
          })
        }, 50)
      }
    },

好了,下拉刷新的逻辑就写完了,该下拉刷新在安卓和ios上的效果差别不是很大,由于业务需要,我是把他做成了一个组件,一些刷新方法和父页面的请求事件成功交互,失败交互,不在本文探讨范围之内,所以就略去了,想试试的朋友可以直接在页面中将slot插槽替换成列表循环元素尝试。

查看原文

赞 15 收藏 15 评论 1

文心雕刺 发布了文章 · 2月6日

小程序省市区选择器,对接公司的地址数据

气,文章一发出来第二天就被51CTO博客某一个人给抄了去了,也不注明出处作者,抄的连文章里面的链接也没了,真的就是一键复制粘贴啊。

公司开始做小程序了,小程序的省市区三级联动picker组件mode="region",之前也有接触过,这一次一上来先尝试了一下,发现不能和之前公司的地址库结合,因为之前项目都是和后端通过地区编码来交互的,这个自带的无法满足现有的情况。于是用小程序picker组件的多列选择器自制了一个。
开始主题之前先说一下地址数据的问题,我司的数据也是我自己2020年8月从高德接口导出来的最新数据并同步给后端的:

接口地址:https://restapi.amap.com/v3/c...

高德行政区查询接口文档

通过这个接口可以获取全国的行政区数据,至于拿到数据以后组装成什么格式,这里不细说了,都是基本功,只要留意着点省直辖县,市直辖镇等特殊级别关系的情况。

当然小程序省市区选择器数据咱们也是有办法弄到,只是,现有在用的数据库不宜大动,于是没有用,传送门也放在这里吧。

小程序官方地区选择器数据

当时我有这么两个格式:一个是树形的,拿到就可以用:
树形json

然而一看大小,好家伙,440kb,太大了,用不了。

另一个是平铺格式的:

image

这个96kb,虽然还是有点大,压缩一下,勉强可以用了。只是这不是一个树形结构,所以前期还要准备几个方法备用。

地区编码是有其规则的,比如,省级行政区是两位,后四位是“0000”,地市一级的是四位,后两位是“00”,到区县一级则是完整的6位。
于是先把平铺的json,过滤出省市区三个级别的数组出来。

// region.js
// 这个data就是那个平铺格式的json
const data = require('./data.js');

const list = []
const province = []
const city = []
const area = []

Object.entries(data).forEach(val => {
  const key = Number.parseInt(val[0])
  const model = { key: val[0], value: val[1] }
  list.push(model)
  if (!(key % 1e4)) {
    province.push(model)
  } else if (!(key % 100)) {
    city.push(model)
  } else {
    const num = Number(val[0].substr(2))
    if (num > 9000) {
      city.push(model)
    } else {
      area.push(model)
    }
  }
})

module.exports = {
  srcList: list,
  srcProvince: province,
  srcCity: city,
  srcArea: area,
}

这样,省一级的行政区数组就拿到了,地市一级和区县一级的则是每次根据选择的上一级的地区编码,来过滤出其管辖的下一级行政区划的数组:

// createTree.js
const region  = require('./region');

module.exports = {
  /**
   * load city list by province data
   *
   * @param province: { key: 330000, value: '浙江省' }
   * @returns {Array}
   */
  loadCity (province) {
    if (province && Object.keys(province).length) {
      const list = region.srcCity.filter(val => {
        const num = Number.parseInt(province.key)
        return (val.key - num) < 1e4 && (val.key % num) < 1e4
      })
      // Municipalities directly under the central government
      return list.length ? list : [province]
    } else return []
  },

  /**
   * load area list by city data
   *
   * @param city: { key: 330100, value: '杭州市' }
   * @returns {Array}
   */
  loadArea (city) {
    if (city && Object.keys(city).length) {
      const cityKey = Number.parseInt(city.key)
      const isNotProvince = cityKey % 1e4
      const calcNum = isNotProvince ? 100 : 1e4
      const list = region.srcArea.filter(val => {
        return (val.key - cityKey) < calcNum && val.key % cityKey < calcNum
      })
      // Prefecture-level city
      return list.length ? list : [city]
    } else return []
  },
}

好了,准备工作完毕,进入小程序页面。

<picker 
  class="picker" 
  mode="multiSelector" 
  bindchange="bindMultiPickerChange" 
  bindcolumnchange="bindMultiPickerColumnChange" 
  bindcancel="cancel"
  value="{{regionIndex}}"
  range="{{regionArray}}"
  range-key="value">
  <mp-cell ext-class="utils__item">
    <text class="text">所在地区</text>
    <input bindinput="inputChange" 
    readonly 
    disabled 
    value="{{areaText}}" 
    class="weui-input" 
    placeholder="所在地区"/>
  </mp-cell>
</picker>

页面元素结构就是这样,没什么好说的,这个可以直接看小程序picker中多列选择器的文档。先看一下,页面用到的相关数据吧。

data: {
    areaText: '', // 显示的省市区文字
    blockArray: [], // 显示的省市区三级数组,二维数组
    blockIndex: [0, 0, 0], // 显示出来的选择下标,默认,[0,0,0]
    regionArray: [], // 选择器当前的省市区三级数组,二维数组
    regionIndex: [0, 0, 0], // 选择器当前选中的下标
    provinceList: srcProvince, // 省级数组,
    cityList: [], // 地市级数组,
    areaList: [], // 区县数组
}

可以看到,我这里有两个array和index的数组,这是因为,其中一个保存的是已被选择的数据,另一个则是当前正在选择的数据,主要考虑到一个取消功能。你要把之前选中的保存起来,如果点击了取消按钮,才能还原为上次选择的数据。

const  { srcProvince } = require('../../utils/region.js');
const { 
    loadCity, 
    loadArea, 
} =  require('../../utils/createTree.js');

onLoad() { // 初始化数据,这里还涉及到一个数据回显,比较麻烦,放到后面再说
  let cityList, areaList,
  cityList = loadCity(srcProvince[0]);
  areaList = loadArea(cityList[0]);
  this.setData({
    blockArray: [srcProvince, cityList, areaList],
    regionArray: [srcProvince, cityList, areaList],
    cityList,
    areaList,
  })
},

bindMultiPickerChange (e) { // 点击确定按钮的事件
    const oldKey = this.data.blockArray[2][this.data.blockIndex[2]].key; // 这是留住上次选中的第三级区域代码
    const newKey = this.data.regionArray[2][this.data.regionIndex[2]].key; // 本次选中的第三级区域代码
    
    if(oldKey !== newKey) { // 如果本次选择和之前的不一样,则修改数据
      this.setData({
        blockArray: this.data.regionArray,
        regionIndex: e.detail.value,
        blockIndex: e.detail.value,
        areaText: `${this.data.regionArray[0][this.data.regionIndex[0]].value}${this.data.regionArray[1][this.data.regionIndex[1]].value}${this.data.regionArray[2][this.data.regionIndex[2]].value}`
      })
    }
  },
  
bindMultiPickerColumnChange (e) { // 这是选择器列数据发生变化的时候,就是选择器滚动的时候
    const regionIndex = this.data.regionIndex;
    // 这个 column 就是发生变化的是第几列,0,1,2,value则是滚动下标
    regionIndex[e.detail.column] = e.detail.value;
    // 下面的处理是当选择上一级的时候,要把下一级下标设置为0,防止下一级下标越界
    if(e.detail.column === 0) {
      regionIndex[2] = 0;
      regionIndex[1] = 0;
    } else if(e.detail.column === 1) {
      regionIndex[2] = 0;
    }
    const provinceItem = srcProvince[regionIndex[0]];
    let cityList = [], areaList = [];
    cityList = loadCity(provinceItem);
    areaList = loadArea(cityList[regionIndex[1]]);

    this.setData({
      regionArray: [srcProvince, cityList, areaList],
      regionIndex: regionIndex,
      cityList,
      areaList,
    })
  },
  
  cancel() { // 点击取消的时候需要把数据还原回选择之前的状态
    this.setData({ 
      regionArray: this.cloneDeep(this.data.blockArray),
      regionIndex: this.cloneDeep(this.data.blockIndex), // 这里需要将数组深拷贝,否则,第二次进入取消下标会乱掉
    });
  },
  
  cloneDeep(initArr) {
    let arr = [];
    arr = JSON.parse(JSON.stringify(initArr));
    return arr;
  }

这个选择器的逻辑就是以上这些,主要是选择器列变化的时候要把当前列下一级的行政区划数组找出来,并且留意用户取消的情况。

最后再说说数据回显,现在是没有数据的时候,就是默认的[0,0,0]的情况,处理很简单,否则,就是有数据的情况,我们跟后端之间的交互都是通过最后一级的行政编码来的,就是我这边给后台就只有一个areaCode,后端返回也只有一个areaCode。因为这个在无论我们当前项目还是在这个小程序的后台项目上,逻辑上都不会存在只有省一级或者只有省市两级的情况,所以。只要有数据,这个areaCode必然是区县一级的代码。于是在createTree.js里增加一个方法:

/*
  * areaCodeToMap
  * @param {string} code
  * @param {array} list
  */

  areaCodeToMap(code, list) {
    const result = {
      map: {},
      index: 0,
    };
    list.map((item, i) => {
      if(item.key === code) {
        result.map = item;
        result.index = i;
      }
    })
    return result;
  },

这是通过这个areaCode把上面两级的行政区对象及其下标找出来,只要有了上面两级的行政区对象,则又可以通过之前的loadCity和loadArea两个方法设置选择器数据了:

setArea() { // 省市区数据回显
    let index = 0;
    let cityList = [], areaList = [], provinceIndex = 0, cityIndex = 0, areaIndex = 0;
    const areaCode = this.data.form.areaCode;
    if(areaCode) {
      // 省级信息回显
      const province = areaCodeToMap(`${areaCode.substring(0, 2)}0000`, srcProvince);
      const provinceItem = province.map;
      provinceIndex = province.index;

      // 地市级信息回显
      cityList = loadCity(provinceItem);
      const city = areaCodeToMap(`${areaCode.substring(0, 4)}00`, cityList);
      const cityItem = city.map;
      cityIndex = city.index;

      // 区县级信息回显
      areaList = loadArea(cityItem);
      const area = areaCodeToMap(areaCode, areaList);
      const areaItem = area.map;
      areaIndex = area.index;
      
      this.setData({
        blockArray: [srcProvince, cityList, areaList],
        regionArray: [srcProvince, cityList, areaList],
        blockIndex: [provinceIndex, cityIndex, areaIndex],
        regionIndex: [provinceIndex, cityIndex, areaIndex],
        cityList,
        areaList,
      })
      this.setData({
        areaText: `${this.data.regionArray[0][this.data.regionIndex[0]].value}${this.data.regionArray[1][this.data.regionIndex[1]].value}${this.data.regionArray[2][this.data.regionIndex[2]].value}`
      })
}

上真机效果图:

image

其实这里还涉及到省市区选择器和地图选址组件的交互,比如选择完地区以后,将经纬度设置成该地区的经纬度,进入地图后定位到该地区,还有在地图定位完位置后根据地图返回的areaCode再调用一遍setArea方法直接回显省市区等等,这些不是本文探讨范围,不再细说了。

文章写完了,喜欢的给个赞吧!

查看原文

赞 8 收藏 6 评论 1

文心雕刺 关注了专栏 · 2020-12-25

前端开发那些事儿

前端知识:HTML、CSS、JS、React,nodejs、Chrome、数据结构与算法,计算机网络等精华知识分享交流。

关注 6688

文心雕刺 关注了专栏 · 2020-12-25

K8S生态

Container, Docker, Go, Kubernetes, Python, Vim; 微信公众号: MoeLove

关注 11666

文心雕刺 关注了专栏 · 2020-12-25

SegmentFault 之声

在这里,我们将为你推送 SegmentFault 思否公司官方合作信息,和合作伙伴最新动态。SegmentFault 思否是中国领先的开发者社区和技术媒体,中国最大的 Hackathon 组织者。我们致力于成为科技企业和开发者沟通的桥梁,帮助科技企业和开发者对话。

关注 19046

文心雕刺 关注了用户 · 2020-12-25

谭光志 @woai3c

公众号:前端编程技术分享

知乎:https://www.zhihu.com/people/...

github: https://github.com/woai3c

关注 10300

文心雕刺 关注了用户 · 2020-12-25

LNMPRG源码研究 @php7internal

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人
希望交流的朋友请加微信 289007301 注明:思否 拉到交流群,也可关注公众号:LNMPRG源码研究

《PHP7底层设计与源码分析》勘误https://segmentfault.com/a/11...

《Redis5命令设计与源码分析》https://item.jd.com/12566383....

景罗 陈雷 李乐 黄桃 施洪宝 季伟滨 闫昌 李志 王坤 肖涛 谭淼 张仕华 方波 周生政 熊浩含 张晶晶(女) 李长林 朱栋 张晶晶(男) 陈朝飞 巨振声 杨晓伟 闫小坤 韩鹏 夏达 周睿 李仲伟 张根红 景罗 欧阳 孙伟 李德 twosee

关注 11690

文心雕刺 关注了用户 · 2020-12-25

王治治 @wangdazhi_sifou

学者所志至大,犹恐所得浅。

关注 5186

文心雕刺 关注了用户 · 2020-12-25

刘征Martin @liumartin

Elastic公司社区布道师,中国DevOps社区组织者,精通DevOps/SRE/ITIL等理论体系。致力于在全国范围内通过社区推广DevOps的理念、技术和实践。热衷于传播开源技术栈在各种场景的应用,包括运维大数据分析、云原生服务治理、APM全链路监控和AIOps等。 微信号:MyDevOps

关注 2904

文心雕刺 关注了专栏 · 2020-12-25

concent

concent,一个内置依赖收集系统,并兼具0入侵、可预测、渐进式、高性能特点的react状态管理方案。

关注 9297

认证与成就

  • 获得 48 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-10-10
个人主页被 2.5k 人浏览