>  기사  >  웹 프론트엔드  >  Vue 3에서 공유 상태를 관리하는 방법에 대한 심층 분석? 【번역하다】

Vue 3에서 공유 상태를 관리하는 방법에 대한 심층 분석? 【번역하다】

青灯夜游
青灯夜游앞으로
2022-01-27 11:08:212891검색

Vue 3에서 공유 상태를 어떻게 관리하나요? 다음 기사에서는 이에 대해 논의할 것입니다. 이 기사가 도움이 되기를 바랍니다!

Vue 3에서 공유 상태를 관리하는 방법에 대한 심층 분석? 【번역하다】

간단한 요약↬ 대규모 애플리케이션을 작성하는 것은 어려울 수 있습니다. Vue3 애플리케이션에서 공유 상태를 사용하면 프로젝트 복잡성을 해결할 수 있습니다. 일반적인 상태 솔루션이 많이 있으며, 이 기사에서는 팩토리, 공유 객체, Vuex와 같은 메서드를 사용할 때의 장단점을 자세히 살펴보겠습니다. 또한 Vue3에서 공유 상태를 사용하는 방법을 변경할 수 있는 Vuex5의 몇 가지 사항도 보여 드리겠습니다. [관련 권장 사항: vue.js 비디오 튜토리얼]

state는 어려울 수 있습니다. 간단한 Vue 프로젝트를 시작할 때는 특정 구성 요소의 작동 상태를 매우 간단하게 유지하기만 하면 됩니다. state可能很难,当我们开始一个简单的Vue项目时,只需要我们保持特定组件的工作状态就很简单:

