Vue.js与Pinia:新状态管理库的使用与比较

318 阅读5分钟

Pinia 的基本概念

Pinia 是 Vue 3 中的一个轻量级状态管理库,旨在简化状态管理的复杂性。它提供了简洁的 API 和强大的功能,使得状态管理变得更加简单和直观。

核心特点

  • 简洁的 API:Pinia 的 API 设计非常简洁,易于上手。
  • 自动持久化:Pinia 支持自动持久化状态到本地存储。
  • 类型安全:Pinia 支持 TypeScript,确保类型安全。
  • 插件系统:Pinia 提供了丰富的插件系统,方便扩展功能。

Pinia 的安装与配置

安装 Pinia 可以通过 npm 或 yarn 安装 Pinia:

npm install pinia
# 或者
yarn add pinia

创建 Pinia 应用实例

在项目中创建一个 store 文件夹,并在其中创建一个 index.js 文件:

// store/index.js
import { createPinia } from 'pinia';

const pinia = createPinia();

export default pinia;

在 Vue 应用中使用 Pinia 在 main.js 或 main.ts 文件中引入 Pinia 并注册到 Vue 应用中:

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import pinia from './store';

const app = createApp(App);
app.use(pinia);
app.mount('#app');

Pinia 的核心功能

1. 定义 Store

Pinia 中的状态管理主要通过定义 Store 来实现。Store 是一个包含状态、GettersActions 的对象。

// store/todo.js
import { defineStore } from 'pinia';

export const useTodoStore = defineStore({
  id: 'todo',
  state: () => ({
    todos: [
      { id: 1, text: 'Learn Pinia', done: false },
      { id: 2, text: 'Build a project', done: true }
    ]
  }),
  getters: {
    completedTodos: (state) => state.todos.filter(todo => todo.done),
    remainingTodos: (state) => state.todos.filter(todo => !todo.done)
  },
  actions: {
    addTodo(text) {
      this.todos.push({ id: Date.now(), text, done: false });
    },
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.done = !todo.done;
      }
    },
    removeTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
    }
  }
});

2. 使用 Store 在 Vue 组件中使用 Store 非常简单,只需通过 useTodoStore 获取 Store 实例即可。

<template>
  <div>
    <h1>Todo List</h1>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" :checked="todo.done" @change="toggleTodo(todo.id)" />
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">Remove</button>
      </li>
    </ul>
    <input v-model="newTodoText" placeholder="New todo" @keyup.enter="addTodo" />
    <button @click="addTodo">Add Todo</button>
  </div>
</template>

<script setup>
import { useTodoStore } from '@/store/todo';
import { ref } from 'vue';

const newTodoText = ref('');
const todoStore = useTodoStore();

const { todos, addTodo, toggleTodo, removeTodo } = todoStore;
</script>

<style scoped>
.done {
  text-decoration: line-through;
}
</style>

Pinia 与 Vuex 的比较

相似之处

  • 状态管理:两者都提供了状态管理功能。
  • 模块化:都可以将状态拆分成多个模块。
  • Getters 和 Actions:都支持 Getters 和 Actions。

不同之处

  • API 设计:Pinia 的 API 更加简洁,易于上手。
  • 类型安全:Pinia 支持 TypeScript 类型推断,更加安全。
  • 自动持久化:Pinia 支持自动持久化状态到本地存储。
  • 插件系统:Pinia 提供了丰富的插件系统,方便扩展功能。

Pinia 实战案例

构建一个复杂的待办事项应用

需求分析

  • 用户可以添加待办事项。
  • 用户可以删除待办事项。
  • 用户可以标记待办事项为完成。
  • 用户可以筛选待办事项(全部、已完成、未完成)。

组件设计

  • TodoList:显示所有待办事项。
  • TodoItem:显示单个待办事项。
  • AddTodo:添加新的待办事项。
  • FilterTodos:筛选待办事项。

实现代码

TodoList

<template>
  <div>
    <h1>Todo List</h1>
    <ul>
      <TodoItem v-for="todo in filteredTodos" :key="todo.id" :todo="todo" />
    </ul>
    <AddTodo @add-todo="addTodo" />
    <FilterTodos @filter-todos="filterTodos" />
  </div>
</template>

<script setup>
import { useTodoStore } from '@/store/todo';
import TodoItem from '@/components/TodoItem.vue';
import AddTodo from '@/components/AddTodo.vue';
import FilterTodos from '@/components/FilterTodos.vue';

