关于 pinia
面向 JARVIS AI 助手开发者的 Pinia 状态管理技能,重点介绍类型化 Store、SSR 隔离、安全持久化和高效响应式更新。
主要功能
- 01使用 TypeScript 编写 Setup Store
- 02隔离 SSR 请求状态
- 03防止持久化敏感数据
- 04验证 Store 操作输入
- 05计算属性与选择性订阅
- 06使用 Vitest 测试 Store
适用场景
- 01跟踪系统指标与状态
- 02管理偏好设置和 HUD 配置
- 03维护命令历史与队列
- 04同步实时应用数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ martinholovsky/ claude-skills-generator --skill pinia安装后在 Claude Code 中输入/pinia确认已加载。
附带文件references/advanced-patterns.mdreferences/security-examples.md
详细介绍
Pinia 状态管理技能
文件组织:此技能采用拆分结构。有关高级模式和安全示例,请参阅
references/。
1. 概述
此技能提供 Pinia 专业知识,用于管理 JARVIS AI Assistant 中的应用状态,包括系统指标、用户偏好设置和 HUD 配置。
风险级别:中等 — 管理敏感状态,需考虑 SSR,并可能导致数据泄露
主要使用场景:
- 系统指标和状态跟踪
- 用户偏好设置和设置
- HUD 配置状态
- 命令历史记录和队列
- 实时数据同步
2. 核心职责
2.1 核心原则
- TDD 优先:先编写 Store 测试,再进行实现
- 关注性能:优化订阅和计算值
- 类型安全:为 Store 定义完整的 TypeScript 类型
- SSR 安全:防止请求之间发生状态泄露
- 组合式 API:使用 setup Store,以获得更好的 TypeScript 支持
- 精简状态:仅存储必要数据,其余数据通过派生获得
- 操作验证:在操作中、修改状态前验证输入
- 持久化安全:绝不将敏感数据持久化到 localStorage
3. 技术栈和版本
3.1 推荐版本
| 包 | 版本 | 说明 |
|---|---|---|
| pinia | ^2.1.0 | 最新稳定版 |
| @pinia/nuxt | ^0.5.0 | Nuxt 集成 |
| pinia-plugin-persistedstate | ^3.0.0 | 可选持久化 |
3.2 Nuxt 配置
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**']
}
})
3.3 实现工作流(TDD)
对每个 Store 都遵循此工作流:
步骤 1:先编写失败的测试
// tests/stores/metrics.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useMetricsStore } from '~/stores/metrics'
describe('MetricsStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with default values', () => {
const store = useMetricsStore()
expect(store.cpu).toBe(0)
expect(store.memory).toBe(0)
})
it('should clamp values within valid range', () => {
const store = useMetricsStore()
store.updateCpu(150)
expect(store.cpu).toBe(100)
store.updateCpu(-50)
expect(store.cpu).toBe(0)
})
it('should compute health status correctly', () => {
const store = useMetricsStore()
store.updateCpu(95)
store.updateMemory(90)
expect(store.healthStatus).toBe('critical')
})
})
步骤 2:实现满足测试所需的最少功能
// stores/metrics.ts
export const useMetricsStore = defineStore('metrics', () => {
const cpu = ref(0)
const memory = ref(0)
const healthStatus = computed(() => {
const avg = (cpu.value + memory.value) / 2
if (avg > 90) return 'critical'
if (avg > 70) return 'warning'
return 'healthy'
})
function updateCpu(value: number) {
cpu.value = Math.max(0, Math.min(100, value))
}
function updateMemory(value: number) {
memory.value = Math.max(0, Math.min(100, value))
}
return { cpu, memory, healthStatus, updateCpu, updateMemory }
})
步骤 3:按照模式进行重构
- 提取验证逻辑
- 添加 TypeScript 接口
- 优化计算依赖项
步骤 4:运行完整验证
npm run test -- --filter=stores
npm run typecheck
npm run build
4. 实现模式
4.1 使用 TypeScript 的 setup Store
// stores/jarvis.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface SystemMetrics {
cpu: number
memory: number
network: number
timestamp: number
}
interface JARVISState {
status: 'idle' | 'listening' | 'processing' | 'responding'
securityLevel: 'normal' | 'elevated' | 'lockdown'
}
export const useJarvisStore = defineStore('jarvis', () => {
// State
const state = ref<JARVISState>({
status: 'idle',
securityLevel: 'normal'
})
const metrics = ref<SystemMetrics>({
cpu: 0,
memory: 0,
network: 0,
timestamp: Date.now()
})
// Getters
const isActive = computed(() =>
state.value.status !== 'idle'
)
const systemHealth = computed(() => {
const avg = (metrics.value.cpu + metrics.value.memory) / 2
if (avg > 90) return 'critical'
if (avg > 70) return 'warning'
return 'healthy'
})
// Actions
function updateMetrics(newMetrics: Partial<SystemMetrics>) {
// ✅ Validate input
if (newMetrics.cpu !== undefined) {
metrics.value.cpu = Math.max(0, Math.min(100, newMetrics.cpu))
}
if (newMetrics.memory !== undefined) {
metrics.value.memory = Math.max(0, Math.min(100, newMetrics.memory))
}
if (newMetrics.network !== undefined) {
metrics.value.network = Math.max(0, newMetrics.network)
}
metrics.value.timestamp = Date.now()
}
function setStatus(newStatus: JARVISState['status']) {
state.value.status = newStatus
}
function setSecurityLevel(level: JARVISState['securityLevel']) {
state.value.securityLevel = level
// ✅ Audit security changes
console.info(`Security level changed to: ${level}`)
}
return {
state,
metrics,
isActive,
systemHealth,
updateMetrics,
setStatus,
setSecurityLevel
}
})
4.2 用户偏好设置 Store(带持久化)
// stores/preferences.ts
export const usePreferencesStore = defineStore('preferences', () => {
const preferences = ref({
theme: 'dark' as 'dark' | 'light',
hudOpacity: 0.8,
soundEnabled: true
})
function updatePreference<K extends keyof typeof preferences.value>(
key: K, value: typeof preferences.value[K]
) {
if (key === 'hudOpacity' && (value < 0 || value > 1)) return
preferences.value[key] = value
}
return { preferences, updatePreference }
}, {
persist: {
key: 'jarvis-preferences',
paths: ['preferences.theme', 'preferences.hudOpacity']
// ❌ Never persist: tokens, passwords, API keys
}
})
4.3 命令队列 Store
// stores/commands.ts
interface Command {
id: string
action: string
status: 'pending' | 'executing' | 'completed' | 'failed'
}
export const useCommandStore = defineStore('commands', () => {
const queue = ref<Command[]>([])
const history = ref<Command[]>([])
const MAX_HISTORY = 100
const pendingCommands = computed(() =>
queue.value.filter(cmd => cmd.status === 'pending')
)
function addCommand(action: string) {
const cmd: Command = { id: crypto.randomUUID(), action, status: 'pending' }
queue.value.push(cmd)
return cmd.id
}
function completeCommand(id: string, status: 'completed' | 'failed') {
const idx = queue.value.findIndex(cmd => cmd.id === id)
if (idx !== -1) {
const [cmd] = queue.value.splice(idx, 1)
cmd.status = status
history.value = [cmd, ...history.value].slice(0, MAX_HISTORY)
}
}
return { queue, history, pendingCommands, addCommand, completeCommand }
})
4.4 SSR 安全的 Store 用法
<script setup lang="ts">
// ✅ Safe for SSR - store initialized per-request
const jarvisStore = useJarvisStore()
// ✅ Fetch data on server
const { data } = await useFetch('/api/metrics')
// Update store with fetched data
if (data.value) {
jarvisStore.updateMetrics(data.value)
}
</script>
4.5 Store 组合
// stores/dashboard.ts
export const useDashboardStore = defineStore('dashboard', () => {
// ✅ Compose from other stores
const jarvisStore = useJarvisStore()
const commandStore = useCommandStore()
const dashboardStatus = computed(() => ({
systemHealth: jarvisStore.systemHealth,
pendingCommands: commandStore.pendingCommands.length,
isActive: jarvisStore.isActive
}))
return {
dashboardStatus
}
})
5. 安全标准
5.1 OWASP 覆盖范围
| OWASP 类别 | 风险 | 缓解措施 |
|---|---|---|
| A01 访问控制失效 | 中等 | 验证操作,检查权限 |
| A04 不安全的设计 | 中等 | SSR 状态隔离 |
| A07 身份验证失效 | 中等 | 绝不持久化令牌 |
5.3 敏感数据处理
// ❌ NEVER persist: tokens, API keys, passwords
// ✅ Store sensitive data in memory only (no persist option)
const authStore = defineStore('auth', () => {
const token = ref<string | null>(null)
return { token }
})
5.5 性能模式
模式 1:选择性订阅
// BAD - Subscribes to entire store
const store = useJarvisStore()
watch(() => store.state, () => { /* ... */ }, { deep: true })
// GOOD - Subscribe to specific properties
const store = useJarvisStore()
watch(() => store.state.status, (newStatus) => {
console.log('Status changed:', newStatus)
})
模式 2:计算属性(记忆化)
// BAD - Recalculates on every access
function getFilteredItems() {
return items.value.filter(i => i.active)
}
// GOOD - Cached until dependencies change
const filteredItems = computed(() =>
items.value.filter(i => i.active)
)
模式 3:批量更新
// BAD - Multiple reactive triggers
function updateAll(data: MetricsData) {
metrics.value.cpu = data.cpu
metrics.value.memory = data.memory
metrics.value.network = data.network
}
// GOOD - Single reactive trigger
function updateAll(data: MetricsData) {
metrics.value = { ...metrics.value, ...data, timestamp: Date.now() }
}
模式 4:延迟初始化 Store
// BAD - Store initializes immediately
const heavyStore = useHeavyDataStore()
// GOOD - Initialize only when needed
const heavyStore = ref<ReturnType<typeof useHeavyDataStore> | null>(null)
function loadHeavyData() {
if (!heavyStore.value) {
heavyStore.value = useHeavyDataStore()
}
return heavyStore.value
}
模式 5:乐观更新
// BAD - Wait for server response
async function deleteItem(id: string) {
await api.delete(`/items/${id}`)
items.value = items.value.filter(i => i.id !== id)
}
// GOOD - Update immediately, rollback on error
async function deleteItem(id: string) {
const backup = [...items.value]
items.value = items.value.filter(i => i.id !== id)
try {
await api.delete(`/items/${id}`)
} catch (error) {
items.value = backup // Rollback
throw error
}
}
6. 测试和质量
有关包含 vitest 示例的完整 TDD 工作流,请参阅第 3.3 节。
8. 常见反模式
安全反模式
// ❌ Global state leaks between SSR users
const state = reactive({ user: null })
// ✅ Pinia isolates per-request
export const useUserStore = defineStore('user', () => {
const user = ref(null)
return { user }
})
// ❌ Never persist auth tokens (XSS risk)
persist: { paths: ['authToken'] }
// ✅ Use httpOnly cookies for auth
性能反模式
有关包含“良好/不良”示例的详细性能模式,请参阅第 5.5 节。
13. 实施前检查清单
阶段 1:编写代码前
- 使用 TypeScript 类型设计 Store 接口
- 创建包含失败测试的测试文件
- 确认安全要求(持久化、SSR)
- 针对使用场景选择性能模式
阶段 2:实施期间
- 每添加一项功能后,测试均通过
- 操作会验证所有输入
- 计算值使用最少的依赖项
- 持久化状态中不包含敏感数据
- SSR 状态已正确隔离
阶段 3:提交前
- 所有 Store 测试均通过:
npm run test -- --filter=stores - 类型检查通过:
npm run typecheck - 构建成功:
npm run build - Pinia 之外没有全局状态
- 已在类型中记录状态结构
14. 总结
Pinia 为 JARVIS 提供类型安全的状态管理:
- 测试驱动开发优先:先编写 Store 测试,再进行实现
- 性能:优化订阅和计算属性
- 安全性:切勿持久化敏感数据,并隔离 SSR 状态
- 类型安全:使用完整支持 TypeScript 的 Setup Store
参考资料:高级模式和安全示例请参见 references/。
评论
支持 Markdown