Еще раз о табличном компоненте: исправлен заголовок и столбец таблицы

JavaScript
Еще раз о табличном компоненте: исправлен заголовок и столбец таблицы

предисловие

Содержание этой статьи включает в себя:

  • Элемент UI, реализующий фиксированное мышление и краткое описание столбцов головного стола
  • translate3dКак добиться фиксации столбца таблицы заголовков

Книга наследует от вышеперечисленного, в предыдущем тексте[Vue Advanced] Бронзовые игроки, как разработать библиотеку пользовательского интерфейсаПредставлены подробности разработки библиотеки компонентов Vue, а разработка таких компонентов, как кнопки и таблицы, реализована в качестве примера. существуетAngeВ этой UI-библиотеке я реализовал табличный компонент с широкими возможностями настройки: заголовки и столбцы можно зафиксировать, а содержимое можно определить само по себе.

Первое, что нужно признать, это то, что этот компонент таблицы реализует просто:

  • Создание таблиц для отображения данных
  • Фиксированный заголовок
  • Фиксированные столбцы таблицы
  • Можно реализовать простую версию многоуровневого заголовка

Табличный компонент является одним из самых сложных компонентов в UI библиотеке.Сценариев использования таблиц в проекте много.Нам сложно охватить все потребности.Более распространенные из них:

  • фиксированный заголовок
  • Фиксированные левые/правые столбцы таблицы
  • многоуровневый заголовок
  • Проверить данные строки
  • Развернуть данные строки
  • Сортировка данных

С точки зрения объекта действия эти потребности можно классифицировать каквлияет на макет(Например: фиксированный столбец таблицы заголовков) иДанные о воздействии(Например: тиковые данные) Две категории. существуетAnge UIВ табличном компоненте реализованы только некоторые из функций ниже класса, которые влияют на макет.Этот компонент не манипулирует данными, и даже определено использование тегов tr и td (и как обернуть данные в td) для отображения данных самим пользователем. сильно ударитьздесьОзнакомьтесь с примерами в Интернете или посмотрите код:

<ag-table offsetTop="57.5">
    <tr slot="thead">
        <!-- 定义表头列 -->
        <th v-if="isExpand">姓名</th>
        <th v-for="(each, index) in singleTableHead" :key="index">{{ each }}</th>
    </tr>
    <tr v-for="(each, index) in singleTableBody" slot='tbody' :key="`tbody-${index}`">
        <!-- 渲染表体内容 -->
        <td v-if="isExpand">{{ each.name }}</td>
        <td>{{ each.verdict }}</td>
        <td>{{ each.song }}</td>
    </tr>
</ag-table>

пройти черезслот слотУкажите thead или tbody. Простота означает точность и масштабируемость, но в то же время проблема заключается в том, что стоимость использования пользователей высока (например, реализация функции выбора данных, конечноag-tableЭта функция также может быть расширена без манипулирования исходными данными).

Поговорите о фиксированном столбце таблицы заголовков элемента

Глядя на эффект рендеринга компонента таблицы Element в браузере, способ, которым Element реализует фиксированные столбцы таблицы заголовков, заключается в разделении фиксированной части (например, заголовка таблицы) и нефиксированной части (например, тело таблицы) на разные областях (в разных div), установите область, в которой находится тело таблицы, для прокрутки, а затем синхронизируйте макет между различными областями определенными средствами (например, слотом, резервным копированием данных таблицы).

В теме Ele.meстатьяПодробно, реализация столбца фиксированной верхней таблицы подробно описана. Ниже приведен простой сводный и организация существующих проблем.

1.1 Идея исправления заголовка

Просмотрите визуализацию табличного компонента в браузере, чтобы увидеть:

Заголовок и тело размещаются в двух разных областях div:el-table__header-wrapper & el-table__body-wrapper, так что когда содержимое тела таблицы превысит высоту контейнера, появится полоса прокрутки и будет прокручиваться только в своей области, достигая эффекта фиксации заголовка таблицы. Такая реализация приводит к двум проблемам:

  • Ширина двух таблиц несовместима: в области, где находится тело таблицы, есть дополнительная полоса прокрутки.
  • Как сохранить постоянную ширину столбца между двумя таблицами

Для вышеуказанных вопросов элемент также обрабатывал его, цитируемый, картина текста:

Добавьте один в раздел заголовкаЭлемент желоба, виртуальный в полосу прокрутки, чтобы занять определенную ширину (розовая вертикальная полоса в правом верхнем углу изображения), и обработка той же ширины, когда пользователь должен ее использовать.Передайте ширину каждого столбца.

Каковы недостатки этой реализации?

  • Дополнительный ремонт и новые элементы (Водосточный желоб);
  • Настройка ширины каждого столбца увеличивает стоимость пользователя и в идеале должна иметь возможность адаптироваться в соответствии с текстовым содержимым;
  • Полоса прокрутки корпуса часов не может подняться вверх (не может прокрутиться до верхней части заголовка часов), это меня очень беспокоит;
  • Заголовок таблицы фиксируется только относительно тела таблицы, можно ли его зафиксировать относительно окна?

1.2 Идея закрепления столбцов таблицы

Реализовать фиксированные столбцы таблицы относительно сложно, и можно сказать, что для реализации этой функции элемент заплатил «огромную цену». В этом рендеринге с фиксированными левой и правой колонками:

выносить3 формы:el-table__header-wrapper & el-table__body-wrapperэто область тела,el-table__fixedэто левая фиксированная область столбца,el-table__fixed-rightЭто правая фиксированная область столбца), и каждая таблица имеет 2 таблицы, всего 6 таблиц; фиксированный эффект достигается за счет установки абсолютного позиционирования и ширины левой и правой областей.

В чем проблема с этой реализацией?

  • Таблица данных визуализируется в трех экземплярах, что увеличивает нагрузку на DOM в три раза. (Это также основная причина снижения производительности таблицы элементов, когда объем данных велик или страница не выгружается)
  • Синхронизация событий прокрутки мыши: необходимость прокрутки синхронизированной прокрутки в двух других областях в регионе
  • Дополнительное обслуживание фиксированных стилей и содержимого столбцов (например, ширины и т. д.)

Исходя из этого, реализация таблицы пользовательского интерфейса Ange рассматривает другой способ достижения наименьшей стоимости DOM.

getBoundingClientRect

Прежде чем представить метод реализации фиксированного заголовка и столбца таблицы, давайте сначала популяризируем API getBoundingClientRect.

Метод getBoundingClientRect() возвращает размер элемента и его положение относительно окна просмотра, а возвращаемое значение — объект DOMRect. Объект DOMRect содержит набор доступных только для чтения свойств для описания границы: левая, правая, верхняя, нижняя, в пикселях. Свойства, отличные от ширины и высоты, относятся к верхнему левому углу области просмотра.

Как показано ниже:

Реализовать фиксированные заголовки

В таблице используйте thead и tbody для отображения заголовка и тела соответственно, как показано ниже:

<template>
  <div class="ange-table">
    <table ref="middle-table">
      <thead class="thead-middle"
             :style="theadStyle">
          <slot name="thead" />
      </thead>
      <tbody>
        <slot name="tbody" />
      </tbody>
    </table>
  </div>
</template>

Слушайте событие прокрутки страницы, вычисляйте смещение таблицы, используйтеtranslate3dОбратно установите значение смещения по оси Y для thead, чтобы добиться эффекта фиксации заголовка. Как показано ниже:

Полоса прокрутки страницы, таблица потоп1 (положительное значение)позиция перемещена втоп2 (отрицательное значение)позиция, тогда, когда thead касается верхней части страницы (т. е. top=0) и продолжает двигаться, thead следует установить наtranslate3d(0px, -top2, 0px). Таким образом, объявление всегда находится вверху страницы. В некоторых сценариях объявление должно фиксироваться, когда оно достигает позиции заголовка, поэтому мы можем установитьoffsetTopпараметр, определяемое пользователем значение смещения, объявление вtop=0 - offsetTopвремя было установлено. Посмотрите на ключевой код реализации:

export default {
  data () {
    return: {
      fixed: { // fixed状态
        top: false
      },
      clientRect: { // 位移值
        top: 0
      }
    }
  },
  computed: {
    theadStyle () {
      const { top } = this.clientRect
      return {
        transform: `translate3d(0px, ${this.fixed.top
            ? -top
            : 0}px, 1px)`
        }
    }
  },
  watch: {
    'clientRect.top': function (val) {
      // 判断到DOMRect的top值小于0时,开始fixed
      this.fixed.top = val < 0
    }
  },
  mounted () {
    // 监听页面滚动事件,获取table对象的DOMRect属性
    window.addEventListener('scroll', this.scrollHandle, {
      capture: false,
      passive: true
    })
  },
  methods: {
    scrollHandle () {
      const $table = this.$refs.table
      if(!$table) return

      const { top } = $table.getBoundingClientRect()
      this.clientRect.top = Math.floor(top - parseInt(this.offsetTop, 10))
    }
  }
}

объединить @предисловиечастьag-tableПример использования, в<ag-tbale>пройти через одинoffsetTopпараметр, вы можете реализовать фиксацию объявления в указанной позиции. Кроме того, поскольку thead и tbody находятся в одной таблице, нет необходимости поддерживать ширину каждого столбца, он может адаптироваться в соответствии с содержимым. Проверитьdemo.

Реализовать фиксированные столбцы таблицы

Для реализации фиксированных столбцов требуются три таблицы (фиксированные левый и правый столбцы соответственно):

<template>
  <div class="ange-table">
    <!-- left table -->
    <table v-if="hasLeftTable"
         ref="leftTable"
         :style="leftStyle">
      <thead class="thead-left"
             :style="theadStyle">
          <slot name="leftThead" />
      </thead>
      <tbody>
          <slot name="leftBody" />
      </tbody>
    </table>
    <!-- middle table -->
    <table ref="table" class="table-middle">
      <thead class="thead-middle"
             :style="theadStyle">
          <slot name="thead" />
      </thead>
      <tbody>
          <slot name="tbody" />
      </tbody>
    </table>
    <!-- right table -->
    <table v-if="hasRightTable"
           ref="rightTable"
           :style="rightStyle">
      <thead class="thead-right"
             :style="theadStyle">
          <slot name="rightThead" />
      </thead>
      <tbody>
          <slot name="rightBody" />
      </tbody>
    </table>
  </div>
</template>

Когда таблица прокручивается горизонтально, вычислить горизонтальное расстояние прокрутки контейнераscrollLeft,использоватьtranslate3dОппостностно утилизировали таблицу вытеснения оси X, левый столбец фиксирован; для правильной таблицы первая на его начальное положение расположено на самом правом конце контейнера, в сочетании с значением смещения оси X, предусмотренное при прокрутке горизонтальной прокрутки. Отказ Как показано ниже:

При инициализации значение горизонтального смещения rightTable:$rightTable.right - $container.right, leftTable равно 0; при горизонтальной прокрутке значение горизонтального смещения leftTable:scrollLeft, значение смещения rightTable:初始位移 - scrollLeft. Посмотрите на ключевой код реализации:

export default {
  computed: {
    leftStyle () { // 左侧表格位移
      const { left } = this.clientRect
      return {
        transform: `translate3d(${this.fixed.left
            ? left
            : 0}px, 0px, 1px)`
      }
    },
    rightStyle () { // 右侧表格位移
      const { right } = this.clientRect
      return {
          transform: `translate3d(${-right}px, 0px, 1px)`
      }
    }
  },
  watch: {
    'clientRect.left': function (val) {
        // 横向滚动距离为正,开始设置fixed
        this.fixed.left = val > 0
      }
  },
  mounted () {
    // 存在由表格时设置其初始位移
    if(this.hasRightTable) {
        const container = this.$refs.container.getBoundingClientRect()
        const rightTable = this.$refs.rightTable.getBoundingClientRect()
        this.clientRect.right = Math.floor(rightTable.right  - container.right)
        // 记录右表格初始位移值
        this.initRight = this.clientRect.right
    }
    // 监听表格容器的滚动事件
    this.$refs.container.addEventListener('scroll', this.scrollXHandle, {
      capture: false,
      passive: true
    })
    
    // ...
  },
  methods: {
    scrollXHandle () {
      // ...
      this.clientRect.left = Math.floor(this.$refs.container.scrollLeft)

      const right = Math.floor(this.initRight - this.$refs.container.scrollLeft)
      this.clientRect.right = right
    }
  }
}

Согласно этой идее, левый и правый столбцы фиксированы, и эффект следующий (Посмотреть онлайн):

Синхронизированный эффект наведения

На последнем шаге, поскольку эта таблица состоит из трех таблиц, при наведении указателя мыши на одну из строк таблицы необходимо синхронизировать эффект наведения в соответствующих строках оставшихся двух таблиц. Посмотрите на реализацию кода ключа:

export default {
  mounted () {
    if(this.hasLeftTable || this.hasRightTable) {
      // 定义鼠标hover事件
      this.$el.addEventListener('mouseover', this.mouseOver, false)
      this.$el.addEventListener('mouseout', this.mouseLeave, false)
    }
  },
  methods: {
    mouseOver (e) {
      this.hoverClass(e, 'add')
    },
    mouseLeave(e) {
      this.hoverClass(e, 'remove')
    },
    hoverClass(e, type) {
      const tr = e.target.closest('tr')
      if(!tr) {
          return
      }
      const idx = tr.rowIndex // 当前hover行的编号
      const trs = querySelectorAll(`tbody tr:nth-child(${idx})`, this.$el)
      if(trs.length === 0) {
          return
      }
      // 对所有tbody下同一编号的tr添加hover类
      trs.forEach(each => {
          each.classList[type]('hover')
      })
    }
  }
}

пройти черезtranslate3dУстановите смещение левого и правого столбцов, чтобы добиться эффекта фиксированных столбцов, избегая:

  • Излишние накладные расходы DOM: не нужно добавлять дополнительные элементы DOM (Gutter), и необходимо копировать несколько копий данных DOM, чтобы минимизировать накладные расходы DOM;
  • Нет необходимости поддерживать ширину столбца и высоту строки между разными таблицами, и он полностью адаптивен;
  • Нет необходимости синхронизировать события прокрутки между несколькими таблицами

Эпилог

Табличный компонент всегда был компонентом с высокой сложностью разработки, учитывая как производительность, так и максимальное удобство для разработчиков. Вот введение, чтобы представить еще одну идею разработки, просто чтобы немного помочь вам, которые планируют разрабатывать компоненты таблицы.

Конечно, если у вас есть другие идеи, пожалуйста, прокомментируйте и поделитесь~

The end.