Vue3 状态管理新选择:Pinia 从入门到实战

一、什么是pinia?

在 Vue3 生态中,状态管理一直是开发者关注的核心话题。随着 Vuex 的逐步淡出,Pinia 作为官方推荐的状态管理库,凭借其简洁的 API、强大的功能和对 Vue3 特性的完美适配,成为了新时代的不二之选。今天我们就来深入探讨 Pinia 的使用方法和最佳实践。

如果用生活中的例子来解释:

Pinia 就像一个 "共享冰箱"

想象一下,你和室友合租一套公寓,你们有一个 共享冰箱(Pinia Store)。这个冰箱的作用是:

  • 存放公共物品(状态 State):比如牛奶、水果、饮料等。
  • 规定取用规则(Getters):比如 "只能在早餐时间喝牛奶"。
  • 处理特殊操作(Actions):比如 "牛奶喝完后要通知所有人"。

pinia官网:Pinia | The intuitive store for Vue.js

二、怎么创建一个Pinia

1. 创建项目的时候直接选择Pinia

2. 项目中没有Pinia时,手动下载

①安装Pinia

bash 复制代码
npm install pinia

②在src中创建stores

③创建ts文件作为Pinia容器

④在counter.ts中加入以下代码

javascript 复制代码
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {

})

1、defineStore 是 Pinia 状态管理库 中的一个核心函数

2、参数说明

第一个参数 'counter':store 的唯一标识符(不可重复)

第二个参数 () => {}:store 的配置函数,返回 store 的状态和方法

⑤在main.ts中挂载Pinia

javascript 复制代码
import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'    // 引入创建Pinia方法
import App from './App.vue'

const app = createApp(App)   

app.use(createPinia())   // 挂载到app页面

app.mount('#app')

这样就成功手动实现了Pinia的手动安转和配置了。

三、定义第一个Pinia

1、第一个选项式Pinia示例

基础语法:

javascript 复制代码
import { defineStore } from 'pinia'

// 定义并导出 Store
export const defineStore实例化对象名 = defineStore('状态管理对象名称', {
  // 状态初始化,相当于data
  state: () => ({
    属性名1:属性值1
  }),
  
  // 计算属性(类似 Vue 组件中的 computed)
  getters: {
    
  },
  
  // 方法(类似 Vue 组件中的 methond)
  actions: {
    函数
  }
})

基础示例:

/stores/counter.ts

javascript 复制代码
// 1、导入defineStore
import { defineStore } from 'pinia'
import {computed,ref} from "vue";

export const useCounterStore = defineStore('counter', {
    state() {    // 相当于组件中的data
        return {
            title:"选项式计数管理器",
            count: 25   // 状态管理变量数据
        }
    },
    getters: {     // 相当于组件中的computed
        doubleCount: (state) => state.count * 2,   // 2倍计算属性数据
        doubleCountPlusOne: (state) => state.count + 10  // 加10计算属性数据
    },
    actions: {       // 相当于组件中的methods
        increment() {
            this.count++   // 创建函数每次点击按钮,count加1
        },
        decrement(){
            this.count--    // 创建函数每次点击按钮,count减1
        }
    }
})

/src/App.vue

javascript 复制代码
<script setup lang="ts">
// 1、导入
import {useCounterStore} from "@/stores/counter.ts"
// 2、实例化
const counterStore = useCounterStore()
</script>

<template>
// 3、使用
<div class="app">
  <h2>标题{{counterStore.title}}</h2>
  <p>当前计数:{{counterStore.count}}</p>
  <p>双倍计数:{{counterStore.doubleCount}}</p>
  <p>计数+10:{{counterStore.doubleCountPlusOne}}</p>
  <button @click="counterStore.increment()">点击+1</button>
  <button @click="counterStore.decrement()">点击-1</button>
</div>
</template>

运行结果:

2、第一个组合式Pinia实例

基础语法:

javascript 复制代码
import { defineStore } from 'pinia'
import { ref, computed, reactive } from 'vue'

export const useStoreNameStore = defineStore('storeId', () => {
  // 1. State - 使用 ref 或 reactive
  const count = ref(0)
  const state = reactive({ 
    name: 'example' 
  })
  
  // 2. Getters - 使用 computed
  const doubleCount = computed(() => count.value * 2)
  
  // 3. Actions - 普通函数
  function increment() {
    count.value++
  }
  
  // 4. 返回需要暴露的属性和方法
  return {
    count,
    doubleCount,
    increment
  }
})

基础示例:

/stores/users.ts

