
状态管理是Web应用程序开发的基石之一;任何非平凡的应用程序都需要某种状态管理。多年来,Vuex 一直是 Vue 应用程序事实上的状态管理工具。
然而,新的Vue文档正式推荐了另一个工具:Pinia。但在你说“哦,不,不是另一个学习工具”之前,你应该知道Pinia是事实上的Vuex 5,正如Evan You在这条推文中写道:

在本教程中,我们将通过学习如何创建、使用和检查数据存储来检查 Pinia 最重要的功能,包括:
·Piniavs.Vuex
·使用基本的Pinia商店
·Pinia入门
·在Pinia中定义应用商店
·定义帖子存储
·定义注释存诸
·定义作者存诸
·在Pinia中创建视图和组件
·创建帖子视图
·创建单个帖子视图
·创建作者视图
·创建单个作者视图
·配置路由器
·检查Vue Devtools中的Pinia商店
在此过程中,我们将构建的项目将演示构建具有复杂状态的应用的基础知识。但首先,让我们看看Pinia与Vuex有何不同。
虽然Pinia可以被认为是Vuex 5,但您应该记住两者之间的一些重要区别:
Pinia API得到了最大的简化。以下是一个基本的Pinia商店的例子:
- import { defineStore } from 'pinia'
-
- export const useCounterStore = defineStore({
- id: 'counter',
- state: () => ({
- counter: 0
- }),
- getters: {
- doubleCount: (state) => state.counter * 2
- },
- actions: {
- increment() {
- this.counter++
- }
- }
- })
为了定义商店,我们使用函数。在这里,使用这个词而不是因为在组件/页面中实际使用商店之前不会创建存储。defineStore define create
商店名称的开头是跨可组合的约定。每个存储必须提供唯一的将存储装载到 devtools。use id
Pinia 还使用 、 和概念,它们等效于 、 和 组件:
state getters actions data computed methods
stategettersstateactions这几乎是定义Pinia商店需要知道的一切。在本教程的其余部分,我们将看到如何在组件/页面中实际使用存储。
在看到Pinia API是多么简单之后,让我们开始构建我们的项目。
为了演示Pinia的功能,我们将构建一个具有以下功能的基本博客引擎:
首先,让我们通过运行以下命令创建一个新的 Vue 项目:
npm init vue@latest
这将安装并执行官方的 Vue 项目脚手架工具,以使用 Vue 和 Vite 设置一个新项目。在此过程中,您必须选择项目所需的工具:create-vue

选择所有标有红色箭头的工具:Router、Pinia、ESLint 和 Prettier。安装完成后,导航到项目并安装依赖项:
- cd vue-project
- npm install
现在,您可以通过运行以下命令在浏览器中打开项目:
npm run dev
您的新 Vue 应用程序将在 http://localhost:3000。以下是您应该看到的内容:

现在,为了使其适应我们的需求,我们将清理默认项目结构。以下是它现在的外观以及我们将删除的内容。

为此,首先,关闭终端并删除红色边框内的所有文件/文件夹。
现在,我们已准备好开始编写项目代码。
让我们首先打开文件,看看 Pinia 根存储是如何创建并包含在项目中的:main.js
- import { createApp } from 'vue'
- import { createPinia } from 'pinia' // Import
-
- import App from './App.vue'
- import router from './router'
-
- const app = createApp(App)
-
- app.use(createPinia()) // Create the root store
- app.use(router)
-
- app.mount('#app')
如您所见,函数被导入,创建Pinia商店,并将其传递给应用程序。createPinia
现在,打开该文件并将其内容替换为以下内容:App.vue
- <script setup>
- import { RouterLink, RouterView } from 'vue-router'
- </script>
-
- <template>
- <header class="navbar">
- <div>
- <nav>
- <RouterLink to="/">Posts</RouterLink> -
- <RouterLink to="/authors">Authors</RouterLink>
- </nav>
- </div>
- </header>
-
- <RouterView />
- </template>
-
- <style>
- .navbar {
- background-color: lightgreen;
- padding: 1.2rem;
- }
- </style>
在这里,我们更改了链接标签,将主页替换为帖子,将“关于”替换为作者。
我们还将“作者”链接从 更改为 并删除了所有默认样式,并为该类添加了我们自己的样式,我们添加这些样式是为了将导航与帖子区分开来。/about/authorsnavbar
好了,现在我们准备更深入地研究Pinia并定义必要的应用商店。
对于我们的小型应用,我们将使用数据源和以下三个资源:、 和 。users posts comments
为了理解我们将如何更好地创建应用商店,让我们看看这些资源是如何相互关联的。请看下图:

如您所见,用户通过其 连接到帖子,并且帖子以相同的方式连接到评论。因此,要获取帖子的作者,我们可以使用 ,并且要获取帖子的评论,我们可以使用 。id userId postId
有了这些知识,我们就可以开始将数据映射到我们的商店。
我们将定义的第一个商店是博客文章。在目录中,重命名为以下内容并将其内容替换为以下内容:store scounter.js post.js
- import { defineStore } from 'pinia'
-
- export const usePostStore = defineStore({
- id: 'post',
- state: () => ({
- posts: [],
- post: null,
- loading: false,
- error: null
- }),
- getters: {
- getPostsPerAuthor: (state) => {
- return (authorId) => state.posts.filter((post) => post.userId === authorId)
- }
- },
- actions: {
- async fetchPosts() {
- this.posts = []
- this.loading = true
- try {
- this.posts = await fetch('https://xxx.com/posts')
- .then((response) => response.json())
- } catch (error) {
- this.error = error
- } finally {
- this.loading = false
- }
- },
- async fetchPost(id) {
- this.post = null
- this.loading = true
- try {
- this.post = await fetch(`https://xxx.com/posts/${id}`)
- .then((response) => response.json())
- } catch (error) {
- this.error = error
- } finally {
- this.loading = false
- }
- }
- }
- })
让我们把它分成小块,并解释发生了什么。首先,我们用 .usePostStore id post
其次,我们用四个属性来定义我们的:state
posts用于持有获取的帖子post担任现职loading用于保持加载状态error用于保存错误(如果存在此类错误)第三,我们创建一个 getter 来获取作者写了多少篇文章。默认情况下,getter 将 作为参数,并使用它来访问数组。Getters 不能接受自定义参数,但我们可以返回一个可以接收自定义参数的函数。stateposts
因此,在我们的 getter 函数中,我们进行筛选以查找具有特定用户 ID 的所有帖子。稍后在组件中使用该 ID 时,我们将提供该 ID。posts
但是,请注意,当我们返回一个带有来自 getter 的参数的函数时,getter 不再缓存。
最后,让我们创建两个异步操作来获取所有帖子和单个帖子。
如果存在错误,我们会将错误分配给 error 属性。最后,我们回到 .fetchPosts() posts loading true loading false
操作几乎相同,但这次我们使用属性并提供 一个来获取单个帖子;确保在获取帖子时使用反引号而不是单引号。fetchPost(id) post id
在这里,我们还重置了属性,因为如果我们不这样做,当前帖子将显示上一个帖子中的数据,并且新获取的帖子将分配给.post post
我们有帖子,现在是时候得到一些评论了。
在目录中,创建一个包含以下内容的文件:storescomment.js
- import { defineStore } from 'pinia'
- import { usePostStore } from './post'
-
- export const useCommentStore = defineStore({
- id: 'comment',
- state: () => ({
- comments: []
- }),
- getters: {
- getPostComments: (state) => {
- const postSore = usePostStore()
- return state.comments.filter((post) => post.postId === postSore.post.id)
- }
- },
- actions: {
- async fetchComments() {
- this.comments = await fetch('https://xxx.com/comments')
- .then((response) => response.json())
- }
- }
- })
在这里,我们在 中创建一个数组属性来保存获取的注释。我们在行动的帮助下获取它们。comments state fetchComments()
这里有趣的部分是获取者。要获取帖子的评论,我们需要当前帖子的 ID。既然我们已经在邮局商店里有它,我们可以从那里得到它吗?getPostComments
是的,幸运的是,Pinia允许我们在另一个商店使用一个商店,反之亦然。因此,为了获取帖子的 ID,我们导入并在 getter 中使用它。usePostStore getPostComments
好的,现在我们有评论了;最后一件事是获得作者。
在目录中,创建一个包含以下内容的文件:storesauthor.js
- import { defineStore } from 'pinia'
- import { usePostStore } from './post'
-
- export const useAuthorStore = defineStore({
- id: 'author',
- state: () => ({
- authors: []
- }),
- getters: {
- getPostAuthor: (state) => {
- const postStore = usePostStore()
- return state.authors.find((author) => author.id === postStore.post.userId)
- }
- },
- actions: {
- async fetchAuthors() {
- this.authors = await fetch('https://xxx.com/users')
- .then((response) => response.json())
- }
- }
- })
这与 完全相同。我们再次导入并使用它在 getter 中提供所需的作者 ID。commentStore usePostStore getPostAuthor
就是这样。您会看到使用Pinia创建商店是多么容易,这是一个简单而优雅的解决方案。
现在,让我们看看如何在实践中使用商店。
在本节中,我们将创建必要的视图和组件,以应用我们刚刚创建的 Pinia 商店。让我们从所有帖子的列表开始。
请注意,我将 Pinia 与 Composition API 和语法结合使用。如果要改用选项 API,请查看本指南。<script setup>
在目录中,重命名为以下内容并将其内容替换为以下内容:views HomeView.vue PostsView.vue
- <script setup>
- import { RouterLink } from 'vue-router'
- import { storeToRefs } from 'pinia'
- import { usePostStore } from '../stores/post'
-
- const { posts, loading, error } = storeToRefs(usePostStore())
- const { fetchPosts } = usePostStore()
-
- fetchPosts()
- </script>
-
- <template>
- <main>
- <p v-if="loading">Loading posts...</p>
- <p v-if="error">{{ error.message }}</p>
- <p v-if="posts" v-for="post in posts" :key="post.id">
- <RouterLink :to="`/post/${post.id}`">{{ post.title }}</RouterLink>
- <p>{{ post.body }}</p>
- </p>
- </main>
- </template>
请注意,如果您收到已重命名文件的通知,请忽略它。
在这里,我们从邮政商店导入并提取所有必要的数据。
我们不能对状态属性和 getter 使用解构,因为它们会失去反应性。为了解决这个问题,Pinia提供了实用程序,该实用程序为每个属性创建一个ref。可以直接提取操作,而不会出现问题。storeToRefs
我们打电话来获取帖子。当使用组合 API 并在函数内部调用函数时,它等效于使用 Hook。因此,我们将在组件装载之前拥有帖子。fetchPosts() setup() created()
我们在模板中还有一系列指令。首先,如果加载是 ,则显示加载消息。然后,如果发生错误,我们会显示错误消息。v-if true
最后,我们循环访问帖子,并为每个帖子显示标题和正文。我们使用该组件向标题添加链接,以便当用户单击它时,他们将导航到单个帖子视图,稍后我们将创建该视图。RouterLink
现在,让我们修改该文件。打开它并将其内容替换为以下内容:router.js
- import { createRouter, createWebHistory } from 'vue-router'
- import PostsView from '../views/PostsView.vue'
-
- const router = createRouter({
- history: createWebHistory(),
- routes: [
- {
- path: '/',
- name: 'posts',
- component: PostsView
- },
- {
- path: '/about',
- name: 'about',
- // route level code-splitting
- // this generates a separate chunk (About.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () => import('../views/AboutView.vue')
- }
- ]
- })
-
- export default router
在这里,我们导入 并将其用作第一个路由中的组件。我们还将名称从“主页”更改为“帖子”。PostsView.vue
测试帖子视图
好了,是时候测试我们到目前为止取得的成就了。运行应用 () 并在浏览器中查看结果:npm run dev

