Help us learn about your current experience with the documentation. Take the survey.

从 Vuex 迁移

Vuex 在 GitLab 中已被弃用,如果你现有的 Vuex store,强烈建议你进行迁移。

为什么?

我们已经将 GraphQL API 定义为所有面向用户功能的首选方案。 我们可以安全地假设,只要 GraphQL 存在,Apollo Client 也会随之存在。 我们不希望将 Vuex 与 Apollo 一起使用,所以随着我们从 REST API 迁移到 GraphQL,VueX store 的数量会自然减少。

本节提供了将现有的 VueX store 转换为纯 Vue 和 Apollo,或者如何减少对 VueX 依赖的指南和方法。

如何操作?

在开始迁移之前,选择你偏好的状态管理方案

迁移到 Vue 管理的状态和 Apollo Client

总体而言,我们希望了解我们的变更会有多复杂。有时,我们只有少数几个真正值得存储在全局状态中的属性,有时它们可以安全地全部提取到纯 Vue 中。VueX 属性通常属于以下类别之一:

  • 静态属性
  • 响应式可变属性
  • Getters
  • API 数据

因此,第一步是阅读当前的 VueX 状态并确定每个属性的类别。

从高层次来看,我们可以将每个类别映射到等效的非 VueX 代码模式:

  • 静态属性:使用 Vue API 的 Provide/Inject。
  • 响应式可变属性:Vue 事件和 props,Apollo Client。
  • Getters:工具函数,Apollo update 钩子,计算属性。
  • API 数据:Apollo Client。

让我们通过一个例子来说明。在每一节中,我们都会引用这个状态,并逐步完成完整的迁移:

// state.js 也就是我们的 store
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
  blobPath,
  summaryEndpoint,
  suiteEndpoint,
  testReports: {},
  selectedSuiteIndex: null,
  isLoading: false,
  errorMessage: null,
  limit : 10,
  pageInfo: {
    page: 1,
    perPage: 20,
  },
});

如何迁移静态值

最容易迁移的值类型是静态值,包括:

  • 客户端常量:如果静态值是客户端常量,它可能已经在 store 中实现,以便其他状态属性或方法可以轻松访问。但是,更好的做法是将此类值添加到 constants.js 文件中,并在需要时导入它。
  • Rails 注入的数据集:这些是我们可能需要提供给 Vue 应用的值。它们是静态的,因此将它们添加到 VueX store 中不是必需的,而是可以通过 provide/inject Vue API 轻松实现,这样效果相同但没有 VueX 的开销。这应该在我们组件挂载的最顶层 JS 文件中注入。

如果我们查看上面的示例,我们已经有到两个属性名称中包含 Endpoint,这可能意味着它们来自我们的 Rails 数据集。为了确认这一点,我们会在代码库中搜索这些属性,查看它们在哪里定义,在我们的示例中确实如此。此外,blobPath 也是一个静态属性,这里不太明显的是 pageInfo 实际上是一个常量!它永远不会被修改,仅用作我们在 getter 中使用的默认值:

// state.js 也就是我们的 store
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
  limit
  blobPath, // 静态 - 数据集
  summaryEndpoint, // 静态 - 数据集
  suiteEndpoint, // 静态 - 数据集
  testReports: {},
  selectedSuiteIndex: null,
  isLoading: false,
  errorMessage: null,
  pageInfo: { // 静态 - 常量
    page: 1, // 静态 - 常量
    perPage: 20, // 静态 - 常量
  },
});

如何迁移响应式可变值

当被许多不同的组件使用时,这些值特别有用,因此我们可以先评估每个属性的读取和写入次数,以及它们之间的距离。读取次数越少,它们越接近在一起,就越容易移除这些属性,改用原生的 Vue props 和事件。

简单的读取/写入值

如果我们回到示例,selectedSuiteIndex 只被一个组件使用,并且在一个 getter 中使用一次。此外,这个 getter 本身只使用一次!将此逻辑转换为 Vue 会相当容易,因为它可以成为组件实例上的 data 属性。对于 getter,我们可以改用计算属性,或者使用组件上的方法返回正确的项目,因为我们也可以在那里访问索引。这是 VueX store 如何通过添加大量抽象来使应用程序复杂化的完美例子,而实际上所有内容都可以存在于同一个组件中。

幸运的是,在我们的示例中,所有属性都可以存在于同一个组件中。但是,有些情况下这是不可能的。当发生这种情况时,我们可以使用 Vue 事件和 props 在兄弟组件之间进行通信。将有问题的数据存储在应该了解状态的父组件中,当子组件想要写入组件时,它可以 $emit 一个带有新值的事件,让父组件更新。然后,通过将 props 级联到所有子组件,所有兄弟组件的实例将共享相同的数据。

