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

Pinia是啥?为啥Vue3推荐用它代替Vuex?

terry 2小时前 阅读数 5 #Vue
文章标签 Pinia;Vuex

现在Vue3成了前端项目的主流框架,状态管理这块很多人从Vuex转向了Pinia,为啥选Pinia?Vue3里咋上手Pinia?不同场景下怎么用它解决状态共享问题?这篇文章用问答形式把Pinia的关键知识点和实战技巧拆明白,不管是刚接触的新手还是想优化项目的开发者,看完能少走不少弯路。

Pinia是Vue生态里的**状态管理库**,专门给Vue.js(尤其是Vue3)做响应式状态共享用的,它和Vuex关系很有意思——俩项目作者是同一个人,Pinia可以理解成“Vuex的下一代”,解决了Vuex之前被吐槽的痛点:
  • 代码更简洁:Vuex里有state、mutation、action、getter、module这些概念,Pinia直接砍掉mutation(修改状态不用再区分同步异步,action里想咋改咋改),只保留state、getter、action,写代码时少了很多“仪式感”。
  • 对Composition API更友好:Vue3主推组合式API,Pinia的API设计和setup语法天然契合,用起来像写组件逻辑一样顺。
  • TS支持拉满:现在项目基本都用TypeScript,Pinia在类型推导上做了优化,定义Store时能自动推断类型,不用像Vuex那样写一堆泛型声明,对TS新手友好太多。
  • 体积更小,性能更好:Pinia打包后体积比Vuex小很多,内部用Vue3的reactivecomputed做响应式,性能和组件内状态管理一致,没有额外性能开销。

Vue官方文档里也能看到,现在更推荐用Pinia做状态管理,Vuex虽然还维护,但新项目优先选Pinia是趋势。

怎么在Vue3项目里装Pinia并初始化?

分两步:安装包 + 挂载到Vue应用里。

安装Pinia

用包管理器装,比如npm:

npm install pinia

yarn或pnpm也一样,换命令就行。

在main.js里初始化

Vue3的项目入口一般是main.js(或main.ts),要创建Pinia实例,然后用app.use()挂载:

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
// 创建Pinia实例
const pinia = createPinia()
// 挂载到Vue应用
app.use(pinia)
app.mount('#app')

这一步做完,整个项目就有了全局的状态管理容器,接下来就能定义各种Store了。

怎么定义第一个Store?

Store是Pinia里的“状态模块”,可以理解成一个独立的逻辑单元,把状态、计算逻辑、方法包在一起,用defineStore函数定义,步骤如下:

创建Store文件

一般在src/store目录下新建文件,比如counterStore.js(TS项目用.ts)。

用defineStore定义结构

defineStore需要两个参数:唯一ID(整个应用里不能重复)、配置对象(包含state、getters、actions)。

举个“计数器”的例子:

// src/store/counterStore.js
import { defineStore } from 'pinia'
// 定义并导出Store
export const useCounterStore = defineStore('counter', {
  // state:存数据,返回对象(和Vuex的state类似)
  state: () => ({
    count: 0
  }),
  // getters:计算属性,依赖state,和组件的computed一样
  getters: {
    doubleCount: (state) => state.count * 2
  },
  // actions:方法,同步/异步都能写,用来修改state
  actions: {
    increment() {
      this.count++ // 直接修改state,Pinia允许这么做
    },
    async incrementAsync() {
      // 模拟异步延迟
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

这里注意几个点:

  • ID'counter'要全局唯一,不然会冲突;
  • state是函数,返回初始状态对象(避免所有实例共享同一对象);
  • getters里的函数能拿到state参数,也能通过this访问state(但TS下用参数更安全);
  • actions里的this指向当前Store实例,所以能直接改state,不用像Vuex那样commit mutation。

组件里咋用Store的状态和方法?

在Vue3的组合式API(setup语法)里,用useStore函数获取Store实例,然后访问state、getters、actions。

示例:在组件里用计数器Store

新建Counter.vue组件:

<template>
  <div>
    <p>当前计数:{{ count }}</p>
    <p>双倍计数:{{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="incrementAsync">异步+1</button>
  </div>
</template>
<script setup>
import { useCounterStore } from '../store/counterStore'
// 注意:直接解构state会丢失响应式,要用storeToRefs
import { storeToRefs } from 'pinia'
// 获取Store实例
const counterStore = useCounterStore()
// 解构state和getters,保持响应式
const { count, doubleCount } = storeToRefs(counterStore)
// 直接拿actions(方法本身不需要ref,因为调用时是触发动作)
const { increment, incrementAsync } = counterStore
</script>

这里关键是storeToRefs——因为Pinia的state是响应式的,但用对象解构后,普通变量会丢失响应式,storeToRefs能把state和getters转成带ref的结构,保证组件里数据变化时UI更新,而actions是函数,不属于响应式数据,直接解构没问题。

State的修改方式有哪些?哪种场景用哪种?

Pinia里改state很灵活,常用三种方式:

直接修改(最简单)

在action里或组件里,直接给state属性赋值:

// action里直接改
actions: {
  increment() {
    this.count++ // 允许!
  }
}
// 组件里直接改(不推荐,但能行)
counterStore.count++

这种方式适合简单场景,比如点击按钮直接+1,但如果是批量修改多个状态,或者想优化性能(减少响应式追踪次数),可以用$patch。

$patch方法(批量修改)

$patch有两种用法:传对象(适合改多个已知属性)、传函数(适合复杂逻辑或动态修改)。

示例1:传对象(一次性改多个属性)
假设state里有countmessage

counterStore.$patch({
  count: counterStore.count + 2,
  message: '更新后'
})

示例2:传函数(基于当前state计算新值)
适合需要先读旧值再修改的场景:

counterStore.$patch((state) => {
  state.count += 3
  state.message = `新消息${state.count}`
})

$patch的优势是批量更新,Vue会把多个状态变化合并成一个响应式更新,减少组件重渲染次数,性能更好。

在action里修改(适合复杂/异步逻辑)

如果修改state的逻辑很复杂,或者需要调接口(异步),把逻辑包在action里更清晰:

actions: {
  async fetchDataAndUpdate() {
    // 调接口(异步)
    const res = await api.get('/data')
    // 改state
    this.count = res.data.count
    this.message = res.data.msg
  }
}

组件里只需要调用这个action:counterStore.fetchDataAndUpdate(),逻辑内聚在Store里,代码更整洁。

Getter怎么用?和组件计算属性有啥区别?

Getter是Store里的“计算属性”,作用和组件的computed类似,但复用性更强——多个组件需要同一个计算逻辑时,把它放到Store的getter里,不用在每个组件重复写。

基本用法

比如前面计数器的doubleCount

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

组件里通过store.doubleCount访问,和访问state一样。

Getter支持传参(返回函数)

如果需要根据不同参数计算,Getter可以返回一个函数:

getters: {
  // 根据传入的乘数,返回对应结果
  multiplyCount: (state) => (multiplier) => state.count * multiplier
}
// 组件里用:
counterStore.multiplyCount(3) // 得到count*3的结果

和组件computed的区别

  • 作用域不同:组件computed只在当前组件生效;Getter在Store里,所有组件都能复用。
  • 依赖源不同:computed依赖组件内的响应式数据;Getter依赖Store的state。
  • 缓存机制:和computed一样,Getter也有缓存——只有依赖的state变化时,才会重新计算,性能友好。

Action里处理异步逻辑咋做?举个实际例子?

开发中常见的场景:调后端接口,拿数据后更新state,用Pinia的action写异步逻辑很丝滑,结合async/await就行。

示例:用户登录状态管理

假设要做一个用户Store,处理登录、登出、获取用户信息:

// src/store/userStore.js
import { defineStore } from 'pinia'
import { apiLogin, apiGetUserInfo } from '../api/user' // 假设的接口函数
export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '', // 从本地缓存取token
    userInfo: null
  }),
  actions: {
    // 登录(异步)
    async login(account, password) {
      try {
        // 调登录接口,拿token
        const res = await apiLogin({ account, password })
        this.token = res.data.token
        localStorage.setItem('token', this.token) // 存到本地
        // 登录成功后,拉取用户信息
        await this.fetchUserInfo()
        return true // 登录成功
      } catch (err) {
        console.error('登录失败', err)
        return false
      }
    },
    // 获取用户信息(异步)
    async fetchUserInfo() {
      const res = await apiGetUserInfo()
      this.userInfo = res.data
    },
    // 登出
    logout() {
      this.token = ''
      this.userInfo = null
      localStorage.removeItem('token')
    }
  }
})

组件里调用登录逻辑:

<template>
  <button @click="handleLogin">登录</button>
</template>
<script setup>
import { useUserStore } from '../store/userStore'
const userStore = useUserStore()
const handleLogin = async () => {
  const success = await userStore.login('test', '123456')
  if (success) {
    // 登录成功,跳转到首页等逻辑
  }
}
</script>

这里能看到:

  • action里可以用async/await处理异步流程;
  • 多个action可以互相调用(比如login里调用fetchUserInfo);
  • 结合本地存储(localStorage)做持久化(后面会讲更完善的持久化方案)。

多个Store之间怎么互相调用?比如用户Store要调商品Store的方法?

实际项目里,不同Store可能有依赖,比如用户登录后,需要更新购物车数据,这时候可以在一个Store的action里,用useOtherStore获取其他Store的实例。

示例:用户登录后更新购物车

假设有userStorecartStore,用户登录后要刷新购物车列表:

// src/store/userStore.js
import { defineStore } from 'pinia'
import { useCartStore } from './cartStore' // 导入其他Store
export const useUserStore = defineStore('user', {
  actions: {
    async login(account, password) {
      // 登录逻辑...(省略)
      // 登录成功后,获取购物车Store实例
      const cartStore = useCartStore()
      // 调用cartStore的action刷新购物车
      await cartStore.fetchCartList()
    }
  }
})
// src/store/cartStore.js
export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: []
  }),
  actions: {
    async fetchCartList() {
      // 调接口拿购物车数据
      const res = await apiGetCartList()
      this.cartList = res.data
    }
  }
})