const todoStore = useTodoStore();

const { todos, addTodo, toggleTodo, removeTodo } = todoStore;

const filter = ref('all');
const filteredTodos = computed(() => {
  if (filter.value === 'all') {
    return todos;
  } else if (filter.value === 'completed') {
    return todoStore.completedTodos;
  } else if (filter.value === 'remaining') {
    return todoStore.remainingTodos;
  }
});

function filterTodos(value) {
  filter.value = value;
}
</script>

TodoItem

<template>
  <li>
    <input type="checkbox" :checked="todo.done" @change="toggleTodo(todo.id)" />
    <span :class="{ done: todo.done }">{{ todo.text }}</span>
    <button @click="removeTodo(todo.id)">Remove</button>
  </li>
</template>

<script setup>
import { defineProps, onMounted } from 'vue';
import { useTodoStore } from '@/store/todo';

const props = defineProps(['todo']);
const todoStore = useTodoStore();

function toggleTodo(id) {
  todoStore.toggleTodo(id);
}

function removeTodo(id) {
  todoStore.removeTodo(id);
}
</script>

<style scoped>
.done {
  text-decoration: line-through;
}
</style>

AddTodo

<template>
  <div>
    <input v-model="newTodoText" placeholder="New todo" @keyup.enter="addTodo" />
    <button @click="addTodo">Add Todo</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useTodoStore } from '@/store/todo';

const newTodoText = ref('');
const todoStore = useTodoStore();

function addTodo() {
  todoStore.addTodo(newTodoText.value);
  newTodoText.value = '';
}
</script>

FilterTodos

<template>
  <div>
    <button @click="filterTodos('all')">All</button>
    <button @click="filterTodos('completed')">Completed</button>
    <button @click="filterTodos('remaining')">Remaining</button>
  </div>
</template>

<script setup>
import { defineEmits } from 'vue';

const emit = defineEmits(['filter-todos']);

function filterTodos(value) {
  emit('filter-todos', value);
}
</script>

Pinia 的高级功能

1. 动态模块 Pinia 支持动态创建和销毁模块,这在某些场景下非常有用。

// store/dynamicModule.js
import { defineStore } from 'pinia';

export const useDynamicStore = defineStore({
  id: 'dynamic',
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  },
  actions: {
    increment() {
      this.count++;
    }
  }
});

// 动态创建模块
function createDynamicModule(moduleId) {
  const module = defineStore({
    id: moduleId,
    state: () => ({
      count: 0
    }),
    getters: {
      doubleCount(state) {
        return state.count * 2;
      }
    },
    actions: {
      increment() {
        this.count++;
      }
    }
  });

  return module;
}

// 使用动态模块
const dynamicModule = createDynamicModule('dynamic1');
console.log(dynamicModule.doubleCount); // 0
dynamicModule.increment();
console.log(dynamicModule.doubleCount); // 2

2. 热更新 Pinia 支持热更新,可以在开发过程中轻松修改状态。

// store/hotUpdate.js
import { defineStore } from 'pinia';

export const useHotUpdateStore = defineStore({
  id: 'hotUpdate',
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  },
  actions: {
    increment() {
      this.count++;
    }
  }
});

// 修改状态
const hotUpdateStore = useHotUpdateStore();
hotUpdateStore.increment();
console.log(hotUpdateStore.doubleCount); // 2

Pinia 的插件系统

Pinia 提供了一个强大的插件系统,可以方便地扩展 Pinia 的功能。

1. 自定义插件 示例代码:

// plugins/persistedState.js
import { createPersistedState } from 'pinia-plugin-persistedstate';

export default createPersistedState({
  key: 'myState',
  storage: window.localStorage
});

// 在 Pinia 应用实例中使用插件
import { createPinia } from 'pinia';
import persistedState from './plugins/persistedState';

const pinia = createPinia();
pinia.use(persistedState);

export default pinia;

2. 日志插件

// plugins/logger.js
export default (store) => {
  store.subscribe((mutation, state) => {
    console.log('Mutation:', mutation);
    console.log('New State:', state);
  });
};

// 在 Pinia 应用实例中使用插件
import { createPinia } from 'pinia';
import logger from './plugins/logger';

const pinia = createPinia();
pinia.use(logger);

export default pinia;

Pinia 的持久化策略

Pinia 提供了多种持久化策略,可以方便地将状态保存到本地存储。

1. 自动持久化

// store/todo.js
import { defineStore } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';

