Vue应用中的数据规范化(Normalization):实现扁平化存储与避免数据重复获取

Vue 应用中的数据规范化:实现扁平化存储与避免数据重复获取

大家好,今天我们来聊聊 Vue 应用中的数据规范化。在实际开发中,我们经常会遇到从后端获取的数据结构复杂且嵌套很深的情况。如果直接将这些数据存储在 Vue 组件的 data 中,可能会导致性能问题,增加组件的复杂度,并且难以维护。因此,我们需要一种方法来优化数据结构,使其更易于管理和使用。这就是数据规范化。

什么是数据规范化?

数据规范化是一种组织数据的方式,旨在减少冗余,提高数据一致性,并简化数据访问。在 Vue 应用中,数据规范化通常指的是将嵌套的数据结构转换为扁平化的数据结构,并使用唯一的标识符(ID)来引用相关数据。

为什么要进行数据规范化?

进行数据规范化有以下几个主要优点:

  1. 减少数据冗余: 避免在多个地方存储相同的数据,减少内存占用。

  2. 提高数据一致性: 当需要更新数据时,只需要更新一个地方,确保所有引用该数据的地方都能反映最新的状态。

  3. 简化数据访问: 可以通过 ID 直接访问数据,避免深层嵌套的遍历。

  4. 提升性能: 扁平化的数据结构更容易被 Vue 追踪和更新,提高渲染性能。

  5. 易于维护: 更清晰的数据结构更容易理解和维护。

数据规范化的实现方式

数据规范化主要分为以下几个步骤:

  1. 定义数据结构: 确定需要规范化的数据类型以及它们之间的关系。

  2. 创建 ID 映射: 为每个数据对象生成唯一的 ID,并创建一个 ID 到数据对象的映射。

  3. 扁平化数据: 将嵌套的数据结构转换为扁平化的数据结构,并使用 ID 来引用相关数据。

  4. 更新数据: 当需要更新数据时,更新 ID 映射中的数据对象。

具体示例

假设我们有一个博客应用,后端返回的文章数据结构如下:

[
  {
    "id": 1,
    "title": "Vue 数据规范化",
    "content": "这是一篇关于 Vue 数据规范化的文章。",
    "author": {
      "id": 101,
      "name": "张三",
      "email": "[email protected]"
    },
    "comments": [
      {
        "id": 201,
        "content": "写得很好!",
        "author": {
          "id": 102,
          "name": "李四",
          "email": "[email protected]"
        }
      },
      {
        "id": 202,
        "content": "学习了!",
        "author": {
          "id": 103,
          "name": "王五",
          "email": "[email protected]"
        }
      }
    ]
  },
  {
    "id": 2,
    "title": "Vue 组件通信",
    "content": "这是一篇关于 Vue 组件通信的文章。",
    "author": {
      "id": 101,
      "name": "张三",
      "email": "[email protected]"
    },
    "comments": [
      {
        "id": 203,
        "content": "很有帮助!",
        "author": {
          "id": 102,
          "name": "李四",
          "email": "[email protected]"
        }
      }
    ]
  }
]

可以看到,这个数据结构存在嵌套关系,文章包含作者和评论,评论又包含作者。如果直接将这个数据结构存储在 Vue 组件的 data 中,可能会导致性能问题,并且难以维护。

我们可以使用数据规范化来优化这个数据结构。

1. 定义数据结构:

  • articles: 文章列表,每个文章对象包含 id, title, content, authorId, commentIds
  • authors: 作者列表,每个作者对象包含 id, name, email
  • comments: 评论列表,每个评论对象包含 id, content, authorId

2. 创建 ID 映射:

我们将创建三个对象,分别用于存储文章、作者和评论的 ID 映射:

  • articlesById: 一个以文章 ID 为键,文章对象为值的对象。
  • authorsById: 一个以作者 ID 为键,作者对象为值的对象。
  • commentsById: 一个以评论 ID 为键,评论对象为值的对象。

3. 扁平化数据:

我们可以编写一个函数来将后端返回的数据转换为扁平化的数据结构:

function normalizeData(data) {
  const articlesById = {};
  const authorsById = {};
  const commentsById = {};

  data.forEach(article => {
    const articleId = article.id;

    // 处理作者
    const author = article.author;
    const authorId = author.id;
    if (!authorsById[authorId]) {
      authorsById[authorId] = { ...author }; // 创建作者对象的副本,避免修改原始数据
    }

    // 处理评论
    const commentIds = [];
    article.comments.forEach(comment => {
      const commentId = comment.id;

      // 处理评论作者
      const commentAuthor = comment.author;
      const commentAuthorId = commentAuthor.id;
      if (!authorsById[commentAuthorId]) {
        authorsById[commentAuthorId] = { ...commentAuthor }; // 创建作者对象的副本,避免修改原始数据
      }

      commentsById[commentId] = { ...comment, authorId: commentAuthorId }; // 创建评论对象的副本,并添加 authorId
      commentIds.push(commentId);
    });

    articlesById[articleId] = {
      id: articleId,
      title: article.title,
      content: article.content,
      authorId: authorId,
      commentIds: commentIds
    };
  });

  return {
    articlesById,
    authorsById,
    commentsById
  };
}