setup() {
  let books: Work[] = reactive([]);

  onMounted(async () => {
    // Call the API
    const response = await bookService.getScienceBooks();
    if (response.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  });

  return {
    books
  };
},

当你的项目是显示数据的单一页面(可能是为了排序或过滤数据)时,这可能很有吸引力。但是在本例中,该组件将获得每个请求的数据。如果你想保留它呢?这时候状态管理就可以发挥作用了。由于网络连接通常代价很高并且偶尔不太可靠。因此在浏览应用程序时最好保持一些状态。

另一个问题是组件之间的通信。虽然您可以使用eventsprops直接与child-parents通信, 但是当你的每个views/pages都是独立的时候,处理诸如错误拦截和忙碌标志之类的简单情况会变得很困难。举个例子, 假设你使用了一个顶级控件来显示error和加载动画:

// App.vue
<template>
  <div class="container mx-auto bg-gray-100 p-1">
    <router-link to="/"><h1>Bookcase</h1></router-link>
    <div class="alert" v-if="error">{{ error }}</div>
    <div class="alert bg-gray-200 text-gray-900" v-if="isBusy">
      Loading...
    </div>
    <router-view :key="$route.fullPath"></router-view>
  </div>
</template>

如果没有有效的方法来处理这种状态,它可能会建议使用发布/订阅系统,但实际上在许多情况下共享数据更为直接。如果你想使用共享状态,你该怎么做呢?让我们看看一些常见的方法。

注意:你将在 GitHub 上示例项目的“main”分支中找到此部分的代码。

Vue3中的共享状态 #

自从迁移到 Vue 3 后,我已经完全迁移到使用 Composition API,这篇文章我还使用了TypeScript 尽管在我展示的示例中这不是必需的。虽然你可以以任何方式共享状态,但我将向你展示我发现的几种最常用的技术模式,每一种都有自己的优点和缺点,所以不要把我在这里所说的任何东西当作唯一。

技术包括:

注意: 在撰写本文时,Vuex 5还处于RFC(征求意见)阶段,所以我想让你为Vuex5的到来做好准备,但目前还没有该选项的工作版本。

让我们深入了解……

工厂#

注意: 此部分的代码位于GitHub 上示例项目的“Factories”分支中。

工厂模式只关心你所创建的状态的实例。在此模式中,你将返回一个与Composition API中的start函数非常相似的函数。你将创建一个作用域并构建你想寻找的组件。举个例子:

export default function () {

  const books: Work[] = reactive([]);

  async function loadBooks(val: string) {
      const response = await bookService.getBooks(val, currentPage.value);
      if (response.status === 200) {
        books.splice(0, books.length, ...response.data.works);
      }
  }

  return {
    loadBooks,
    books
  };
}

你可以要求工厂创建你所需要的那部分对象:

// In Home.vue
  const { books, loadBooks } = BookFactory();

如果我们添加一个isBusy标记来显示网络请求何时发生,上面的代码不会改变,但你可以决定在哪里显示isBusy

export default function () {

  const books: Work[] = reactive([]);
  const isBusy = ref(false);

  async function loadBooks(val: string) {
    isBusy.value = true;
    const response = await bookService.getBooks(val, currentPage.value);
    if (response.status === 200) {
      books.splice(0, books.length, ...response.data.works);
    }
  }

  return {
    loadBooks,
    books,
    isBusy
  };
}

在另一个视图(vue?)中,你可以只要isBusy

// App.vue
export default defineComponent({
  setup() {
    const { isBusy } = BookFactory();
    return {
      isBusy
    }
  },
})

이 기능은 프로젝트가 데이터를 표시하는 단일 페이지인 경우(데이터 정렬 또는 필터링용) 매력적일 수 있습니다. 하지만 이 경우 구성 요소는 모든 요청에 ​​대한 데이터를 가져옵니다. 이를 유지하고 싶다면 여기에서 상태 관리가 시작됩니다. 네트워크 연결은 종종 비용이 많이 들고 때로는 신뢰할 수 없기 때문입니다. 따라서 앱을 탐색하는 동안 일부 상태를 유지하는 것이 가장 좋습니다. 🎜🎜또 다른 문제는 구성 요소 간의 통신입니다. 이벤트props를 사용하여 하위-부모와 직접 통신할 수 있지만 각 페이지가 조회/언제 독립적이므로 오류 차단 및 사용 중 플래그와 같은 간단한 상황을 처리하기가 어려워집니다. 예를 들어 최상위 컨트롤을 사용하여 오류를 표시하고 애니메이션을 로드한다고 가정해 보겠습니다. 🎜
const books: Work[] = reactive([]);
const isBusy = ref(false);

async function loadBooks(val: string) {
  isBusy.value = true;
  const response = await bookService.getBooks(val, currentPage.value);
  if (response.status === 200) {
    books.splice(0, books.length, ...response.data.works);
  }
}

export default function () {
 return {
    loadBooks,
    books,
    isBusy
  };
}
🎜 이 상태를 처리할 효율적인 방법이 없는 경우 게시/구독 시스템을 사용하는 것이 좋습니다. 그러나 실제로는 많은 경우 공유 데이터는 더 간단합니다. 공유 상태를 사용하려면 어떻게 해야 합니까? 몇 가지 일반적인 방법을 살펴보겠습니다. 🎜🎜참고:GitHub의 예 이 섹션의 코드는 프로젝트의 "기본" 분기에 있습니다 🎜. 🎜

Vue3#🎜

🎜Vue 3으로 마이그레이션한 이후 Composition API를 사용하도록 완전히 마이그레이션했습니다. , 이 기사에서는 TypeScript도 사용합니다. 하지만 제가 보여드리는 예에서는 이것이 필요하지 않습니다. 어떤 방식으로든 상태를 공유할 수 있지만, 제가 찾은 가장 일반적인 기술 패턴 중 몇 가지를 보여 드리겠습니다. 각 패턴에는 장단점이 있으므로 여기서 말하는 내용을 단지 그것만으로 받아들이지 마십시오. 🎜🎜기술에는 다음이 포함됩니다.🎜🎜🎜NOTE🎜: 이 기사를 작성하는 시점에는 Vuex 5가 아직 RFC(댓글 요청) 단계이므로 Vuex5 출시에 대비하고 싶습니다. , 그러나 아직 사용할 수 없습니다. 이 옵션의 작동하는 버전이 없습니다. 🎜🎜자세히 살펴보겠습니다... 🎜

🎜Factory🎜🎜#🎜🎜

🎜🎜Note🎜: 🎜이 부분의 코드는 다음 위치에 있습니다. 🎜의 "공장" 분기의 샘플 프로젝트. 🎜🎜🎜팩토리 패턴은 사용자가 생성한 상태의 인스턴스에만 관심을 갖습니다. 이 모드에서는 Composition API의 🎜start🎜 함수와 매우 유사한 함수를 반환합니다. 범위를 만들고 원하는 구성 요소를 빌드합니다. 예: 🎜
// In Home.vue
  const { books, loadBooks } = BookFactory();

  books = []; // Error, books is defined as const
🎜 필요한 객체의 일부를 생성하도록 공장에 요청할 수 있습니다. 🎜
export default reactive({

  books: new Array<Work>(),
  isBusy: false,

  async loadBooks() {
    this.isBusy = true;
    const response = await bookService.getBooks(this.currentTopic, this.currentPage);
    if (response.status === 200) {
      this.books.splice(0, this.books.length, ...response.data.works);
    }
    this.isBusy = false;
  }
});
🎜 네트워크 요청이 발생할 때 표시하기 위해 isBusy 태그를 추가하면 위 코드는 그렇지 않습니다. 변경하지만 isBusy를 표시할 위치를 결정할 수 있습니다. 🎜
// Home.vue
import state from "@/state";

export default defineComponent({
  setup() {

    // ...

    onMounted(async () => {
      if (state.books.length === 0) state.loadBooks();
    });

    return {
      state,
      bookTopics,
    };
  },
});
🎜다른 보기(vue?)에서는 공장의 다른 부분을 알지 못한 채 isBusy 태그만 지정할 수 있습니다. 작품:🎜
// App.vue
export default defineComponent({
  setup() {
    const { isBusy } = BookFactory();
    return {
      isBusy
    }
  },
})

但是你可能注意到了一个问题;每次我们调用工厂时,我们都会获得所有对象的新实例。有时,你希望工厂返回新实例,但在我们的例子中,我们讨论的是共享状态,因此我们需要将创建移到工厂之外:

const books: Work[] = reactive([]);
const isBusy = ref(false);

async function loadBooks(val: string) {
  isBusy.value = true;
  const response = await bookService.getBooks(val, currentPage.value);
  if (response.status === 200) {
    books.splice(0, books.length, ...response.data.works);
  }
}

export default function () {
 return {
    loadBooks,
    books,
    isBusy
  };
}

现在,工厂给了我们一个共享实例,或者一个单例。虽然这种模式可以工作,但返回一个每次都不创建新实例的函数可能会令人困惑。

因为底层对象被标记为const,所以不能替换它们(也不能破坏单例的性质)。所以这段代码应该会报错:

// In Home.vue
  const { books, loadBooks } = BookFactory();

  books = []; // Error, books is defined as const

因此,确保可变状态能够被更新是很重要的(例如,使用books.splice()而不是设置books)。另一种处理方法是使用共享实例

共享实例 #

本节的代码在GitHub上示例项目的“Sharedstate”分支中。

如果你使用共享状态,最好清楚状态是一个单例的事实。在本例中,它可以作为一个静态对象导入。例如,我喜欢创建一个可以作为响应式对象导入的对象:

export default reactive({

  books: new Array<Work>(),
  isBusy: false,

  async loadBooks() {
    this.isBusy = true;
    const response = await bookService.getBooks(this.currentTopic, this.currentPage);
    if (response.status === 200) {
      this.books.splice(0, this.books.length, ...response.data.works);
    }
    this.isBusy = false;
  }
});

在这种情况下,你只需导入对象(在本例中我将其称为商店):

// Home.vue
import state from "@/state";

export default defineComponent({
  setup() {

    // ...

    onMounted(async () => {
      if (state.books.length === 0) state.loadBooks();
    });

    return {
      state,
      bookTopics,
    };
  },
});

然后很容易绑定到状态:

<!-- Home.vue -->
<div class="grid grid-cols-4">
  <div
    v-for="book in state.books"
    :key="book.key"
    class="border bg-white border-grey-500 m-1 p-1"
  >
  <router-link :to="{ name: &#39;book&#39;, params: { id: book.key } }">
    <BookInfo :book="book" />
  </router-link>
</div>

与其他模式一样,你可以在视图之间共享此实例:

// App.vue
import state from "@/state";

export default defineComponent({
  setup() {
    return {
      state
    };
  },
})

然后它可以绑定到同一个对象(无论它是Home.Vue的父页面)或路由到其他页面):

