如何利用 Vue 的响应式系统,实现一个可配置的数据筛选和排序组件?

哈喽,大家好!我是今天的主讲人,很高兴能和大家一起聊聊 Vue 响应式系统和数据筛选排序组件。今天咱们不搞那些虚头巴脑的理论,直接上干货,手把手带你撸一个可配置的数据筛选和排序组件。

开场白:为啥要折腾这个?

话说,咱们前端开发,天天跟数据打交道。表格、列表,哪个项目离得开?数据一多,筛选排序就成了刚需。如果每次都手动写,那得累死。所以,一个灵活、可配置的筛选排序组件,绝对是提高生产力的神器!

第一部分:Vue 响应式系统:咱们的基石

要实现一个好的筛选排序组件,首先得理解 Vue 的响应式系统。简单来说,它就像一个超级侦察兵,时刻监视着你的数据,一旦数据发生变化,它就能迅速通知相关的组件进行更新。

  • 响应式原理: Vue 内部使用了 Object.defineProperty (Vue 2) 或 Proxy (Vue 3) 来劫持数据的 getter 和 setter。当你在模板中使用数据时,Vue 会自动追踪这些数据的依赖关系。当数据发生变化时,Vue 会通知所有依赖该数据的组件进行重新渲染。
  • data 选项: 这是定义组件数据的关键。所有在 data 中声明的属性,都会被 Vue 转化为响应式属性。
  • computed 属性: 用于声明基于其他响应式属性计算得出的属性。computed 属性具有缓存机制,只有当依赖的属性发生变化时,才会重新计算。
  • watch 监听器: 用于监听特定响应式属性的变化。当属性发生变化时,可以执行自定义的回调函数。

第二部分:组件架构设计:搭好积木的框架

咱们的组件,要足够灵活,可以应对各种不同的数据结构和筛选排序需求。所以,要好好设计一下。

  • 核心组件: 咱们创建一个名为 DataFilterSorter 的核心组件。这个组件负责接收数据、配置项,并根据配置项进行筛选和排序。
  • 配置项: 配置项是灵魂!它决定了组件的行为。配置项可以包括:
    • fields: 一个数组,包含需要筛选和排序的字段信息。
    • sortable: 一个布尔值,指示是否允许排序。
    • filterable: 一个布尔值,指示是否允许筛选。
    • filters: 一个对象,包含初始的筛选条件。
    • sort: 一个对象,包含初始的排序字段和方向。
  • 数据流: 数据从父组件传递给 DataFilterSorter 组件。DataFilterSorter 组件根据配置项对数据进行处理,并将处理后的数据传递给子组件进行展示。

第三部分:代码实现:撸起袖子开干