有时,事件和 props 可能会显得很麻烦,特别是在非常深的组件树中。然而,重要的是要意识到这主要是一个便利性问题,而不是需要修复的架构缺陷或问题。传递 props,即使是深度嵌套的,也是跨组件通信的非常可接受的模式。

共享的读取/写入值

假设我们有一个 store 中的属性,被多个组件用于读取和写入,这些操作要么数量众多,要么相距甚远,以至于 Vue props 和 events 似乎不是一个好的解决方案。相反,我们使用 Apollo 客户端解析器。本节需要了解 Apollo Client 的知识,请随时根据需要查看 apollo 详情。

首先,我们需要设置我们的 Vue 应用程序以使用 VueApollo。然后在创建 store 时,我们将 resolverstypedefs(稍后定义)传递给 Apollo Client:

import { resolvers } from "./graphql/settings.js"
import typeDefs from './graphql/typedefs.graphql';

...
const apolloProvider = new VueApollo({
  defaultClient: createDefaultClient({
    resolvers, // 待会编写
    { typeDefs }, // 我们马上创建这个
  }),
});

对于我们的示例,让我们将字段命名为 app.status,我们需要定义使用 @client 指令的查询和变异。让我们现在创建它们:

// get_app_status.query.graphql
query getAppStatus {
  app @client {
    status
  }
}
// update_app_status.mutation.graphql
mutation updateAppStatus($appStatus: String) {
  updateAppStatus(appStatus: $appStatus) @client
}

对于我们模式中不存在的字段,我们需要设置 typeDefs。例如:

// typedefs.graphql

type TestReportApp {
  status: String!
}

extend type Query {
  app: TestReportApp
}

现在我们可以编写我们的解析器,以便我们可以使用变异来更新字段:

// settings.js
export const resolvers = {
  Mutation: {
    // appStatus 是我们变异的参数
    updateAppStatus: (_, { appStatus }, { cache }) => {
      cache.writeQuery({
        query: getAppStatus,
        data: {
          app: {
            __typename: 'TestReportApp',
            status: appStatus,
          },
        },
      });
    },
  }
}

对于查询,这不需要任何额外的说明,因为它表现得像任何 Object,因为查询 app { status } 等同于 app.status。然而,我们需要编写一个"默认"的 writeQuery(来定义我们的字段将拥有的第一个值),或者我们可以为我们的 cacheConfig 设置 typePolicies 来提供这个默认值。

所以现在当我们想要从这个值读取时,我们可以使用我们的本地查询。当我们需要更新它时,我们可以调用变异并将新值作为参数传递。

网络相关的值

有些值如 isLoadingerrorMessage 与网络请求状态相关。这些是读取/写入属性,但稍后将很容易被 Apollo Client 自身的功能替换,而无需我们做任何额外工作:

// state.js 也就是我们的 store
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
  blobPath, // 静态 - 数据集
  summaryEndpoint, // 静态 - 数据集
  suiteEndpoint, // 静态 - 数据集
  testReports: {},
  selectedSuiteIndex: null, // 可变 -> data 属性
  isLoading: false, // 可变 -> 与网络相关
  errorMessage: null, // 可变 -> 与网络相关
  pageInfo: { // 静态 - 常量
    page: 1, // 静态 - 常量
    perPage: 20, // 静态 - 常量
  },
});

如何迁移 getters

Getters 必须逐个案例审查,但一般指导原则是,很有可能编写一个纯 JavaScript 工具函数,该函数将我们在 getter 中使用的状态值作为参数,然后返回我们想要的任何值。考虑以下 getter:

// getters.js
export const getSelectedSuite = (state) =>
  state.testReports?.test_suites?.[state.selectedSuiteIndex] || {};

我们在这里所做的只是引用两个状态值,它们都可以成为函数的参数:

//new_utils.js
export const getSelectedSuite = (testReports, selectedSuiteIndex) =>
  testReports?.test_suites?.[selectedSuiteIndex] || {};

这个新的工具函数可以像以前一样导入和使用,但直接在组件内部使用。此外,大多数 getters 的规范可以很容易地移植到工具函数中,因为逻辑得到了保留。

如何迁移 API 数据

我们的最后一个属性叫做 testReports,它通过 axios 调用 API 获取。我们假设我们处于一个纯 REST 应用程序中,GraphQL 数据尚不可用:

// actions.js
export const fetchSummary = ({ state, commit, dispatch }) => {
  dispatch('toggleLoading');

  return axios
    .get(state.summaryEndpoint)
    .then(({ data }) => {
      commit(types.SET_SUMMARY, data);
    })
    .catch(() => {
      createAlert({
        message: s__('TestReports|获取摘要时出错。'),
      });
    })
    .finally(() => {
      dispatch('toggleLoading');
    });
};