javascript 复制代码
import {defineStore} from 'pinia'
import {computed, reactive, ref} from "vue";
export const useUserStore = defineStore('user', ()=>{
    let isLogin = ref(false);
    let username = ref('未知');
    let email = ref('未知');
    let displayName = ref('未知');
    let roles = reactive(['管理员','用户','玩家','游客']);
    let nowRole = ref('未知');
    let theme = ref('白色');
    let language = ref('chinese');
    let message = ref(0);

    function updateLoginStatus(){
        if(isLogin.value){
            isLogin.value = !isLogin.value;
            username.value = "未知";
            displayName.value = "未知";
            nowRole.value = "未知";
            message.value = 0;
            email.value = "未知";
        }
        else{
            isLogin.value = !isLogin.value;
            username.value = "张三";
            displayName.value = "追风少年";
            let random:number = Math.floor(Math.random()*4);
            nowRole.value = roles[random];
            message.value = 10;
            email.value = "zhangsan@163.com";
        }

    }
    function updateUserProfile(){
        theme.value = theme.value === 'white' ? 'dark' : 'white';
        language.value = language.value === 'chinese' ? 'english' : 'chinese';
    }
    function resetUser(){
         username.value = "未知";
         displayName.value = "未知";
         nowRole.value = "未知";
         message.value = 0;
         roles.splice(0,roles.length);
         email.value = "未知";
    }
    return {
        isLogin,
        username,
        email,
        displayName,
        nowRole,
        theme,
        language,
        message,
        updateLoginStatus,
        updateUserProfile,
        resetUser
    }
})

/src/App.vue

javascript 复制代码
<script setup lang="ts">
import { useUserStore } from "@/stores/users.ts";
const userStore = useUserStore();
</script>
<template>
<div class="user-profile">
    <h1>用户资料</h1>
    <!-- 直接访问 -->
    <p>用户名: {{ userStore.username }}</p>
    <p>网名: {{ userStore.displayName }}</p>

    <!-- 解构访问 -->
    <div>
      <p>邮箱: {{userStore.email }}</p>
      <p>登录状态: {{ userStore.isLogin ? '登录':'未登录'}}</p>
    </div>

    <!-- 复杂数据访问 -->
    <div>
      <p>主题: {{ userStore.theme }}</p>
      <p>语言: {{ userStore.language }}</p>
    </div>

    <!-- 数组数据 -->
    <div>
      <p>角色: {{ userStore.nowRole }}</p>
      <p>未读通知: {{ userStore.message}}</p>
    </div>

    <button @click="userStore.updateLoginStatus" >{{ userStore.isLogin ? '退出':'登录'}}</button>
    <button @click="userStore.updateUserProfile">更新资料</button>
    <button @click="userStore.resetUser">重置用户</button>
</div>
</template>

运行结果:

五、综合案例

/stores/counter.ts

javascript 复制代码
import {reactive} from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  let products = reactive(
      [
            {
                id: 1, 			    // 商品ID
                name: "苹果16ProMax", // 商品名称
                price: 12999,       // 商品价格
                category: "phone", // 商品类别
                num:123,     // 商品库存数量
                rating: 4.8,       // 商品评分
            },
            {
                id: 2,
                name: "联想拯救者",
                price: 23999,
                category: "laptop",   // 笔记本
                num: 0,
                rating: 3.8,
            },
           {
                id: 3,
                name: "华硕天选6pro",
                price: 11499,
                category: "laptop",   // 笔记本
                num: 15,
                rating: 4.9,
            },
            {
                id: 3,
                name: "iQoo平板7",
                price: 3499,
                category: "tablet",  // 平板电脑
                num: 899,
                rating: 3.7,
            },
            {
                id: 4,
                name: "iPad Air",
                price: 8599,
                category: "tablet",  // 平板电脑
                num: 899,
                rating: 4.1,
            },
            {
                id: 5,
                name: "小米手环7",
                price: 999,
                category: "watch",  // 手表
                num:45,
                rating: 4.61,
            },
            {
                id: 6,
                name: "苹果手表6",
                price: 3888,
                category: "watch",  // 手表
                num:45,
                rating: 4.9,
            },
             {
                id: 6,
                name: "小米手机",
                price: 3999,
                category: "phone",
                num:425,
                nun: 442,
                rating: 4.7,
            },
        ],
  )
    let avgrating = products.reduce((sum,crr) => sum+crr.rating,0) / products.length
    let avgprice = products.reduce((sum,crr) => sum+crr.price,0) / products.length
    let sumNum = products.reduce((sum,crr) => sum+crr.num,0)
    let stockNum = products.filter(item=>item.num <= 0).length
    let categories = reactive(["phone", "laptop", "tablet", "watch"])
    return {
        products,
        categories,
        avgrating,
        avgprice,
        sumNum,
        stockNum
    }
})