export const useTodoStore = defineStore({
  id: 'todo',
  state: () => ({
    todos: [
      { id: 1, text: 'Learn Pinia', done: false },
      { id: 2, text: 'Build a project', done: true }
    ]
  }),
  getters: {
    completedTodos: (state) => state.todos.filter(todo => todo.done),
    remainingTodos: (state) => state.todos.filter(todo => !todo.done)
  },
  actions: {
    addTodo(text) {
      this.todos.push({ id: Date.now(), text, done: false });
    },
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.done = !todo.done;
      }
    },
    removeTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
    }
  }
});

// 在 Pinia 应用实例中使用插件
import { createPinia } from 'pinia';
import persistedState from 'pinia-plugin-persistedstate';

const pinia = createPinia();
pinia.use(createPersistedState());

export default pinia;

Pinia 的性能优化

1. 惰性加载 Pinia 支持惰性加载 Store,只有当 Store 被首次访问时才会被初始化。

// store/lazy.js
import { defineStore } from 'pinia';

export const useLazyStore = defineStore({
  id: 'lazy',
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  },
  actions: {
    increment() {
      this.count++;
    }
  }
});

// 惰性加载 Store
const lazyStore = useLazyStore();
console.log(lazyStore.doubleCount); // 0
lazyStore.increment();
console.log(lazyStore.doubleCount); // 2

2. 异步操作 Pinia 支持异步操作,可以方便地处理异步数据。

// store/async.js
import { defineStore } from 'pinia';

export const useAsyncStore = defineStore({
  id: 'async',
  state: () => ({
    data: null
  }),
  getters: {
    hasData(state) {
      return !!state.data;
    }
  },
  actions: {
    async fetchData() {
      try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        this.data = data;
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    }
  }
});

// 使用异步操作
const asyncStore = useAsyncStore();
asyncStore.fetchData();
console.log(asyncStore.hasData); // false
setTimeout(() => {
  console.log(asyncStore.hasData); // true
}, 2000);

Pinia 在大型项目中的应用

1. 模块化设计 在大型项目中,Pinia 的模块化设计非常重要。每个 Store 只负责一项功能,通过组合多个 Store 来构建复杂功能。

// store/user.js
import { defineStore } from 'pinia';

export const useUserStore = defineStore({
  id: 'user',
  state: () => ({
    name: '',
    email: ''
  }),
  getters: {
    fullName(state) {
      return `${state.name} (${state.email})`;
    }
  },
  actions: {
    login(name, email) {
      this.name = name;
      this.email = email;
    },
    logout() {
      this.name = '';
      this.email = '';
    }
  }
});

// store/todo.js
import { defineStore } from 'pinia';

export const useTodoStore = defineStore({
  id: 'todo',
  state: () => ({
    todos: []
  }),
  getters: {
    completedTodos(state) {
      return state.todos.filter(todo => todo.done);
    },
    remainingTodos(state) {
      return state.todos.filter(todo => !todo.done);
    }
  },
  actions: {
    addTodo(text) {
      this.todos.push({ id: Date.now(), text, done: false });
    },
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id);
      if (todo) {
        todo.done = !todo.done;
      }
    },
    removeTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
    }
  }
});

2. 状态管理 在大型项目中,状态管理尤为重要。Pinia 提供了灵活的状态管理机制,可以通过模块化的方式组织状态。

示例代码:

// store/auth.js
import { defineStore } from 'pinia';

export const useAuthStore = defineStore({
  id: 'auth',
  state: () => ({
    token: localStorage.getItem('token') || '',
    user: JSON.parse(localStorage.getItem('user')) || {}
  }),
  getters: {
    isLoggedIn(state) {
      return !!state.token;
    }
  },
  actions: {
    login(token, user) {
      this.token = token;
      this.user = user;
      localStorage.setItem('token', token);
      localStorage.setItem('user', JSON.stringify(user));
    },
    logout() {
      this.token = '';
      this.user = {};
      localStorage.removeItem('token');
      localStorage.removeItem('user');
    }
  }
});

3. 异步操作 在大型项目中,异步操作非常常见。Pinia 提供了异步操作的支持,可以方便地处理异步数据。

示例代码:

// store/api.js
import { defineStore } from 'pinia';

export const useApiStore = defineStore({
  id: 'api',
  state: () => ({
    data: null
  }),
  getters: {
    hasData(state) {
      return !!state.data;
    }
  },
  actions: {
    async fetchData() {
      try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        this.data = data;
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    }
  }
});