这里我们有两个选择。如果这个动作只使用一次,没有什么能阻止我们将所有代码从 actions.js 文件移动到执行获取的组件中。然后,很容易移除所有与状态相关的代码,改用 data 属性。在这种情况下,isLoadingerrorMessages 会与它一起存在,因为它只使用一次。

如果我们多次重用这个函数(或计划这样做),那么可以利用 Apollo Client 做它最擅长的事情:网络调用和缓存。在本节中,我们假设你了解 Apollo Client 并且知道如何设置它,但请随时阅读 GraphQL 文档

我们可以使用本地 GraphQL 查询(带有 @client 指令)来结构化我们想要接收数据的方式,然后使用客户端解析器告诉 Apollo Client 如何解析该查询。我们可以在浏览器网络选项卡中查看我们的 REST 调用,并确定哪种结构适合用例。在我们的示例中,我们可以这样编写查询:

query getTestReportSummary($fullPath: ID!, $iid: ID!, endpoint: String!) {
  project(fullPath: $fullPath){
    id,
    pipeline(iid: $iid){
      id,
      testReportSummary(endpoint: $endpoint) @client {
        testSuites{
          nodes{
            name
            totalTime,
            # 这里还有更多字段,但我们的示例不需要
          }
        }
      }
    }
  }
}

这里的结构是任意的,因为我们可以按照我们想要的方式编写它。可能会想跳过 project.pipeline.testReportSummary,因为这不是 REST 调用的结构。然而,通过使查询结构符合 GraphQL API,如果我们决定稍后过渡到 GraphQL,我们就不需要修改我们的查询,只需删除 @client 指令即可。这也给了我们免费缓存,因为如果我们尝试再次为同一管道获取摘要,Apollo Client 知道我们已经有了结果!

此外,我们将一个 endpoint 参数传递给我们的字段 testReportSummary。在纯 GraphQL 中这不是必需的,但我们的解析器稍后需要这些信息来进行 REST 调用。

现在我们需要编写一个客户端解析器。当我们用 @client 指令标记一个字段时,它不会发送到服务器,Apollo Client 期望我们定义我们自己的代码来解析该值。我们可以在传递给 Apollo Client 的 cacheConfig 对象中为 testReportSummary 编写一个客户端解析器。我们希望这个解析器进行 Axios 调用并返回我们想要的任何数据结构。这也是转移 getter 的完美位置,如果它总是在访问 API 数据或处理数据结构时使用:

// graphql_config.js
export const resolvers = {
  Query: {
    testReportSummary(_, { summaryEndpoint }): {
    return axios.get(summaryEndpoint).then(({ data }) => {
      return data // 我们可以在这里格式化/处理数据,而不是使用 getter
    }
  }
}

每次我们对 testReportSummary @client 字段进行调用时,都会执行这个解析器并返回操作结果,这基本上与 VueX 动作做的工作相同。

如果我们假设我们的 GraphQL 调用存储在一个名为 testReportSummary 的数据属性中,我们可以在触发此查询的任何组件中将 isLoading 替换为 this.$apollo.queries.testReportSummary.lodaing。错误可以在查询的 error 钩子内部处理。

迁移策略

现在我们已经了解了每种数据类型,让我们回顾一下如何规划从基于 VueX 的 store 到非 VueX store 的过渡。我们试图避免 VueX 和 Apollo 同时存在,因此在同一上下文中两个 store 可用的时间越少越好。为了最小化这种重叠,我们应该从 store 中移除所有不涉及添加 Apollo store 的内容开始。以下每一点都可以是一个独立的 MR:

  1. 远离静态值,包括 Rails 数据集和客户端常量,改用 provide/injectconstants.js 文件。
  2. 将简单的读取/写入操作替换为:
    • 如果在单个组件中,使用 data 属性和 methods
    • 如果在局部组件组中共享,使用 propsemits
  3. 使用 Apollo Client @client 指令替换共享的读取/写入操作。
  4. 使用 Apollo Client 替换网络数据,当可用时使用实际的 GraphQL 调用,或使用客户端解析器进行 REST 调用。

如果无法快速替换共享的读取/写入操作或网络数据(例如在一两个里程碑内),考虑在功能标志后面创建一个不同的 Vue 组件,该组件专门使用 Apollo Client 功能,并将当前使用 VueX 的组件重命名为带有 legacy- 前缀的名称。新组件可能无法立即实现所有功能,但随着我们创建 MR,我们可以逐步添加它们。这样,我们的遗留组件专门使用 VueX 作为 store,而新组件只使用 Apollo。在新组件重新实现了所有逻辑后,我们可以打开功能标志并确保其行为符合预期。