Pinia 插件系统 #

概述 #

Pinia 插件是一个函数,可以用来扩展每个 Store 的功能。插件可以在 Store 创建时添加新属性、修改现有行为或响应特定事件。

基本用法 #

注册插件 #

ts
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()

// 注册插件
pinia.use(({ store }) => {
  console.log(`Store created: ${store.$id}`)
})

const app = createApp(App)
app.use(pinia)

插件结构 #

ts
interface PiniaPluginContext {
  pinia: Pinia
  app: App
  store: Store
  options: DefineStoreOptions
}

function myPlugin(context: PiniaPluginContext) {
  const { pinia, app, store, options } = context
  
  // 扩展 store
}

扩展 Store #

添加属性 #

ts
// 添加全局属性
pinia.use(({ store }) => {
  store.$name = 'My Store'
})

// 在组件中使用
const userStore = useUserStore()
console.log(userStore.$name)  // 'My Store'

添加方法 #

ts
pinia.use(({ store }) => {
  store.$log = function() {
    console.log(`[${store.$id}]`, this.$state)
  }
})

// 使用
userStore.$log()

添加计算属性 #

ts
import { computed } from 'vue'

pinia.use(({ store }) => {
  // 添加计算属性
  store.$isEmpty = computed(() => Object.keys(store.$state).length === 0)
})

内置钩子 #

$subscribe #

监听状态变化:

ts
pinia.use(({ store }) => {
  store.$subscribe((mutation, state) => {
    console.log('State changed:', mutation.type)
    console.log('New state:', state)
  })
})

$onAction #

监听 action 调用:

ts
pinia.use(({ store }) => {
  store.$onAction(({ name, args, after, onError }) => {
    console.log(`Action ${name} called with:`, args)
    
    after((result) => {
      console.log(`Action ${name} returned:`, result)
    })
    
    onError((error) => {
      console.error(`Action ${name} failed:`, error)
    })
  })
})

实用插件示例 #

1. 日志插件 #

ts
// plugins/logger.ts
import type { PiniaPluginContext } from 'pinia'

export function loggerPlugin({ store }: PiniaPluginContext) {
  // 记录 action 调用
  store.$onAction(({ name, args, after, onError }) => {
    const startTime = Date.now()
    console.log(`[${store.$id}] ${name} started`, args)
    
    after((result) => {
      const duration = Date.now() - startTime
      console.log(`[${store.$id}] ${name} finished in ${duration}ms`, result)
    })
    
    onError((error) => {
      console.error(`[${store.$id}] ${name} failed`, error)
    })
  })
  
  // 记录状态变化
  store.$subscribe((mutation, state) => {
    console.log(`[${store.$id}] State changed via ${mutation.type}`, state)
  })
}
ts
// main.ts
import { loggerPlugin } from './plugins/logger'

const pinia = createPinia()
pinia.use(loggerPlugin)

2. 重置插件 #

ts
// plugins/reset.ts
import type { PiniaPluginContext } from 'pinia'

export function resetPlugin({ store }: PiniaPluginContext) {
  // 保存初始状态
  const initialState = JSON.parse(JSON.stringify(store.$state))
  
  // 添加重置方法
  store.$resetTo = function(path?: string) {
    if (path) {
      this.$patch({ [path]: initialState[path] })
    } else {
      this.$patch(initialState)
    }
  }
}

3. 加载状态插件 #

ts
// plugins/loading.ts
import { ref } from 'vue'
import type { PiniaPluginContext } from 'pinia'

export function loadingPlugin({ store }: PiniaPluginContext) {
  const loadingActions = new Set<string>()
  
  store.$onAction(({ name, after, onError }) => {
    // 只处理异步 action
    loadingActions.add(name)
    store.loading = true
    
    after(() => {
      loadingActions.delete(name)
      if (loadingActions.size === 0) {
        store.loading = false
      }
    })
    
    onError(() => {
      loadingActions.delete(name)
      if (loadingActions.size === 0) {
        store.loading = false
      }
    })
  })
}

4. API 请求插件 #

ts
// plugins/api.ts
import type { PiniaPluginContext } from 'pinia'

interface ApiOptions {
  baseUrl: string
  headers?: Record<string, string>
}

export function apiPlugin(options: ApiOptions) {
  return ({ store }: PiniaPluginContext) => {
    store.$api = {
      async get(endpoint: string) {
        const response = await fetch(`${options.baseUrl}${endpoint}`, {
          headers: options.headers
        })
        return response.json()
      },
      
      async post(endpoint: string, data: any) {
        const response = await fetch(`${options.baseUrl}${endpoint}`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            ...options.headers
          },
          body: JSON.stringify(data)
        })
        return response.json()
      }
    }
  }
}
ts
// main.ts
import { apiPlugin } from './plugins/api'