您可能会在控制台中收到一些 Vue 警告,以“未找到匹配项...”开头。这是因为我们尚未创建必要的组件,您可以安全地忽略它们。
如果帖子未显示,您可能还需要重新加载页面。
让我们继续创建单个帖子视图。关闭终端以避免任何不必要的错误消息。
在目录中,创建一个包含以下内容的文件:views PostView.vue
- <script setup>
- import { useRoute } from 'vue-router'
- import { storeToRefs } from 'pinia'
- import { useAuthorStore } from '../stores/author'
- import { usePostStore } from '../stores/post'
- import Post from '../components/Post.vue'
-
- const route = useRoute()
- const { getPostAuthor } = storeToRefs(useAuthorStore())
- const { fetchAuthors} = useAuthorStore()
- const { post, loading, error } = storeToRefs(usePostStore())
- const { fetchPost } = usePostStore()
-
- fetchAuthors()
- fetchPost(route.params.id)
- </script>
-
- <template>
- <div>
- <p v-if="loading">Loading post...</p>
- <p v-if="error">{{ error.message }}</p>
- <p v-if="post">
- <post :post="post" :author="getPostAuthor"></post>
- </p>
- </div>
- </template>
在设置中,我们从作者存储中提取和从后期存储中提取必要的数据。我们还呼吁获取现有作者。getPostAuthor fetchAuthors fetchAuthors()
接下来,我们使用对象帮助提供的 ID 调用操作。这将更新,我们可以在模板中有效地使用它。fetchPost(route.params.id) route getPostAuthor
为了提供实际的帖子,我们使用一个组件,该组件需要两个道具:和。现在,让我们创建组件。post post author
创建组件post
在目录中,创建一个包含以下内容的文件:components Post.vue
- <script setup>
- import { RouterLink } from 'vue-router'
- import { storeToRefs } from 'pinia'
- import { useCommentStore } from '../stores/comment'
- import Comment from '../components/Comment.vue'
-
- defineProps(['post', 'author'])
-
- const { getPostComments } = storeToRefs(useCommentStore())
- const { fetchComments } = useCommentStore()
-
- fetchComments()
- </script>
-
- <template>
- <div>
- <div>
- <h2>{{ post.title }}</h2>
- <p v-if="author">Written by: <RouterLink :to="`/author/${author.username}`">{{ author.name }}</RouterLink>
- | <span>Comments: {{ getPostComments.length }}</span>
- </p>
- <p>{{ post.body }}</p>
- </div>
- <hr>
- <h3>Comments:</h3>
- <comment :comments="getPostComments"></comment>
- </div>
- </template>
在这里,我们使用函数定义所需的 props,并从注释存储中提取必要的数据。然后,我们获取注释,以便可以正确更新。defineProps getPostComments
在模板中,我们首先显示帖子标题,然后在署名中,我们添加一个作者姓名,其中包含指向作者页面的链接和帖子中的评论数量。然后,我们在下面添加帖子正文和评论部分。
为了显示评论,我们将使用单独的组件,并将帖子评论传递给道具。comments
创建组件comment
在目录中,创建一个包含以下内容的文件:components Comment.vue
- <script setup>
- defineProps(['comments'])
- </script>
-
- <template>
- <div>
- <div v-for="comment in comments" :key="comment.id">
- <h3>{{ comment.name }}</h3>
- <p>{{ comment.body }}</p>
- </div>
- </div>
- </template>
这很简单。我们定义道具并使用它来迭代帖子的评论。comments
在我们再次测试应用程序之前,请将以下内容添加到:router.js
- import PostView from '../views/PostView.vue'
- // ...
- routes: [
- // ...
- { path: '/post/:id', name: 'post', component: PostView },
- ]
再次运行应用。当您导航到单个帖子时,您应该会看到类似的视图:

现在是时候显示作者了。再次关闭终端。
在目录中,将文件重命名为以下内容,并将内容替换为以下内容:views AboutView.vue AuthorsView.vue
- <script setup>
- import { RouterLink } from 'vue-router'
- import { storeToRefs } from 'pinia'
- import { useAuthorStore } from '../stores/author'
-
- const { authors } = storeToRefs(useAuthorStore())
- const { fetchAuthors } = useAuthorStore()
-
- fetchAuthors()
- </script>
-
- <template>
- <div>
- <p v-if="authors" v-for="author in authors" :key="author.id">
- <RouterLink :to="`/author/${author.username}`">{{ author.name }}</RouterLink>
- </p>
- </div>
- </template>
在这里,我们使用作者存储来获取并让作者在模板中循环访问它们。对于每个作者,我们都会提供指向其页面的链接。
再次打开文件,并将“关于”页面的路由更改为以下内容:router.js
- {
- path: '/authors',
- name: 'authors',
- // route level code-splitting
- // this generates a separate chunk (About.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () => import('../views/AuthorsView.vue')
- },
在这里,我们将路径和名称分别更改为 和 ,并导入延迟加载。/authors authors AuthorsView.vue
再次运行应用。访问作者视图时,应会看到以下内容:

现在,让我们创建单个作者视图。再次关闭终端。
在目录中,创建一个包含以下内容的文件:views AuthorView.vue
- <script setup>
- import { computed } from 'vue'
- import { useRoute } from 'vue-router'
- import { storeToRefs } from 'pinia'
- import { useAuthorStore } from '../stores/author'
- import { usePostStore } from '../stores/post'
- import Author from '../components/Author.vue'
-
- const route = useRoute()
- const { authors } = storeToRefs(useAuthorStore())
- const { getPostsPerAuthor } = storeToRefs(usePostStore())
- const { fetchPosts } = usePostStore()
-
- const getAuthorByUserName = computed(() => {
- return authors.value.find((author) => author.username === route.params.username)
- })
-
- fetchPosts()
- </script>
-
- <template>
- <div>
- <author
- :author="getAuthorByUserName"
- :posts="getPostsPerAuthor(getAuthorByUserName.id)">
- </author>
- </div>
- </template>
在这里,为了找到当前作者是谁,我们使用他们的用户名从路由中获取它。因此,我们为此目的创建了一个计算;我们将和 props 传递给一个组件,我们现在将创建该组件。getAuthorByUserName author posts author
创建组件author
在目录中,创建包含以下内容的文件:components Author.vue
- <script setup>
- import { RouterLink } from 'vue-router'
-
- defineProps(['author', 'posts'])
- </script>
-
- <template>
- <div>
- <h1>{{author.name}}</h1>
- <p>{{posts.length}} posts written.</p>
- <p v-for="post in posts" :key="post.id">
- <RouterLink :to="`/post/${post.id}`">{{ post.title }}</RouterLink>
- </p>
- </div>
- </template>
此组件显示作者姓名、作者撰写的帖子数以及帖子本身。
接下来,将以下内容添加到文件中:router.js
- import AuthorView from '../views/AuthorView.vue'
- // ...
- routes: [
- // ...
- { path: '/author/:username', name: 'author', component: AuthorView }
- ]
再次运行应用。转到作者视图时,应看到以下内容:

以下是最终文件的外观:router.js
- import { createRouter, createWebHistory } from 'vue-router'
- import PostsView from '../views/PostsView.vue'
- import PostView from '../views/PostView.vue'
- import AuthorView from '../views/AuthorView.vue'
-
- const router = createRouter({
- history: createWebHistory(),
- routes: [
- {
- path: '/',
- name: 'posts',
- component: PostsView
- },
- {
- path: '/authors',
- name: 'authors',
- // route level code-splitting
- // this generates a separate chunk (About.[hash].js) for this route
- // which is lazy-loaded when the route is visited.
- component: () => import('../views/AuthorsView.vue')
- },
- { path: '/post/:id', name: 'post', component: PostView },
- { path: '/author/:username', name: 'author', component: AuthorView },
- ]
- })
-
- export default router
现在,所有关于缺少资源/组件的 Vue 警告都应该消失了。
就是这样。我们成功地在一个相当复杂的应用程序中创建并使用了Pinia商店。
最后,让我们看看如何在 Vue devtools 中检查应用程序。
在接下来的屏幕截图中,我们打开了一个ID为2的帖子。以下是应用程序的路由在“路由”选项卡中的列出方式:

我们可以看到,我们创建的所有路由都在这里,并且单个帖子的路由处于活动状态,因为它当前正在使用中。
现在,让我们切换到“组件”选项卡,以便我们可以浏览帖子视图的应用程序组件树:

正如我们所看到的,该应用程序从两个组件开始,并在 中定义组件。然后,我们有单个帖子视图,后跟组件。最后,还有另一个注释组件。RouretLink RouterView App.vue post RouterLink
现在让我们看看商店,这是有趣的部分。Pinia 显示活动组件中使用的所有存储区。在我们的例子中,我们拥有所有三个,因为我们在打开单个帖子时都使用它们。
这是邮政商店:

我们可以看到Pinia显示了正确的打开的帖子。对于作者存储也是如此:

最后,注释存储显示注释:

同样,我们可以看到第一个注释的名称与浏览器中显示的名称匹配。所以,一切都如预期的那样工作。
现在,您知道了如何创建、使用和检查 Pinia 商店。
我对新的官方Vue状态管理工具非常满意。正如我们所看到的,它采用模块化设计,易于使用,占地面积小,最后但并非最不重要的是,它简单,灵活且功能强大。与Pinia一起创建商店真的很愉快。
在本教程中,我们构建了一个基本的博客引擎,其中包含Pinia提供的主要功能(状态,getters和操作)。当然,可以通过为作者、帖子和评论添加 CRUD 功能来进一步扩展该项目,但这超出了本教程的范围。
有关 Pinia 用法的更复杂和实际示例,您可以浏览 Directus 项目的代码。
最后,请务必查看Pinia文档,以了解更高级的使用方法。
调试 Vue.js应用程序可能很困难,尤其是在用户会话期间有几十个甚至数百个突变时。如果你有兴趣在生产环境中监控和跟踪所有用户的 Vue 突变,请尝试 LogRocket。

LogRocket就像一个用于Web和移动应用程序的DVR,记录Vue应用程序中发生的所有事情,包括网络请求,JavaScript错误,性能问题等等。您不必猜测问题发生的原因,而是可以汇总并报告问题发生时应用程序所处的状态。
LogRocket Vuex 插件将 Vuex 突变记录到 LogRocket 控制台,为您提供导致错误的原因以及问题发生时应用程序所处的状态的上下文。
现代化您的 Vue 应用程序调试方式 - 开始免费监控。
资料: