Skip to content

第七章:Vue3 生态深度集成

在前几章中,我们深入探讨了 Pinia 的核心机制、插件系统和 SSR 支持。现在,让我们看看 Pinia 如何与 Vue3 生态系统深度集成,包括 Devtools 集成、TypeScript 类型增强、依赖注入等高级主题。

useStore() 如何实现?getCurrentInstance() + inject('pinia')

Pinia 中的 useStore() 函数是获取 store 实例的关键。其实现依赖于 Vue 的依赖注入系统和当前实例获取机制:

typescript
// 简化的 useStore 实现原理
import { getCurrentInstance, inject } from 'vue'
import { Pinia, piniaSymbol } from './rootStore'

function useStore(pinia?: Pinia | null) {
  // 尝试从参数获取 pinia 实例
  if (pinia) {
    return pinia
  }
  
  // 获取当前 Vue 实例
  const instance = getCurrentInstance()
  
  if (instance) {
    // 从组件实例中注入 pinia
    pinia = instance.appContext.provides[piniaSymbol] as Pinia
  }
  
  // fallback 到全局实例(不推荐)
  if (!pinia) {
    throw new Error('Pinia instance not found')
  }
  
  return pinia
}

// 在 defineStore 中使用
export function defineStore(id, options) {
  return function useStore(pinia?: Pinia) {
    // 获取 pinia 实例
    const piniaInstance = getActivePinia(pinia)
    
    // 从 pinia 实例中获取或创建 store
    if (!piniaInstance._s.has(id)) {
      // 创建新的 store 实例
      createAndSetupStore(id, options, piniaInstance)
    }
    
    // 返回 store 实例
    return piniaInstance._s.get(id)
  }
}

依赖注入获取 pinia 实例

Pinia 利用 Vue 的依赖注入机制来管理实例:

typescript
// Pinia 安装过程
export function createPinia() {
  const pinia: Pinia = {
    install(app: App) {
      // 提供 pinia 实例
      app.provide(piniaSymbol, pinia)
      
      // 将 pinia 实例添加到应用配置中
      app.config.globalProperties.$pinia = pinia
      
      // 设置当前激活的 pinia 实例
      setActivePinia(pinia)
    },
    
    // 存储所有 store 实例
    _s: new Map<string, Store>(),
    
    // 存储插件
    _p: [],
    
    // 存储状态
    state: shallowRef({}) as Ref<Record<string, StateTree>>,
    
    // 插件订阅
    _e: true
  }
  
  return pinia
}

// 符号用于依赖注入
const piniaSymbol = Symbol('pinia')

// 设置和获取当前激活的 pinia 实例
let activePinia: Pinia | undefined

export function setActivePinia(pinia: Pinia) {
  activePinia = pinia
}

export function getActivePinia(pinia?: Pinia): Pinia {
  return pinia || activePinia || inject(piniaSymbol)
}

Devtools 集成:如何向 Vue Devtools 发送 action 执行记录?

Pinia 与 Vue Devtools 的集成是其强大调试能力的关键。通过 Devtools API,Pinia 可以发送状态变更和 action 执行记录:

typescript
// 简化的 Devtools 集成实现
function createDevtoolsPlugin() {
  return (context) => {
    const { app, store } = context
    
    // 仅在开发环境启用
    if (process.env.NODE_ENV !== 'production') {
      setupDevtools(app, store)
    }
  }
}