这个函数接收后端返回的文章数据,然后将数据转换为扁平化的数据结构,并返回 articlesById, authorsByIdcommentsById 三个对象。

4. 在 Vue 组件中使用规范化的数据:

在 Vue 组件中,我们可以使用 computed 属性来访问规范化的数据:

<template>
  <div>
    <div v-for="article in articles" :key="article.id">
      <h2>{{ article.title }}</h2>
      <p>作者:{{ getAuthorName(article.authorId) }}</p>
      <p>{{ article.content }}</p>
      <h3>评论</h3>
      <ul>
        <li v-for="commentId in article.commentIds" :key="commentId">
          {{ getCommentContent(commentId) }} - {{ getAuthorName(getCommentAuthorId(commentId)) }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      rawData: [], // 原始数据,从后端获取
      normalizedData: {
        articlesById: {},
        authorsById: {},
        commentsById: {}
      }
    };
  },
  async mounted() {
    // 模拟从后端获取数据
    this.rawData = await this.fetchData();
    this.normalizedData = normalizeData(this.rawData);
  },
  computed: {
    articles() {
      return Object.values(this.normalizedData.articlesById);
    }
  },
  methods: {
    async fetchData() {
      // 模拟异步获取数据
      return new Promise((resolve) => {
        setTimeout(() => {
          const mockData = [
            {
              "id": 1,
              "title": "Vue 数据规范化",
              "content": "这是一篇关于 Vue 数据规范化的文章。",
              "author": {
                "id": 101,
                "name": "张三",
                "email": "[email protected]"
              },
              "comments": [
                {
                  "id": 201,
                  "content": "写得很好!",
                  "author": {
                    "id": 102,
                    "name": "李四",
                    "email": "[email protected]"
                  }
                },
                {
                  "id": 202,
                  "content": "学习了!",
                  "author": {
                    "id": 103,
                    "name": "王五",
                    "email": "[email protected]"
                  }
                }
              ]
            },
            {
              "id": 2,
              "title": "Vue 组件通信",
              "content": "这是一篇关于 Vue 组件通信的文章。",
              "author": {
                "id": 101,
                "name": "张三",
                "email": "[email protected]"
              },
              "comments": [
                {
                  "id": 203,
                  "content": "很有帮助!",
                  "author": {
                    "id": 102,
                    "name": "李四",
                    "email": "[email protected]"
                  }
                }
              ]
            }
          ];
          resolve(mockData);
        }, 500);
      });
    },
    getAuthorName(authorId) {
      return this.normalizedData.authorsById[authorId]?.name || '未知作者';
    },
    getCommentContent(commentId) {
      return this.normalizedData.commentsById[commentId]?.content || '无评论';
    },
    getCommentAuthorId(commentId) {
      return this.normalizedData.commentsById[commentId]?.authorId;
    }
  }
};
</script>

在这个组件中,我们首先在 mounted 钩子函数中从后端获取原始数据,然后使用 normalizeData 函数将数据转换为扁平化的数据结构。接着,我们使用 computed 属性 articles 来获取文章列表。在模板中,我们使用 v-for 指令来渲染文章列表,并使用 getAuthorNamegetCommentContent 方法来获取作者姓名和评论内容。

Vuex 中的数据规范化

在大型 Vue 应用中,我们通常使用 Vuex 来管理应用的状态。Vuex 也可以使用数据规范化来优化状态管理。