<!-- App.vue -->
  <div class="container mx-auto bg-gray-100 p-1">
    <router-link to="/"><h1>Bookcase</h1></router-link>
    <div class="alert bg-gray-200 text-gray-900"   
         v-if="state.isBusy">Loading...</div>
    <router-view :key="$route.fullPath"></router-view>
  </div>

无论使用工厂模式还是共享实例,它们都有一个共同的问题:可变状态。当你不希望绑定或代码更改状态时,可能会产生意外的副作用。在本文中我使用简单示例中,它还没有那么复杂,因此不用担心。但是当你构建的应用程序越大越大时,将需要更仔细地考虑状态的变化。这时候Vuex就可以伸出援手了。

VUEX 4 #

本节的代码位于GitHub上示例项目的“Vuex4”分支中。

Vuex是vue的状态管理器。它是由核心团队构建的,但它作为一个单独的项目进行管理。Vuex的目的是将状态与状态执行操作分离开来。所有状态更改都必须通过 Vuex,这意味着它更复杂,但你可以防止意外状态更改。

Vuex 的想法是提供可预测的状态管理流程。视图流向 Actions,Actions 反过来使用 Mutations 来改变状态,从而更新视图。通过限制状态更改的流程,可以减少改变应用程序状态的副作用;因此更容易构建更大的应用程序。Vuex 有一个学习曲线,但有了这种复杂性,你就可以获得可预测性。