function setupDevtools(app, store) {
  // 初始化 Devtools 连接
  connectDevtools(app)
  
  // 订阅状态变更
  store.$subscribe((mutation, state) => {
    // 发送状态变更事件到 Devtools
    sendToDevtools('state:mutation', {
      storeId: store.$id,
      type: mutation.type,
      payload: mutation.payload,
      state: cloneState(state) // 克隆状态以避免引用问题
    })
  })
  
  // 订阅 action 执行
  store.$onAction((context) => {
    const startTime = Date.now()
    
    // 发送 action 开始事件
    sendToDevtools('action:start', {
      storeId: store.$id,
      action: context.name,
      args: context.args,
      timestamp: startTime
    })
    
    // 订阅成功回调
    context.after((result) => {
      // 发送 action 完成事件
      sendToDevtools('action:end', {
        storeId: store.$id,
        action: context.name,
        duration: Date.now() - startTime,
        result
      })
    })
    
    // 订阅错误回调
    context.onError((error) => {
      // 发送 action 错误事件
      sendToDevtools('action:error', {
        storeId: store.$id,
        action: context.name,
        duration: Date.now() - startTime,
        error: error.message
      })
    })
  })
}

// 实际的 Devtools 集成
function connectDevtools(app) {
  // 这里是与 Vue Devtools 的实际集成代码
  // 通常涉及自定义事件和 API 调用
  app.config.globalProperties.__PINIA_DEVTOOLS__ = {
    // 提供调试 API
  }
}

时间旅行调试支持

Devtools 集成还支持时间旅行调试功能:

typescript
// 时间旅行实现
class PiniaDevtools {
  private history: Array<{ state: any, mutation: any }> = []
  private currentIndex = -1
  
  // 记录状态变更历史
  recordMutation(storeId: string, mutation: any, state: any) {
    // 清除当前点之后的历史(如果有的话)
    if (this.currentIndex < this.history.length - 1) {
      this.history = this.history.slice(0, this.currentIndex + 1)
    }
    
    // 添加新的历史记录
    this.history.push({
      mutation,
      state: cloneState(state)
    })
    
    this.currentIndex = this.history.length - 1
  }
  
  // 回到指定的历史点
  jumpTo(index: number) {
    if (index >= 0 && index < this.history.length) {
      const historyItem = this.history[index]
      this.currentIndex = index
      
      // 恢复状态
      this.restoreState(historyItem.state)
    }
  }
  
  // 恢复状态
  private restoreState(state: any) {
    // 实际的状态恢复逻辑
    // 需要遍历所有 store 并恢复其状态
  }
}

TypeScript 类型增强:如何推导 defineStore 的类型?

Pinia 的 TypeScript 支持是其一大亮点。通过高级类型技巧,Pinia 能够自动推导 store 的类型:

typescript
// 简化的类型定义
type Store<Id, S, G, A> = S & G & A & PiniaCustomProperties

interface DefineStoreOptions<Id, S, G, A> {
  id: Id
  state?: () => S
  getters?: G & ThisType<Readonly<S> & StoreGetters<G>>
  actions?: A & ThisType<A & S & StoreGetters<G> & PiniaCustomProperties>
}

// defineStore 的类型重载
export function defineStore<
  Id extends string,
  S extends StateTree,
  G extends GettersTree<S>,
  A
>(
  options: DefineStoreOptions<Id, S, G, A>
): StoreDefinition<Id, S, G, A>

export function defineStore<Id extends string, SS>(
  id: Id,
  storeSetup: () => SS
): StoreDefinition<Id, SS, {}, {}>

// StoreDefinition 类型
interface StoreDefinition<Id, S, G, A> {
  (): Store<Id, S, G, A>
  $id: Id
}

// 高级类型工具
type StateTree = Record<string | number | symbol, any>

type GettersTree<S extends StateTree> = Record<
  string,
  ((state: S) => any) | (() => any)
>

type StoreGetters<G> = {
  [K in keyof G]: G[K] extends (...args: any[]) => infer R ? R : never
}

// ThisType 的使用示例
interface UserState {
  name: string
  age: number
}

interface UserGetters {
  isAdult: (state: UserState) => boolean
}

interface UserActions {
  setName: (name: string) => void
  setAge: (age: number) => void
}