/src/App.vue

html 复制代码
<script setup lang="ts">
import {useCounterStore} from './stores/counter.ts'
import {computed, ref} from 'vue'
const store = useCounterStore()
const products = store.products
const minPrice = ref(0)
const maxPrice = ref(0)
const filteredProducts = computed(() => {
  return products.filter(
    item => item.price >= minPrice.value && item.price <= maxPrice.value
  )
})
</script>

<template>
  <div class="container">
    <h1>库存管理系统</h1>
    <h2>总产品数据</h2>
    <table>
      <tr>
        <th>商品名</th>
        <th>商品价格</th>
        <th>商品库存</th>
        <th>商品评分</th>
      </tr>
      <tr v-for="(item) in products">
        <td>{{item.name}}</td>
        <td>{{item.price}}</td>
        <td>{{item.num}}</td>
        <td>{{item.rating}}</td>
      </tr>
    </table>
    <strong>产品数量:{{store.sumNum}}</strong>
    <strong>缺货商品数量:{{store.stockNum}}</strong>
    <strong>平均价格:{{store.avgprice}}</strong>
    <strong>平均评分:{{store.avgrating}}</strong>
    <h2>分类统计</h2>
    <div class="classify" v-for="item in store.categories">
      <strong>{{item}}</strong>
      <table>
        <tr>
          <th>商品名</th>
          <th>商品价格</th>
          <th>商品库存</th>
          <th>商品评分</th>
        </tr>
        <template v-for="product in products" :key="product.id">
            <tr v-if="product.category === item">
              <td>{{ product.name }}</td>
              <td>{{ product.price }}</td>
              <td>{{ product.num }}</td>
              <td>{{ product.rating }}</td>
            </tr>
         </template>
      </table>
    </div>
    <h2>商品筛选</h2>
    <div>
      <label>价格区间:</label>
      <input type="number" v-model="minPrice">---<input type="number" v-model="maxPrice">
      <table v-if="filteredProducts.length > 0">
        <tr>
          <th>商品名</th>
          <th>商品价格</th>
          <th>商品库存</th>
          <th>商品评分</th>
        </tr>
        <tr v-for="item in filteredProducts" :key="item.id">
          <td>{{ item.name }}</td>
          <td>{{ item.price }}</td>
          <td>{{ item.num }}</td>
          <td>{{ item.rating }}</td>
        </tr>
      </table>
      <p v-else>暂无符合条件的商品。</p>
    </div>
  </div>
</template>

<style scoped>
.container{
  width:1000px;
  padding: 20px;
  border: #6e7681 1px solid;
  box-shadow: 2px 2px 4px #bdc1c6;
  margin: 0 auto;
}
h1{
  text-align: center;
}
table{
  margin: 0 auto;
  border: #6e7681 1px solid;
  width:1000px;
  border-collapse: collapse; /* 合并边框 */
  margin-bottom: 30px;
}
strong{
  display: block;
  margin-top: 10px;
}
th,tr,td{
   border: #6e7681 1px solid;  /* 关键:为单元格添加边框 */
   padding: 10px;              /* 添加内边距使内容更美观 */
   text-align: center;
}
</style>

运行结果:

相关推荐
好好研究3 小时前
使用JavaScript实现轮播图的自动切换和左右箭头切换效果
开发语言·前端·javascript·css·html
paopaokaka_luck4 小时前
基于Spring Boot+Vue的吉他社团系统设计和实现(协同过滤算法)
java·vue.js·spring boot·后端·spring
伍哥的传说6 小时前
Radash.js 现代化JavaScript实用工具库详解 – 轻量级Lodash替代方案
开发语言·javascript·ecmascript·tree-shaking·radash.js·debounce·throttle
程序视点7 小时前
IObit Uninstaller Pro专业卸载,免激活版本,卸载清理注册表,彻底告别软件残留
前端·windows·后端
前端程序媛-Tian7 小时前
【dropdown组件填坑指南】—怎么实现下拉框的位置计算
前端·javascript·vue
iamlujingtao7 小时前
js多边形算法:获取多边形中心点,且必定在多边形内部
javascript·算法
嘉琪0017 小时前
实现视频实时马赛克
linux·前端·javascript
烛阴8 小时前
Smoothstep
前端·webgl
若梦plus8 小时前
Eslint中微内核&插件化思想的应用
前端·eslint
爱分享的程序员8 小时前
前端面试专栏-前沿技术:30.跨端开发技术(React Native、Flutter)
前端·javascript·面试