这种跨Store调用的关键是:在需要的地方用useOtherStore获取实例,和组件里用Store的方式一样,Pinia会自动管理Store的单例性,不用担心重复创建。

Pinia的状态持久化怎么实现?刷新页面数据不丢?

默认情况下,Pinia的state存在内存里,页面刷新就没了,要实现持久化(比如存localStorage/sessionStorage),可以用社区插件pinia-plugin-persistedstate

步骤:

  1. 安装插件:

    npm install pinia-plugin-persistedstate
  2. 在main.js里注册插件:

    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入插件

const app = createApp(App) const pinia = createPinia() pinia.use(piniaPluginPersistedstate) // 注册插件 app.use(pinia) app.mount('#app')


3. 在Store里配置`persist`选项:  
以用户Store为例,让`token`和`userInfo`刷新后保留:  
```js
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null
  }),
  // 配置持久化
  persist: {
    key: 'user-store', // 存储的key(默认是Store的id)
    storage: localStorage, // 存哪里:localStorage/sessionStorage,默认localStorage
    paths: ['token', 'userInfo'] // 要持久化的state字段,默认全存
  },
  actions: { /* ... */ }
})

配置项说明:

  • key:自定义存储的键名,避免和其他项目冲突;
  • storage:选localStorage(永久存储)或sessionStorage(会话级存储,关闭标签页清空);
  • paths:数组,指定要持久化的state属性,比如只存token就写['token'],减少存储体积。

这样配置后,state里的tokenuserInfo会自动存到localStorage,页面刷新时Pinia会自动读取并恢复状态。

大型项目里,Pinia怎么拆分模块更合理?

大型项目状态多,全放一个Store里会很臃肿。按业务模块拆分Store是关键,比如用户模块、购物车模块、商品模块、订单模块,每个模块一个Store文件。

拆分步骤:

  1. src/store目录,按模块建文件:

    store/
    ├─ user.js    // 用户相关状态
    ├─ cart.js    // 购物车相关
    ├─ product.js // 商品列表相关
    ├─ index.js   // (可选)统一导出所有Store
  2. 每个模块独立定义Store:
    比如product.js负责商品列表的获取和筛选:

// src/store/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', {
  state: () => ({
    list: [],
    filter: 'all' // 筛选条件:all/sale/new
  }),
  actions: {
    async fetchProductList() {
      const res = await apiGetProductList(this.filter)
      this.list = res.data
    },
    setFilter(newFilter) {
      this.filter = newFilter
      this.fetchProductList() // 切换筛选后重新拉数据
    }
  }
})
  1. 组件里按需导入Store:
    需要商品列表的组件里,只导入useProductStore,不需要关心其他模块的状态,代码解耦。

  2. (可选)用index.js统一导出:
    如果觉得每次导入路径麻烦,可以在store/index.js里集中导出:

export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useProductStore } from './product'

组件里就可以这样导入:

import { useUserStore, useCartStore } from '../store'

Pinia和

版权声明

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

发表评论:

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

热门