乐趣区

关于vue.js:移动端日历组件设计与实现

前言

在大多数的客户端利用中,日期的抉择与操作是一个常见的性能,应用日历组件实现对于这一性能的实现,往往是一个高效的解决方案。对于日历组件的设计与开发,在常见的开源我的项目中,通常有两种设计思路:

  • 横向切换展现,默认渲染单个月份,通过按钮或左右滑动,进行月份切换;
  • 纵向切换展现,默认渲染展现多个月份,高低滑动进行月份切换;

例如增加 picker 进行视图切换,增加自定义按钮,日期单选 / 多选,自定义文案,日期范畴限度等等性能,这些根本都是在两种思路的根底上进行的性能扩大。

在日常的利用中,两种形式各有优劣:

  • 横向切换,初始渲染的节点更少,渲染性能更加优异;
  • 纵向切换,有更加直观的视觉感触,更良好的交互操作;

然而,鱼和熊掌不可兼得,交互体验与性能上的取舍,是一个始终都要直面的问题。随着挪动端设施的一直倒退,挪动端浏览器一直的欠缺,用户设施在兼容性与运行效率上都有显著晋升,因而,本文次要论述的,是以竖向切换形式实现的 NutUI Calendar 日历组件。

主题介绍

明天的主题是 NutUI Calendar 组件的设计与实现,Calendar 组件是 NutUI 的一个日历组件,它用认为用户提供一个直观的日期抉择形式,以滑动的形式切换月份,反对单个日期与日期范畴的抉择,反对自定义日期内容等性能。明天,让咱们一起来看看,在组件的开发过程中,是如何一步步实现组件性能的。

组件设计思路

日历组件,不论以何种形式设计交互,日期工夫数据的解决都是必不可少的,毕竟视图也是为数据信息服务的。在本文中采取的竖向切换展现的形式,也意味着咱们要在节点的渲染性能上做一些优化调整。所以咱们的实现思路次要有以下几点:

  1. 日期数据处理,一次性初始化原始数据,在可视区域内,分段渲染节点元素。
  2. 利用虚构列表的形式,缩小节点元素的渲染开销
  3. 滚动事件与边界条件的解决
  4. 功能完善,丰盛 Slots,Props,Events 事件等, 晋升扩展性

组件的实现原理

基本参数需要

在解决日期数据时,咱们须要先明确咱们所需的根本工夫入参,例如:日历组件的可选工夫范畴,以后选中的工夫。
通过对传入参数的解析解决,失去咱们所需的数据内容,在之后的开发过程中,实现组件内容的渲染与事件处理。

这里我画了一张图不便大家更好了解:

  • 原始日期数据:是咱们依据日期范畴计算的原始数据
  • 以后选中日期:可视范畴的展现以后月份,须要判断选中日期是否在日期范畴内
  • 展现范畴区间:依据以后选中日期解决得出,为以后须要渲染的数据范畴
  • 容器尺寸信息:用以计算日期滚动切换时的位移信息

日期数据处理

日期数据的计算,须要有多个处理过程。首先,咱们须要先计算传入的日期范畴是否存在,如果不存在,默认应用最近一年的工夫范畴。之后计算存在多少个月。在依据月的数量去遍历生成日期数据。

在计算单个月日期时,每个月的 第一天 最初一天 的星期数是不同的,咱们须要依据不同的 星期数,以前一个月与后一个月的日期进行补全。这样既能够省去计算 1 号开始地位偏移量,也能够为性能扩大做出铺垫。

// 获取单个月的日期与状态
const getDaysStatus = (currMonthDays: number,  dateInfo: any) => {let { year, month} = dateInfo;
  return Array.from(Array(currMonthDays), (v, k) => {
    return {
      day: k + 1,
      type: "curr",
      year,
      month,
    };
  });
  // 获取上一个月的最初一周天数,填充当月空白
  const getPreDaysStatus = (
    preCurrMonthDays: number
    weekNum: number,
    dateInfo: any,
  ) => {let { year, month} = dateInfo;
    if (weekNum >= 7) {weekNum -= 7;}
    let months = Array.from(Array(preCurrMonthDays), (v, k) => {
      return {
        day: k + 1,
        type: "prev",
        year,
        month,
      };
    });
    return months.slice(preCurrMonthDays - weekNum);
  };
};

解决后的数据如下:

虚构列表

当咱们生成或加载的数据量十分大时,可能会产生重大的性能问题,导致视图无奈响应操作一段时间。在小程序中视图的渲染问题更为显著,为了解决这个问题,虚构列表是一种不错的解决方案:比起全量渲染数据生成的视图,能够只渲染以后可视区域 (visible viewport) 的视图,非可视区域的视图在用户滚动到可视区域再渲染。
例如,Taro 中的长列表渲染(虚构列表):

当然以上只是一个简略的利用,日历组件的构建须要在这个的根底上进行肯定的优化。如下图,months wrapper 为须要展现月份的容器。这样设置,是因为在咱们的视口范畴内,会存在不止一个月份。同时因为单个月份蕴含的节点较多,当通过 视口边界 后在进行渲染,可能会存在留白景象,所以咱们能够预留局部月份内容,在不可视区域进行节点变更与渲染。

如上图所示,

  • scrollWarpper:是一个高度为总月份高度的容器,次要用来作为 viewport 中的滚动容器;
  • monthsWrapper:内为以后渲染出的月份的容器;
  • viewport:为以后视口范畴;

当滚动事件触发后,scrollWrapper 进行向下或向上挪动。达到边界后,monthsWrapper 内的月份信息扭转,其总体高度也可能发生变化。通过对 monthsWrapper 的 transition 进行批改,保障在月份变更后,视口中内容不变,视口外数据更新。

在利用虚构列表的同时,联合以后的支流框架,将数据退出框架的响应式数据中,框架应用 diff 算法或其它机制依据数据的不同,能够对 DOM 节点进行肯定水平上的复用,缩小 DOM 节点元素的新增与删除操作。毕竟频繁的进行 DOM 增删操作是一件较为耗费性能的事件。

<!-- 视口 -->
<view class="nut-calendar-content" ref="months" @scroll="mothsViewScroll">
  <!-- 整体容器 - 设置一个总体高度用以撑起视口 -->
  <view class="calendar-months-panel" ref="monthsPanel">
    <!-- 月份容器 -->
    <view
      class="viewArea"
      ref="viewArea"
      :style="{transform: `translateY(${translateY}px)` }"
    >
      <view
        class="calendar-month"
        v-for="(month, index) of compConthsData"
        :key="index"
      >
        <view class="calendar-month-title">{{month.title}}</view>
        <view class="calendar-month-con">
          <view
            class="calendar-month-item"
            :class="type ==='range'?'month-item-range':''"
          >
            <template v-for="(day, i) of month.monthData" :key="i">
              <view
                class="calendar-month-day"
                :class="getClass(day, month)"
                @click="chooseDay(day, month)"
              >
                <!-- 日期显示 slot -->
                <view class="calendar-day">
                  <slot name="day" :date="day.type =='curr'? day :''">
                    {{day.type == 'curr' ? day.day : ''}}
                  </slot>
                </view>
                <view
                  class="calendar-curr-tip-curr"
                  v-if="!bottomInfo && showToday && isCurrDay(day)"
                >
                  明天
                </view>
                <view
                  class="calendar-day-tip"
                  :class="{'calendar-curr-tips-top': rangeTip(day, month) }"
                  v-if="isStartTip(day, month)"
                >
                  {{startText}}
                </view>
                <view class="calendar-day-tip" v-if="isEndTip(day, month)"
                  >{{endText}}</view
                >
              </view>
            </template>
          </view>
        </view>
      </view>
    </view>
  </view>
</view>

事件处理与边界状态

事件抉择

在 Calendar 组件中,月份的切换变更是通过对滚动事件监听实现的。
思考应用滚动事件,是因为思考到对于 Taro 转换为微信小程序的兼容解决。touchmove 事件同样能够实现加载切换交互,然而 touch 事件要实现滚动成果,须要频繁的触发事件批改元素地位, 在小程序中就体现为频繁的 setData,而这会导致较大的性能开销,使得页面卡顿。

边界条件

