Code前端首页关于Code前端联系我们

或者yarn add pinia

terry 2周前 (09-29) 阅读数 35 #Vue
文章标签 yarn pinia

不少刚接触Vue3的同学,一提到「Store」就犯懵——这东西到底是干啥的?和之前Vue2里的Vuex有啥区别?现在项目里该选Pinia还是Vuex?别慌,这篇文章把Vue3 Store从基础概念到实战细节,再到避坑技巧全拆明白,不管你是新手入门还是想优化项目状态管理,看完心里都有数~

Vue3里说的“Store”,到底指啥?

很多同学看到“Store”第一反应是“状态管理工具”,但Vue3生态里,Store已经从原来的Vuex为主,变成官方推荐Pinia作为首选方案了,简单说,Store是用来集中管理跨组件/页面共享数据的地方。

为啥需要它?比如电商项目里,用户的购物车数据、登录状态,这些信息可能在首页、商品页、结算页都要用,如果每个组件自己存一份,不仅重复代码,数据同步也容易乱,Store就像个“全局数据管家”,把这些共享状态统一存起来,谁要用就来取,改数据也有统一的规则。

Pinia和Vuex的关系得拎清:Vuex是Vue2时代的官方状态管理库,而Pinia可以理解为Vuex的“进化版”——它由Vuex核心团队成员开发,更轻量、API更简洁,还天生适配Vue3的Composition API,现在Vue3项目里,除非维护老项目,否则优先选Pinia~

为啥Vue3官方推荐Pinia,而不是继续用Vuex?

这得从开发体验、性能、生态适配这几点唠:

  • API更简单,学习成本低:Vuex里有State、Mutation、Action、Getter、Module这些概念,还得处理命名空间;Pinia直接把Mutation砍掉了,用Actions既能同步也能异步改状态,代码少了一大截,比如修改状态,Vuex得写commit('mutationName'),Pinia里直接store.count++或者在actions里改,更直观。
  • 对Composition API更友好:Vue3主推Composition API,Pinia的defineStore可以和setup语法无缝配合,你可以把Store里的逻辑拆成组合式函数,复用性拉满。
  • Tree-shaking友好,体积更小:Pinia的代码是按需打包的,没用的功能不会被打包进项目,比Vuex轻量很多,对于追求性能的项目,这点太香了。
  • TypeScript支持拉满:写TypeScript项目时,Pinia的类型推导几乎“零配置”,定义State时能自动推断类型;Vuex想做好类型提示,得写一堆额外代码,很麻烦。

从零开始,怎么在Vue3项目里搭Store?

实战步骤走一遍,你跟着做就会了~

第一步:安装Pinia

打开终端,进项目目录,执行:

npm install pinia```  
#### 第二步:创建Store实例并挂载  
在项目的入口文件(main.js`)里,引入并注册Pinia:  
```js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const store = createPinia() // 创建Pinia实例
app.use(store) // 挂载到Vue应用
app.mount('#app')

第三步:定义第一个Store模块

新建一个文件(比如src/store/counter.js),用defineStore定义Store:

import { defineStore } from 'pinia'
// defineStore的第一个参数是唯一ID,保证整个应用里不重复
export const useCounterStore = defineStore('counter', {
  // state:存共享数据
  state: () => ({
    count: 0,
    userInfo: { name: '默认名', age: 18 }
  }),
  // getters:对state做计算(类似组件的computed)
  getters: {
    doubleCount: (state) => state.count * 2,
    // 也能通过this访问state,但要注意类型推导(加return)
    userGreet: function() {
      return `你好,${this.userInfo.name}`
    }
  },
  // actions:处理同步/异步逻辑,修改state
  actions: {
    increment() {
      this.count++
    },
    // 异步示例:模拟请求数据
    async fetchUserInfo() {
      const res = await fetch('https://xxx/api/user')
      const data = await res.json()
      this.userInfo = data // 直接修改state
    }
  }
})

第四步:在组件里用Store

在Vue组件的setup中,用useStore函数拿到Store实例:

<template>
  <div>
    <p>当前计数:{{ store.count }}</p>
    <p>双倍计数:{{ store.doubleCount }}</p>
    <button @click="store.increment">+1</button>
    <button @click="fetchUser">获取用户信息</button>
  </div>
</template>
<script setup>
import { useCounterStore } from '../store/counter.js'
const store = useCounterStore() // 拿到Store实例
const fetchUser = () => {
  store.fetchUserInfo() // 调用actions里的异步方法
}
</script>

Store里的State,修改时有啥讲究?

State是Store存数据的地方,但修改方式得注意“响应式”和“逻辑内聚”

直接修改VS Actions里改?

Pinia里允许直接修改state(比如store.count++),但如果是复杂逻辑(比如异步请求后改数据,或者多个state联动),建议把逻辑放到actions里,这么做有两个好处:

  • 代码可维护:所有修改逻辑集中在actions,后期改需求时,不用满组件找哪里改了state;
  • 调试友好:用Pinia DevTools(Chrome插件)能看到actions的调用记录,直接改state的话,调试时不好追踪。

响应式怎么保证?

Vue的响应式靠的是Proxy,所以修改state时,要遵循“响应式规则”:

  • 直接改对象属性:store.userInfo.name = '新名字' ✔️(因为userInfo是响应式对象,改属性会触发更新);

  • 直接替换整个对象/数组:store.userInfo = { name: '新', age: 20 } ✔️(Pinia会自动处理响应式);

  • 但如果是给对象新增属性,得用$patch(类似Vuex的patch):

    // 错误示例:直接新增属性不会触发响应式
    store.userInfo.gender = '男' 
    // 正确做法:用$patch
    store.$patch({
      userInfo: { ...store.userInfo, gender: '男' }
    })
    // 或者用函数式$patch(适合复杂修改)
    store.$patch((state) => {
      state.userInfo.gender = '男'
      state.count += 1
    })

怎么重置State?

有时候需要把state恢复成初始值(比如用户登出后清空数据),Pinia给每个Store实例提供了$reset方法:

const store = useCounterStore()
store.$reset() // 执行后,count回到0,userInfo回到初始的{ name: '默认名', age: 18 }

Getters在Store里扮演啥角色?怎么写更顺手?

Getters就像Store的“计算属性”,用来基于State生成新数据,而且会缓存结果(只有依赖的State变了,才会重新计算)。

基础用法:依赖State

比如前面的doubleCount,依赖count,所以count变了,doubleCount才会重新计算:

getters: {
  doubleCount: (state) => state.count * 2
}

进阶:依赖其他Getters

Getters里可以通过this访问其他Getters(注意用普通函数,别用箭头函数,否则this指向不对):

getters: {
  doubleCount: (state) => state.count * 2,
  tripleCount() {
    return this.doubleCount + this.count // 依赖doubleCount和count
  }
}

给Getters传参?

Getters本身是函数,但直接返回函数的话,会失去缓存(因为每次调用都算新函数),如果需要传参,建议封装成“带参数的Getters”:

getters: {
  getUserByAge() {
    return (targetAge) => {
      // 假设state里有users数组:[{name: 'A', age: 20}, {name: 'B', age: 25}]
      return this.users.filter(user => user.age === targetAge)
    }
  }
}
// 组件里用:
store.getUserByAge(20) // 拿到age为20的用户

这种写法虽然没缓存,但适合需要动态筛选数据的场景~

Actions和Mutation有啥区别?Vue3里咋用Actions?

Vuex里Mutation是同步改State,Action是异步+提交Mutation;但Pinia里没有Mutation,Actions既可以同步也可以异步改State,相当于把两者的功能合并了。

同步Actions示例(替代Mutation)

actions: {
  increment() {
    this.count++ // 直接改state,同步操作
  }
}

异步Actions示例(处理网络请求)

actions: {
  async login(userInfo) {
    const res = await api.login(userInfo)
    if (res.code === 200) {
      this.user = res.data.user // 存用户信息到state
      this.isLogin = true // 改登录状态
    }
  }
}

为啥Pinia要去掉Mutation?

核心原因是减少概念复杂度,Mutation存在的意义是“强制同步改State,方便调试”,但实际开发中,很多人觉得写Mutation+Action太繁琐,Pinia通过Actions同时支持同步/异步,还能在DevTools里追踪操作,既简化了API,又没丢调试能力~

项目大了,多个Store模块咋组织?

当项目有用户模块、购物车模块、商品模块时,每个模块单独写一个defineStore,就是天然的“模块划分”

示例:用户模块和购物车模块

src/store/user.js

export const useUserStore = defineStore('user', {
  state: () => ({ token: '', userInfo: {} }),
  actions: { async login() {} }
})

src/store/cart.js

import { useUserStore } from './user.js'
export const useCartStore = defineStore('cart', {
  state: () => ({ goodsList: [] }),
  actions: { 
    async addGoods(goodsId) {
      const userStore = useUserStore()
      if (!userStore.token) {
        await userStore.login() // 调用user模块的登录方法
      }
      // 然后发请求添加商品到购物车
    }
  }
})

这种“模块间通过useStore调用”的方式,比Vuex的模块命名空间简单太多,不用再纠结rootStatenamespace这些东西~

Store和Composition API咋结合更丝滑?

Vue3的Composition API强调“逻辑复用”,Store和它结合能玩出很多花样:

抽离Store逻辑到组合式函数

比如把“用户登录、登出、获取信息”的逻辑抽成useUserLogic.js

import { useUserStore } from '../store/user.js'
import { onMounted } from 'vue'
export function useUserLogic() {
  const userStore = useUserStore()
  // 封装登录方法
  const handleLogin = async (form) => {
    await userStore.login(form)
    // 登录成功后的其他逻辑,比如跳转页面
  }
  // 封装登出方法
  const handleLogout = () => {
    userStore.$reset() // 清空用户状态
    // 其他登出逻辑,比如清除token
  }
  // 组件挂载时自动获取用户信息
  onMounted(() => {
    if (userStore.token) {
      userStore.fetchUserInfo()
    }
  })
  return { handleLogin, handleLogout }
}

然后在组件里直接用:

<script setup>
import { useUserLogic } from '../composables/useUserLogic.js'
const { handleLogin, handleLogout } = useUserLogic()
</script>

这样组件里只关心“调用方法”,逻辑全在组合式函数里,复用性和可读性都提升了~

生产环境用Store,这些优化点要注意!

项目上线后,性能和可维护性很重要,这几个技巧能帮你少踩坑:

按需引入,利用Tree-shaking

Pinia本身支持Tree-shaking,但如果你的Store文件里有很多逻辑,建议按功能拆分Store(比如把用户、购物车、订单分成不同文件),这样打包时只会把用到的Store打包进去。

避免不必要的响应式

如果有些数据不需要响应式(比如纯工具类的配置信息),可以不用放State里,直接导出普通对象:

// 非响应式配置,直接导出
export const appConfig = {
  apiBaseUrl: 'https://xxx.com/api',
  theme: 'light'
}
// 组件里用:import { appConfig } from '../config.js'

用DevTools监控状态

Pinia官方有DevTools扩展(Chrome商店搜Vue DevTools,支持Pinia),能实时看State变化、Actions调用记录,生产环境前用它排查状态不同步的问题,效率翻倍~

这些Store常见错误,你踩过吗?

最后避坑环节,分享几个新手常犯的错:

错误1:State修改后页面不更新

原因:可能是直接修改了非响应式数据,或者用了箭头函数导致this指向错误。
解决:

  • 对象/数组新增属性用$patch
  • Getters和Actions里用普通函数,别用箭头函数(否则this不是Store实例)。

错误2:多个组件用Store,数据不同步

原因:没正确使用useStore,比如在setup外调用。
解决:必须在setup函数或组合式函数里调用useStore,因为Pinia是基于Vue的依赖注入,只有在组件上下文里才能拿到正确的Store实例。

错误3:TypeScript类型报错

原因:State的类型没正确推导,或者actions参数类型不对。
解决:

  • 定义State时,尽量用字面量初始化(比如state: () => ({ count: 0 })),让TypeScript自动推导;
  • 给actions的参数加类型注解:async login(userInfo: UserInfo) {}

写到这,Vue3 Store(Pinia)的核心知识点和实战技巧差不多覆盖全了,Store的本质是“管理共享状态”,别为了用而用——如果组件间数据传递用Props/Events、Provide/Inject能解决,就没必要上Store,但遇到复杂的跨组件状态同步、异步逻辑管理,Pinia绝对是高效利器~ 现在可以动手在自己项目里搭个Store试试,把登录状态、购物车这些场景练一遍,很快就能上手啦~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门