现在,咱们开始写代码。

  1. 创建 DataFilterSorter.vue 组件:

    <template>
      <div>
        <!-- 筛选表单 -->
        <div v-if="filterable">
          <form @submit.prevent="applyFilters">
            <div v-for="field in fields" :key="field.key">
              <label :for="field.key">{{ field.label }}:</label>
              <input
                type="text"
                :id="field.key"
                v-model="filterValues[field.key]"
              />
            </div>
            <button type="submit">筛选</button>
            <button type="button" @click="resetFilters">重置</button>
          </form>
        </div>
    
        <!-- 排序表头 -->
        <div v-if="sortable">
          <table>
            <thead>
              <tr>
                <th v-for="field in fields" :key="field.key" @click="sortBy(field.key)">
                  {{ field.label }}
                  <span v-if="sort.field === field.key">
                    {{ sort.direction === 'asc' ? '▲' : '▼' }}
                  </span>
                </th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="item in filteredAndSortedData" :key="item.id">
                <td v-for="field in fields" :key="field.key">{{ item[field.key] }}</td>
              </tr>
            </tbody>
          </table>
        </div>
    
        <!-- 如果不需要筛选和排序,直接展示数据 -->
        <div v-else>
            <table>
                <thead>
                    <tr>
                        <th v-for="field in fields" :key="field.key">{{ field.label }}</th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="item in data" :key="item.id">
                        <td v-for="field in fields" :key="field.key">{{ item[field.key] }}</td>
                    </tr>
                </tbody>
            </table>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        data: {
          type: Array,
          required: true,
        },
        fields: {
          type: Array,
          required: true,
        },
        sortable: {
          type: Boolean,
          default: false,
        },
        filterable: {
          type: Boolean,
          default: false,
        },
        filters: {
          type: Object,
          default: () => ({}),
        },
        sort: {
          type: Object,
          default: () => ({
            field: null,
            direction: 'asc',
          }),
        },
      },
      data() {
        return {
          filterValues: { ...this.filters }, // 使用浅拷贝,避免直接修改父组件的filters
          sortData: { ...this.sort } //同样使用浅拷贝
        };
      },
      computed: {
        filteredData() {
          if (!this.filterable) {
            return this.data;
          }
    
          return this.data.filter((item) => {
            for (const key in this.filterValues) {
              if (this.filterValues[key] && String(item[key]).toLowerCase().indexOf(this.filterValues[key].toLowerCase()) === -1) {
                return false;
              }
            }
            return true;
          });
        },
        filteredAndSortedData() {
          let data = [...this.filteredData]; // Create a copy to avoid modifying the original data
          if (this.sortable && this.sortData.field) {
            data.sort((a, b) => {
              const field = this.sortData.field;
              const direction = this.sortData.direction;
    
              let comparison = 0;
              if (a[field] > b[field]) {
                comparison = 1;
              } else if (a[field] < b[field]) {
                comparison = -1;
              }
    
              return direction === 'asc' ? comparison : -comparison;
            });
          }
          return data;
        },
      },
      methods: {
        applyFilters() {
            // 这里可以做一些验证,或者触发事件通知父组件
            this.$emit('update:filters', this.filterValues); // 通知父组件更新filters
        },
        resetFilters() {
            for (let key in this.filterValues) {
                this.filterValues[key] = '';
            }
            this.$emit('update:filters', this.filterValues); // 通知父组件更新filters
        },
        sortBy(field) {
          if (!this.sortable) {
            return;
          }
          if (this.sortData.field === field) {
            this.sortData.direction = this.sortData.direction === 'asc' ? 'desc' : 'asc';
          } else {
            this.sortData.field = field;
            this.sortData.direction = 'asc';
          }
    
          this.$emit('update:sort', this.sortData);
        },
      },
    };
    </script>
  2. 父组件中使用 DataFilterSorter 组件:

    <template>
      <div>
        <DataFilterSorter
          :data="myData"
          :fields="myFields"
          :sortable="true"
          :filterable="true"
          :filters="myFilters"
          :sort="mySort"
          @update:filters="updateFilters"
          @update:sort="updateSort"
        />
      </div>
    </template>
    
    <script>
    import DataFilterSorter from './DataFilterSorter.vue';
    
    export default {
      components: {
        DataFilterSorter,
      },
      data() {
        return {
          myData: [
            { id: 1, name: '张三', age: 25, city: '北京' },
            { id: 2, name: '李四', age: 30, city: '上海' },
            { id: 3, name: '王五', age: 28, city: '广州' },
            { id: 4, name: '赵六', age: 22, city: '深圳' },
          ],
          myFields: [
            { key: 'name', label: '姓名' },
            { key: 'age', label: '年龄' },
            { key: 'city', label: '城市' },
          ],
          myFilters: {
            name: '',
            age: '',
            city: ''
          },
          mySort: {
            field: 'age',
            direction: 'desc'
          }
        };
      },
      methods: {
        updateFilters(newFilters) {
          this.myFilters = newFilters;
        },
        updateSort(newSort) {
          this.mySort = newSort;
        }
      }
    };
    </script>