确定好事件后,边界条件的判断,就是咱们须要思考的一个问题:每个月所占高度,不肯定雷同。每个月蕴含有几个星期,不肯定雷同。导致每个月所占据的高度也不肯定雷同。所以要精确到判断以后滚动的地位信息,就须要找到一个相同点来进行判断。

这里咱们以单个日期的高度作为基准值,通过单个日期的高度计算月份的高度,在得出均匀单个月份的高度。滚动地位除以均匀高度获得近似 current。
如下图所示:

在计算高度过程中,因为小程序的单位为 rpx,h5 为 rem,所以须要对 px 进行转换计算。

let titleHeight, itemHeight;
// 计算单个日期高度
// 对小程序与 H5,rpx 与 rem 转换 px 解决
if (TARO_ENV === "h5") {
  titleHeight = 46 * scalePx.value + 16 * scalePx.value * 2;
  itemHeight = 128 * scalePx.value;
} else {
  titleHeight =
    Math.floor(46 * scalePx.value) + Math.floor(16 * scalePx.value) * 2;
  itemHeight = Math.floor(128 * scalePx.value);
}
monthInfo.cssHeight =
  titleHeight +
  (monthInfo.monthData.length > 35 ? itemHeight * 6 : itemHeight * 5);
let cssScrollHeight = 0;
// 保留月份地位信息
if (state.monthsData.length > 0) {
  cssScrollHeight =
    state.monthsData[state.monthsData.length - 1].cssScrollHeight +
    state.monthsData[state.monthsData.length - 1].cssHeight;
}
monthInfo.cssScrollHeight = cssScrollHeight;

当咱们失去以后的均匀 current,就能够进行边界条件的判断。

const mothsViewScroll = (e: any) => {
  const currentScrollTop = e.target.scrollTop;
  // 获取均匀 current
  let current = Math.floor(currentScrollTop / state.avgHeight);
  if (current == 0) {if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight) {current += 1;}
  } else if (current > 0 && current < state.monthsNum - 1) {if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight) {current += 1;}
    if (currentScrollTop < state.monthsData[current].cssScrollHeight) {current -= 1;}
  } else {
    // 获取视口高度 判断是否曾经到最初一个月
    const viewPosition = Math.round(currentScrollTop + viewHeight.value);
    if (
      viewPosition <
        state.monthsData[current].cssScrollHeight +
          state.monthsData[current].cssHeight &&
      currentScrollTop < state.monthsData[current].cssScrollHeight
    ) {current -= 1;}
    if (
      current + 1 <= state.monthsNum &&
      viewPosition >=
        state.monthsData[current + 1].cssScrollHeight +
          state.monthsData[current + 1].cssHeight
    ) {current += 1;}
    if (currentScrollTop < state.monthsData[current - 1].cssScrollHeight) {current -= 1;}
  }
  if (state.currentIndex !== current) {
    state.currentIndex = current;
    setDefaultRange(state.monthsNum, current);
  }
  // 设置月份题目信息
  state.yearMonthTitle = state.monthsData[current].title;
};

让咱们来看一看成果吧:

功能完善

通过以上过程,咱们曾经实现了一个根本的滚动日历组件。在这个根底上,咱们须要进行一些欠缺,以扩大组件的通用性。

  1. 为日期信息增加 slots,容许日期信息自定义展现
  2. 题目处提供 slots。不便用户插入自定义操作
  3. 题目,按钮,日期范畴文案等信息提供 props 设置
  4. 增加回调办法,如抉择日期,点击日期,敞开日历等操作
// 未传入的 slot 不进行加载,缩小无意义的 dom
<view
  class="calendar-curr-tips calendar-curr-tips-top"
  v-if="topInfo"
>
  <slot name="topInfo" :date="day.type =='curr'? day :''"></slot>
</view>

结语

本文介绍了 NutUI 中 Calendar 组件的设计思路与实现原理,心愿能够为大家提供一些灵感与思路。最初再提一下咱们的 NutUI 组件库,长期以来,团队的小伙伴都在尽心尽力地保护着 NutUI。在之后的日子里,这种保持也不会放弃,咱们仍然会踊跃地保护与迭代,为有须要的同学提供技术支持,也会不定时地公布一些相干的文章帮忙大家更好地了解与应用咱们的组件库。

来点个 Star ❤️ 反对咱们一下吧 ~

退出移动版