const useUserStore = defineStore('user', {
  state: (): UserState => ({
    name: '',
    age: 0
  }),
  
  getters: {
    // this 的类型是 UserState
    isAdult(state): boolean {
      return state.age >= 18
    }
  },
  
  actions: {
    // this 的类型是 UserState & UserGetters & UserActions & PiniaCustomProperties
    setName(name: string) {
      this.name = name // 可以访问 state
      console.log(this.isAdult) // 可以访问 getters
    },
    
    setAge(age: number) {
      this.age = age
      this.setName(this.name) // 可以调用其他 actions
    }
  }
})

// 使用时自动推导类型
const userStore = useUserStore()
userStore.name // string 类型
userStore.isAdult // boolean 类型
userStore.setName('John') // 参数类型检查

UnionToIntersection 类型工具

Pinia 使用了一些高级的 TypeScript 类型工具:

typescript
// UnionToIntersection 工具类型
type UnionToIntersection<U> = (
  U extends any ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never

// 使用示例
type A = { a: number }
type B = { b: string }
type C = { c: boolean }

type Intersection = UnionToIntersection<A | B | C>
// 结果: { a: number } & { b: string } & { c: boolean }

// 在 Pinia 中的应用
type StoreActions<Actions> = {
  [K in keyof Actions]: Actions[K] extends (...args: infer Args) => infer Return
    ? (...args: Args) => Return
    : never
}

Tree-shaking 友好:每个 store 独立,未引用的 store 不会打包

Pinia 的设计天然支持 Tree-shaking,只有被实际使用的 store 才会被打包:

typescript
// stores/user.js
export const useUserStore = defineStore('user', () => {
  // 用户 store 实现
})

// stores/product.js
export const useProductStore = defineStore('product', () => {
  // 产品 store 实现
})

// 在组件中只使用用户 store
import { useUserStore } from '@/stores/user'

export default {
  setup() {
    const userStore = useUserStore()
    // useProductStore 未被导入,会被 Tree-shaking 移除
    return { userStore }
  }
}

与 Vue Router 的集成

Pinia 与 Vue Router 可以很好地协同工作:

typescript
// 路由级别的状态管理
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'

export default {
  setup() {
    const route = useRoute()
    const userStore = useUserStore()
    
    // 监听路由变化并更新状态
    watch(
      () => route.params.id,
      (userId) => {
        if (userId) {
          userStore.fetchUser(userId)
        }
      },
      { immediate: true }
    )
    
    return { userStore }
  }
}

与 Suspense 的集成

Pinia 与 Vue 3 的 Suspense 组件可以很好地配合:

typescript
// 异步 store 与 Suspense
export const useAsyncUserStore = defineStore('async-user', () => {
  const user = ref(null)
  
  // 返回 Promise 的 action
  async function fetchUser() {
    const response = await fetch('/api/user')
    user.value = await response.json()
  }
  
  return { user, fetchUser }
})

// 在组件中使用
export default {
  async setup() {
    const userStore = useAsyncUserStore()
    
    // 在 Suspense 中使用
    await userStore.fetchUser()
    
    return { userStore }
  }
}

// 父组件中使用 Suspense
<template>
  <Suspense>
    <template #default>
      <UserProfile />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

小结

在本章中,我们深入探讨了 Pinia 与 Vue3 生态的深度集成:

  1. useStore() 通过 getCurrentInstance()inject() 获取 Pinia 实例
  2. 与 Vue Devtools 的深度集成,支持状态变更和 action 执行记录
  3. 时间旅行调试功能的实现原理
  4. 高级 TypeScript 类型推导,包括 ThisType 和泛型约束
  5. Tree-shaking 友好设计,未使用的 store 会被自动移除
  6. 与 Vue Router 和 Suspense 的良好集成

通过这些深度集成,Pinia 成为了 Vue3 生态中不可或缺的状态管理解决方案。


思考题

  1. 你在项目中是否使用过 Pinia 的 Devtools 功能?它对你的开发效率有什么帮助?
  2. 对于复杂的 TypeScript 类型推导,你通常如何调试和验证类型是否正确?