我们可以将 articlesById, authorsByIdcommentsById 存储在 Vuex 的 state 中,并创建 getter 来访问这些数据。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    articlesById: {},
    authorsById: {},
    commentsById: {}
  },
  mutations: {
    SET_NORMALIZED_DATA(state, data) {
      state.articlesById = data.articlesById;
      state.authorsById = data.authorsById;
      state.commentsById = data.commentsById;
    }
  },
  actions: {
    async fetchAndNormalizeData({ commit }) {
      // 模拟从后端获取数据
      const rawData = await new Promise((resolve) => {
        setTimeout(() => {
          const mockData = [
            {
              "id": 1,
              "title": "Vue 数据规范化",
              "content": "这是一篇关于 Vue 数据规范化的文章。",
              "author": {
                "id": 101,
                "name": "张三",
                "email": "[email protected]"
              },
              "comments": [
                {
                  "id": 201,
                  "content": "写得很好!",
                  "author": {
                    "id": 102,
                    "name": "李四",
                    "email": "[email protected]"
                  }
                },
                {
                  "id": 202,
                  "content": "学习了!",
                  "author": {
                    "id": 103,
                    "name": "王五",
                    "email": "[email protected]"
                  }
                }
              ]
            },
            {
              "id": 2,
              "title": "Vue 组件通信",
              "content": "这是一篇关于 Vue 组件通信的文章。",
              "author": {
                "id": 101,
                "name": "张三",
                "email": "[email protected]"
              },
              "comments": [
                {
                  "id": 203,
                  "content": "很有帮助!",
                  "author": {
                    "id": 102,
                    "name": "李四",
                    "email": "[email protected]"
                  }
                }
              ]
            }
          ];
          resolve(mockData);
        }, 500);
      });

      const normalizedData = normalizeData(rawData);
      commit('SET_NORMALIZED_DATA', normalizedData);
    }
  },
  getters: {
    articles: (state) => Object.values(state.articlesById),
    getAuthorById: (state) => (authorId) => state.authorsById[authorId],
    getCommentById: (state) => (commentId) => state.commentsById[commentId]
  }
})

然后,在 Vue 组件中,我们可以使用 mapGetters 辅助函数来访问 Vuex 的 state:

<template>
  <div>
    <div v-for="article in articles" :key="article.id">
      <h2>{{ article.title }}</h2>
      <p>作者:{{ getAuthorName(article.authorId) }}</p>
      <p>{{ article.content }}</p>
      <h3>评论</h3>
      <ul>
        <li v-for="commentId in article.commentIds" :key="commentId">
          {{ getCommentContent(commentId) }} - {{ getAuthorName(getCommentAuthorId(commentId)) }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
  computed: {
    ...mapGetters([
      'articles',
      'getAuthorById',
      'getCommentById'
    ])
  },
  mounted() {
    this.$store.dispatch('fetchAndNormalizeData');
  },
  methods: {
    getAuthorName(authorId) {
      const author = this.getAuthorById(authorId);
      return author ? author.name : '未知作者';
    },
    getCommentContent(commentId) {
      const comment = this.getCommentById(commentId);
      return comment ? comment.content : '无评论';
    },
    getCommentAuthorId(commentId) {
      const comment = this.getCommentById(commentId);
      return comment ? comment.authorId : null;
    }
  }
};
</script>

一些更高级的规范化技巧

  • 使用专门的库: 可以使用像 normalizr 这样的库来简化数据规范化的过程。normalizr 提供了一种声明式的方式来定义数据结构,并自动将数据转换为扁平化的数据结构。

    import { normalize, schema } from 'normalizr';
    
    // 定义 schema
    const user = new schema.Entity('users');
    const comment = new schema.Entity('comments', { author: user });
    const article = new schema.Entity('articles', {
      author: user,
      comments: [comment]
    });
    
    // 规范化数据
    const normalizedData = normalize(data, [article]);
    
    // normalizedData.entities 包含 articles, users, comments
    // normalizedData.result 包含文章 ID 列表
  • 使用 Immer.js 来更新 immutable 数据: 当使用数据规范化时,通常需要更新嵌套的数据结构。为了避免直接修改原始数据,可以使用 Immer.js 这样的库来创建 immutable 数据的副本,并安全地更新数据。

    import produce from "immer"
    
    const nextState = produce(currentState, draft => {
      draft.articlesById[articleId].title = '新的标题';
    })
  • 服务器端规范化: 尽可能在服务器端进行数据规范化,减少客户端的计算量。

规范化与非规范化数据的权衡

虽然数据规范化有很多优点,但它也有一些缺点:

  • 增加代码复杂度: 需要编写额外的代码来规范化和访问数据。
  • 增加学习成本: 需要理解数据规范化的概念和实现方式。
  • 可能降低初始渲染速度: 规范化过程需要一些计算时间。

因此,在实际应用中,需要权衡规范化和非规范化数据的优缺点,选择最适合的方案。一般来说,对于复杂且嵌套的数据结构,或者需要频繁更新的数据,建议使用数据规范化。而对于简单且静态的数据,可以直接使用非规范化的数据结构。

总结

数据规范化是一种优化 Vue 应用中数据结构的重要技术。通过将嵌套的数据结构转换为扁平化的数据结构,可以减少数据冗余,提高数据一致性,简化数据访问,提升性能,并易于维护。希望今天的讲解能够帮助大家更好地理解和应用数据规范化。
规范化通过扁平化数据结构,ID映射,简化数据访问,减少冗余,提升数据一致性和性能,使应用更易于维护。需要权衡规范化与非规范化的优缺点,选择最适合的方案。

更多IT精英技术系列讲座,到智猿学院

发表回复

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