代码解释:

  • DataFilterSorter.vue 组件:
    • props: 定义了组件接收的配置项,包括数据、字段信息、是否允许筛选和排序等。
    • data: 定义了组件内部的状态,包括筛选条件和排序信息。
    • computed: 定义了计算属性,用于根据筛选条件和排序信息对数据进行处理。
      • filteredData: 根据筛选条件过滤数据。这里使用了 String(item[key]).toLowerCase().indexOf(this.filterValues[key].toLowerCase()) === -1 来进行模糊匹配,并且忽略大小写。
      • filteredAndSortedData: 先过滤数据,然后根据排序信息对数据进行排序。这里使用了 sort 方法进行排序,并根据 sort.direction 来决定升序还是降序。
    • methods: 定义了组件的方法,包括应用筛选、重置筛选、排序等。
      • applyFilters: 应用筛选条件。这里可以做一些验证,或者触发事件通知父组件。
      • resetFilters: 重置筛选条件。
      • sortBy: 根据指定的字段进行排序。如果已经按照该字段排序,则切换排序方向;否则,按照该字段升序排序。
  • 父组件:
    • 引入 DataFilterSorter 组件。
    • 定义了 myDatamyFieldsmyFiltersmySort 等数据,用于配置 DataFilterSorter 组件。
    • 使用了 @update:filters@update:sort 来监听 DataFilterSorter 组件触发的事件,并更新父组件的状态。 使用了 :filters.sync="myFilters" :sort.sync="mySort" 是vue2的语法,vue3使用@update:filters="updateFilters" @update:sort="updateSort"来代替.

第四部分:进阶技巧:让组件更上一层楼

  • 自定义筛选器: 可以允许用户自定义筛选器,例如使用日期范围选择器、下拉选择器等。这可以通过在 fields 配置项中添加 type 属性来实现。
  • 服务端排序: 当数据量非常大时,前端排序可能会影响性能。可以考虑将排序逻辑放在服务端,前端只需要传递排序字段和方向即可。
  • 可访问性: 确保组件具有良好的可访问性,例如使用适当的 ARIA 属性。
  • 单元测试: 编写单元测试,确保组件的稳定性和可靠性。

第五部分:总结:好记性不如烂笔头

咱们今天一起撸了一个可配置的数据筛选和排序组件,核心是利用 Vue 的响应式系统,根据配置项动态地对数据进行处理。

组件的核心功能和特点:

功能 描述
数据接收 通过 props 接收父组件传递的数据 (data) 和字段配置信息 (fields)。
灵活配置 通过 props 接收 sortablefilterable 属性,控制组件是否启用排序和筛选功能。
初始状态 通过 props 接收 filterssort 属性,设置组件的初始筛选条件和排序状态。
响应式筛选 使用 computed 属性 filteredData,根据 filterValues 响应式地过滤数据。 筛选条件通过双向绑定到表单输入框,实现实时筛选。
响应式排序 使用 computed 属性 filteredAndSortedData,在筛选后的数据基础上,根据 sort.fieldsort.direction 响应式地排序数据。
用户交互 提供筛选表单和重置按钮,允许用户输入筛选条件。 通过点击表头触发排序,切换排序字段和方向。
事件通信 通过 $emit 触发 update:filtersupdate:sort 事件,将最新的筛选条件和排序状态传递给父组件,实现父子组件之间的双向数据绑定。
浅拷贝 使用浅拷贝 filterValues: { ...this.filters }sortData: { ...this.sort },避免直接修改父组件传递的 filterssort 属性。
模糊匹配 筛选时使用 String(item[key]).toLowerCase().indexOf(this.filterValues[key].toLowerCase()) === -1 进行模糊匹配,并且忽略大小写。
避免修改原数据 在排序前使用 let data = [...this.filteredData] 创建数据的副本,避免修改原始数据。

记住,代码不是一蹴而就的,需要不断地练习和改进。希望今天的分享能帮助你更好地理解 Vue 响应式系统,并能够应用到实际项目中。

下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注