const pinia = createPinia()
pinia.use(apiPlugin({
  baseUrl: 'https://api.example.com',
  headers: {
    'Authorization': 'Bearer token'
  }
}))
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
  actions: {
    async fetchUsers() {
      this.users = await this.$api.get('/users')
    }
  }
})

5. 验证插件 #

ts
// plugins/validation.ts
import type { PiniaPluginContext } from 'pinia'

interface ValidationRule {
  validate: (value: any) => boolean
  message: string
}

export function validationPlugin({ store, options }: PiniaPluginContext) {
  const rules = options.validation as Record<string, ValidationRule[]> | undefined
  
  if (!rules) return
  
  store.$validate = function() {
    const errors: Record<string, string[]> = {}
    
    for (const [field, fieldRules] of Object.entries(rules)) {
      const value = this[field]
      const fieldErrors: string[] = []
      
      for (const rule of fieldRules) {
        if (!rule.validate(value)) {
          fieldErrors.push(rule.message)
        }
      }
      
      if (fieldErrors.length > 0) {
        errors[field] = fieldErrors
      }
    }
    
    return Object.keys(errors).length === 0 ? null : errors
  }
}
ts
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    email: '',
    age: 0
  }),
  validation: {
    email: [
      { validate: (v) => !!v, message: 'Email is required' },
      { validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: 'Invalid email' }
    ],
    age: [
      { validate: (v) => v >= 0, message: 'Age must be positive' },
      { validate: (v) => v <= 150, message: 'Invalid age' }
    ]
  }
})

6. 历史记录插件 #

ts
// plugins/history.ts
import { ref } from 'vue'
import type { PiniaPluginContext } from 'pinia'

export function historyPlugin({ store }: PiniaPluginContext) {
  const history = ref<any[]>([])
  const currentIndex = ref(-1)
  const maxHistory = 50
  
  // 记录状态变化
  store.$subscribe((mutation, state) => {
    // 移除当前位置之后的历史
    history.value = history.value.slice(0, currentIndex.value + 1)
    
    // 添加新状态
    history.value.push(JSON.parse(JSON.stringify(state)))
    
    // 限制历史长度
    if (history.value.length > maxHistory) {
      history.value.shift()
    }
    
    currentIndex.value = history.value.length - 1
  })
  
  // 添加导航方法
  store.$undo = function() {
    if (currentIndex.value > 0) {
      currentIndex.value--
      this.$patch(history.value[currentIndex.value])
    }
  }
  
  store.$redo = function() {
    if (currentIndex.value < history.value.length - 1) {
      currentIndex.value++
      this.$patch(history.value[currentIndex.value])
    }
  }
  
  store.$canUndo = () => currentIndex.value > 0
  store.$canRedo = () => currentIndex.value < history.value.length - 1
}

插件配置 #

条件应用 #

ts
pinia.use(({ store, options }) => {
  // 只对特定 store 应用插件
  if (options.persist) {
    // 持久化逻辑
  }
})

插件选项 #

ts
interface MyPluginOptions {
  prefix?: string
  debug?: boolean
}

export function myPlugin(options: MyPluginOptions = {}) {
  const { prefix = '', debug = false } = options
  
  return ({ store }: PiniaPluginContext) => {
    if (debug) {
      console.log(`${prefix}Store created: ${store.$id}`)
    }
  }
}

// 使用
pinia.use(myPlugin({ prefix: '[Pinia]', debug: true }))

插件最佳实践 #

1. 类型安全 #

ts
import type { PiniaPluginContext, Store } from 'pinia'

declare module 'pinia' {
  interface PiniaCustomProperties {
    $log: () => void
    $api: {
      get: (endpoint: string) => Promise<any>
      post: (endpoint: string, data: any) => Promise<any>
    }
  }
}

export function myPlugin({ store }: PiniaPluginContext) {
  store.$log = function() {
    console.log(this.$state)
  }
}

2. 清理资源 #

ts
export function myPlugin({ store }: PiniaPluginContext) {
  const interval = setInterval(() => {
    console.log('Polling...')
  }, 1000)
  
  // 返回清理函数
  return () => {
    clearInterval(interval)
  }
}

3. 错误处理 #

ts
export function myPlugin({ store }: PiniaPluginContext) {
  store.$onAction({
    onError(error) {
      // 统一错误处理
      console.error('Action error:', error)
      // 可以发送到错误追踪服务
    }
  })
}

下一步 #

现在你已经掌握了 Pinia 插件系统,接下来让我们学习状态持久化。

最后更新:2026-03-28