此外,Vuex 还支持程序调试(通过 Vue Tools)来处理状态管理,包括一个称为time-travel的功能。这允许你查看状态的历史记录并前后移动以查看它如何影响应用程序的。

有时,Vuex 也很重要。

要将其添加到你的 Vue 3 项目中,你可以将包添加到项目中:

> npm i vuex

或者,你也可以使用 Vue CLI 添加它:

> vue add vuex

通过使用CLI,它将为你的 Vuex  Store 创建初始的起点, 否则你需要手动将其接入到项目。让我们来看看这是如何工作的。

首先,你需要使用 Vuex 的 createStore 函数创建一个状态对象:

import { createStore } from &#39;vuex&#39;

export default createStore({
  state: {},
  mutations: {},
  actions: {},
  getters: {}
});

如你所见,Store需要定义多个属性。State只是一个想要授予应用程序访问权限的数据列表:

import { createStore } from &#39;vuex&#39;

export default createStore({
  state: {
    books: [],
    isBusy: false
  },
  mutations: {},
  actions: {}
});

注意state不应使用ref or reactive 包装。这里的数据与我们在共享实例或工厂中使用的共享数据类型相同。这个store在你的应用程序中将作为一个单例,因此状态中的数据也会被共享

然后,让我们看看actions。 Actions是可以授予你改变state中数据的一个操作台,举个例子:

  actions: {
    async loadBooks(store) {
      const response = await bookService.getBooks(store.state.currentTopic,
      if (response.status === 200) {
        // ...
      }
    }
  },

Actions 会传递一个 store 的实例,以便你可以获取状态和其他操作。通常,我们只会解构我们需要的部分:

  actions: {
    async loadBooks({ state }) {
      const response = await bookService.getBooks(state.currentTopic,
      if (response.status === 200) {
        // ...
      }
    }
  },

最后一个是Mutations。Mutations是能够改变状态的函数。只有Mutations才能影响状态。在这个例子中,我们需要Mutations来改变状态:

  mutations: {
    setBusy: (state) => state.isBusy = true,
    clearBusy: (state) => state.isBusy = false,
    setBooks(state, books) {
      state.books.splice(0, state.books.length, ...books);
    }
 },

Mutation函数总是传入状态对象,以便你可以改变状态。在前两个示例中,你可以看到我们显式地设置了状态。但在第三个例子中,我们传入了set的状态。调用mutation时,mutation总是接收两个参数:state和实参。

要调用mutation, 你需要在store中使用commit函数。在我们的例子中,我只是将它添加到解构中:

  actions: {
    async loadBooks({ state, commit }) {
      commit("setBusy");
      const response = await bookService.getBooks(state.currentTopic, 
      if (response.status === 200) {
        commit("setBooks", response.data);
      }
      commit("clearBusy");
    }
  },

你将在此处看到commit如何要求action的命名。有一些技巧使它不仅仅是有魔力的字符串。但是我现在跳过它们。这种有魔力字符串是限制Vuex的使用方式之一。

虽然使用 commit 可能看起来像是一个不必要的包装器,但请记住,Vuex 不会让你改变状态,除非在mutation内部,因此只有通过commit调用才会。

你还可以看到对setBooks的调用采用了第二个参数。这是调用mutation的第二个参数。如果你需要更多信息,则需要将其打包成一个参数(目前 Vuex 的另一个限制)。假设你需要将一本书插入到图书列表中,你可以这样称呼它:

commit("insertBook", { book, place: 4 }); // object, tuple, etc.

然后你可以解构成你需要的部分:

mutations: {
  insertBook(state, { book, place }) => // ...    
}

这优雅吗?并不是,但它有效。

现在我们的 action 已经处理了mutations,我们需要能够在我们的代码中使用 Vuex 存储。有两种方法可以得到store。首先,通过使用应用程序(例如 main.ts/js)注册store,你将可以访问一个集中式store, 你可以在应用程序的任何地方访问它:

// main.ts
import store from &#39;./store&#39;

createApp(App)
  .use(store)
  .use(router)
  .mount(&#39;#app&#39;)

注意,这并不是在添加Vuex,而是你实际上正在创建的商店。一旦添加后, 你只需要调用useStore即可获取store对象:

import { useStore } from "vuex";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const store = useStore();
    const books = computed(() => store.state.books);
    // ...

这很好用,但我更喜欢直接导入store:

import store from "@/store";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const books = computed(() => store.state.books);
    // ...

既然你可以访问 store 对象,那么如何使用它呢?对于state,你需要使用计算函数包装它们,以便将它们更改并传递到你的绑定上:

export default defineComponent({
  setup() {

    const books = computed(() => store.state.books);

    return {
      books
    };
  },
});

要调用actions,你需要调用dispatch 方法:

export default defineComponent({
  setup() {

    const books = computed(() => store.state.books);

    onMounted(async () => await store.dispatch("loadBooks"));

    return {
      books
    };
  },
});

Actions可以在方法名称后添加的参数。最后,要更改状态,你需要像我们在 Actions 中所做的那样去调用 commit。例如,我在 store 中有一个 分页属性,然后我可以使用commit更改状态:

const incrementPage = () =>
  store.commit("setPage", store.state.currentPage + 1);
const decrementPage = () =>
  store.commit("setPage", store.state.currentPage - 1);

注意, 这样调用可能会抛出一个异常(因为你不能手动改变state):

const incrementPage = () => store.state.currentPage++;
  const decrementPage = () => store.state.currentPage--;

这是Vuex真正强大的地方,我们想要控制状态改变的地方并且不会造成在开发过程中产生进一步错误。

你可能对 Vuex 中大量改变状态的方式感到不知所措,但它确实可以帮助管理更大、更复杂的项目中的状态。我不会说你在每种情况下都需要它,但是会有一些大型项目在总体上对你有帮助。

这就是 Vuex 5 旨在简化 Vuex 在 TypeScript(以及一般的 JavaScript 项目)中的工作方式的地方。让我们看看它在下一次发布后将如何运作。

VUEX 5 #

注意: 此部分的代码位于GitHub 上示例项目的“Vuex5”分支中。

在撰写本文时,Vuex 5 还没有出现。这是一个 RFC(征求意见)。这是计划。是讨论的起点。所以我在这里说明的很多东西可能会发生变化。但是为了让你们对 Vuex 的变化有所准备,我想让你们了解一下它的发展方向。因此,与此示例关联的代码不会生成。

Vuex工作原理的基本概念从一开始就没有改变过。随着Vue 3的引入,Vuex 4的创建主要允许Vuex在新项目中工作。但该团队正试图找到Vuex的真正痛点并解决它们。为此,他们正在计划一些重要的改变:

  • 没有更多的mutations: actions能改变mutation (可能任意的也可以)。
  • 更好的TypeScript支持。
  • 更好的多store功能。

那么它是如何工作的呢?让我们从创建store开始:

export default createStore({
  key: &#39;bookStore&#39;,
  state: () => ({
    isBusy: false,
    books: new Array<Work>()
  }),
  actions: {
    async loadBooks() {
      try {
        this.isBusy = true;
        const response = await bookService.getBooks();
        if (response.status === 200) {
          this.books = response.data.works;
        }
      } finally {
        this.isBusy = false;
      }
    }
  },
  getters: {
    findBook(key: string): Work | undefined {
      return this.books.find(b => b.key === key);
    }
  }
});

首先要看到的是,每个store现在都需要有自己的key。这允许你检索多个store。接下来,你会注意到state对象现在是一个工厂(例如,从一个函数返回,不是在解析时创建的)。没有mutations部分了。最后,在actions内部,你可以看到我们访问的状态只是this指向的属性。不需要再通过传递state来commit Actions。这不仅有助于简化开发,还使TypeScript更容易推断类型。

要将 Vuex 注册到你的应用程序中,你将注册 Vuex 而不是你的全局store:

import { createVuex } from &#39;vuex&#39;

createApp(App)
  .use(createVuex())
  .use(router)
  .mount(&#39;#app&#39;)

最后,要使用store,你将导入store然后创建它的一个实例:

import bookStore from "@/store";

export default defineComponent({
  components: {
    BookInfo,
  },
  setup() {
    const store = bookStore(); // Generate the wrapper
    // ...

请注意,从商店返回的是一个工厂对象,无论你调用工厂多少次,它都会返回该商店的实例。返回只是一个具有单一类行为的actions、state和getters对象(带有类型信息):

onMounted(async () => await store.loadBooks());

const incrementPage = () => store.currentPage++;
const decrementPage = () => store.currentPage--;

你将在此处看到 state(例如currentPage)只是简单的属性。而actions(例如loadBooks)只是函数。实际上你在这里使用的store是一个副作用。你可以将 Vuex 对象视为一个对象并继续工作。这是 API 的重大改进。

另一个需要指出的重要变化是,你也可以使用类似于Composition API的语法来生成你的store:

export default defineStore("another", () => {

  // State
  const isBusy = ref(false);
  const books = reactive(new Array≷Work>());

  // Actions
  async function loadBooks() {
    try {
      this.isBusy = true;
      const response = await bookService.getBooks(this.currentTopic, this.currentPage);
      if (response.status === 200) {
        this.books = response.data.works;
      }
    } finally {
      this.isBusy = false;
    }
  }

  findBook(key: string): Work | undefined {
    return this.books.find(b => b.key === key);
  }

  // Getters
  const bookCount = computed(() => this.books.length);

  return {
    isBusy,
    books,
    loadBooks,
    findBook,
    bookCount
  }
});

这允许你像使用 Composition API 构建视图那样去构建 Vuex 对象,并且可以说它更简单。

这种新设计的一个主要缺点是失去了状态的不可变性。围绕能够启用此功能(仅用于开发,就像 Vuex 4 一样)进行了讨论,但对此的重要性尚未达成共识。我个人认为这是 Vuex 的主要有点,但我们必须看看它是如何发挥作用的。

我们在哪里?#

在单页面应用程序中管理共享状态是大多数应用程序开发的关键部分。在设计解决方案时,制定一个关于如何使用Vue的策略是非常重要的一步。在本文中,我向你展示了几种管理共享状态的模式,包括即将在Vuex 5中推出的模式。希望你现在拥有为自己的项目做出正确决策的知识。

原文地址:https://www.smashingmagazine.com/2021/06/managing-shared-state-vue3/

原文作者:Shawn Wildermuth

更多编程相关知识,请访问:编程入门!!

위 내용은 Vue 3에서 공유 상태를 관리하는 방법에 대한 심층 분석? 【번역하다】의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제