美文网首页vuex pinia
pinia 新一代的vue状态管理

pinia 新一代的vue状态管理

作者: 林哥学前端 | 来源:发表于2021-10-29 11:58 被阅读0次

    0.前言

    本来vue全家桶系列本来打算写个vuex的教程的,但是现在有了新的pinia,咱们就来学习新的pinia
    现在正好pinia还没有中文的官方文档,我就试着翻译一下
    我现在开始的是日期是2021年10月29日,
    我安装的pinia的版本是2.0.0
    下面就正式开始学习pinia了

    1.简介

    pinia在2019年11月开始时候是一个实验项目,目的就是重新设计一个与组合API匹配的vue状态存储。基本原则和原来还是一样的,pinia同时支持vue2和vue3,比不要求你必须使用组合API。不管是使用vue2或者vue3,pinia的API是相同的,文档是基于vue3写的,同时在需要的地方也标注了vue2的用法,所以不管你是使用vue2还是vue3开发,都能够通过这个文档来学习。

    你为什么应该使用pinia

    pinia是一个vue的状态存储库,你可以使用它来存储、共享一些跨组件或者页面的数据。如果你对组合API很熟,你也许会想到你可以使用组合API来做一个简单的全局共享状态存储,像这样

    export const state = reactive({})
    

    对于spa来说确实可以这么用,如果是服务端渲染(ssr)那么这样会给你的项目带来安全风险。
    但是,就算是spa,你使用pinia也可以有很多优势:

    dev tools支持

    有跟踪action和mutation的时间轴
    按需导入状态存储
    可进行时光旅行调试,而且调试更方便

    热更新

    不刷新页面更新你的状态
    保持在开发中已有的状态

    插件:使用插件来扩展pinia的功能
    更好的ts支持和代码自动补全
    支持ssr

    基础示例

    下面就是使用pinia的一个例子(一定要看后面的文档,从头学习)。这样你就创建了一个状态存储。

    // stores/counter.js
    import { defineStore } from 'pinia'
    
    export const useCounterStore = defineStore('counter', {
      state: () => {
        return { count: 0 }
      },
      // 也可以这样定义状态
      // state: () => ({ count: 0 })
      actions: {
        increment() {
          this.count++
        },
      },
    })
    

    在组件中使用:

    import { useCounterStore } from '@/stores/counter'
    
    export default {
      setup() {
        const counter = useCounterStore()
    
        counter.count++
        // 编辑器会有代码提示 ✨
        counter.$patch({ count: counter.count + 1 })
        // 也可以使用action来代替
        counter.increment()
      },
    }
    

    你甚至可以用一个函数(像setup函数一样)来定义你的store:

    export const useCounterStore = defineStore('counter', () => {
      const count = ref(0)
      function increment() {
        count.value++
      }
    
      return { count, increment }
    })
    

    如果你不是很喜欢setup函数和组合API,不用担心,pinia也有类似vuex的map的功能。你可以用上面的方式定义你的store,但是使用时用mapStores(), mapState(),或者 mapActions():

    const useCounterStore = defineStore('counter', {
      state: () => ({ count: 0 }),
      getters: {
        double: (state) => state.count * 2,
      },
      actions: {
        increment() {
          this.count++
        }
      }
    })
    
    const useUserStore = defineStore('user', {
      // ...
    })
    
    export default {
      computed: {
        // 其他计算属性
        // ...
        // 可以使用 this.counterStore 和 this.userStore获取
        ...mapStores(useCounterStore, useUserStore)
        // 可以使用 this.count 和this.double获取
        ...mapState(useCounterStore, ['count', 'double']),
      },
      methods: {
        // 可以使用 this.increment()调用
        ...mapActions(useCounterStore, ['increment']),
      },
    }
    

    你可以在核心概念中发现map的更多使用方法。

    为什么名字是pinia

    pinia(在英语中发音类似/peenya/)是和pina(西班牙语的pineapple)最近接的词(pineapple是菠萝的意思,译者注)。刚好别人没有使用过这个包名。菠萝实际上是好多独立的花聚合在一起形成了一个水果。这和状态管理类似,每一个状态都是独立的,但是最后要把它们聚合在一起。菠萝本身是一种原产自南美的很好吃的热带水果。

    一个更完整的例子

    下面是一个更完整的pinia的例子。对于一些人来说不用看更深入的文档,直接看这个例子就可以学会pinia了,但是我们还是推荐看完整个文档,甚至可以跳过不看这个例子,等你看完核心概念再返回来这个例子。

    import { defineStore } from 'pinia'
    
    export const todos = defineStore('todos', {
      state: () => ({
        /** @type {{ text: string, id: number, isFinished: boolean }[]} */
        todos: [],
        /** @type {'all' | 'finished' | 'unfinished'} */
        filter: 'all',
        // 累心会被自动推断为number类型
        nextId: 0,
      }),
      getters: {
        finishedTodos(state) {
          // 会有代码自动补全! ✨
          return state.todos.filter((todo) => todo.isFinished)
        },
        unfinishedTodos(state) {
          return state.todos.filter((todo) => !todo.isFinished)
        },
        /**
         * @returns {{ text: string, id: number, isFinished: boolean }[]}
         */
        filteredTodos(state) {
          if (this.filter === 'finished') {
            // 调用其他的getter,同样会有代码自动补全 ✨
            return this.finishedTodos
          } else if (this.filter === 'unfinished') {
            return this.unfinishedTodos
          }
          return this.todos
        },
      },
      actions: {
        // 传入任意参数,可以返回promise,也可以不返回
        addTodo(text) {
          // 可以直接更改状态
          this.todos.push({ text, id: this.nextId++, isFinished: false })
        },
      },
    })
    

    与vuex对比

    pinia尽量和vuex的理念保持一致。我们设计它的目的就是为下一代vuex做一个试验,pinia很成功。所以我们设计的vuex5的API和pinia很类似,现在已经有RFC了。我(Eduardo),pinia的作者,也是vue核心团队的一名成员,在Router和vuex中都有我设计的API。我做这个项目的个人目的就是为了重新设计一个符合vue理念的全局状态管理。我让pinia的API尽量接近vuex,这样以后pinia的用户转到vuex的时候更简单,甚至这两个项目以后可能会合并成为一个(Vuex)。

    RFC

    vuex是通过RFC在社区得到很多反馈的,pinia并不是这样。我是基于我的开发经验、阅读别人的代码、与使用pinia的人交流和在Discord上回答问题来设计pinia的。这样,我可以使pinia善于处理各种情况、适用于大小项目。我经常更新,并且在保持API不变的同时,pinia的内部代码不断地提升。

    与vuex 3.x/4.x对比

    vuex 3.x对应的是vue2,vuex 4.x对应的是vue3

    与vue4之前的版本相比,pinia的API是有很多不同的,即:

    • 去掉了mutation。因为好多人认为mutation是多余的。以前它方便devtools集成,现在这不是个问题了。
    • 不用在写复杂的ts类型包装,所有的都是有类型的,API设计的都是尽量符合ts的类型推断
    • 不再使用一个莫名其妙的字符串了,只需要导入一个函数,调用他们就行了,同时还有代码自动补全
    • 不需要动态添加store了,因为它们现在本来就是动态。如果你想,你随时可以手动去写一个store。
    • 没有复杂的嵌套模块了。你仍然可以在一个store中导入其他的store来实现嵌套模块,但是pinia还是推荐使用一个扁平的结构。但是即使你使用循环依赖也没关系。
    • 不再需要命名空间了。因为现在store本来就是扁平结构了。你也可以理解为所有的store本来就有命名空间了。

    开始

    安装

    你可以使用你喜欢的包管理工具安装pinia

    yarn add pinia
    # 或者使用npm
    npm install pinia
    

    提示
    如果你使用的是vue2,你要安装@vue/composition-api,如果你使用的是nuxt,你需要看这里

    如果你使用vue cli,你可以试一试这个非官方插件

    创建一个pinia(根store),并且把它传给app:

    import { createPinia } from 'pinia'
    
    app.use(createPinia())
    

    如果你使用的是vue2,你需要安装一个插件,并且把创建的pinia传给根app:

    import { createPinia, PiniaVuePlugin } from 'pinia'
    
    Vue.use(PiniaVuePlugin)
    const pinia = createPinia()
    
    new Vue({
      el: '#app',
      // 其他选项...
      // ...
      // 注意,一个pinia实例可以在同一个页面的多个vue app中共用
      pinia,
    })
    

    这样可以通过devtools调试了。在vue3中,有些特性,比如时间旅行调试和编辑还不支持,因为devtools还没有开发相关的API,但是devtools还有很多其他特性,开发体验会很好。在vue2中,pinia用的是vuex的现成的接口。

    什么是store(状态存储)?

    像pinia这样的状态存储就是要保存一些状态和一些业务逻辑,并且要与你的组件树解耦。换句话说,它要保存全局数据。就好像有一个公共数据组件,其他的组件都可以从它那里读取数据和更改数据。它有三个核心概念,state、getters和actions。就好比是组件中的data、computed和methods。

    什么情况下你需要用store?

    你的应用中的全局数据需要保存在store中。在很多地方你都要使用这些数据,比如说,用户信息需要在导航栏中显示,也需要在个人中心显示。还有些数据,需要暂存起来,比如一个需要分好几页填写的表单。
    另一方面,一些只有在一个页面用的局部数据就不要放到全局的store中,比如一个页面上某个弹窗显示不显示,就没有必要放在store中了。
    不是所有的应用都需要全局状态管理,但是如果你需要使用,pinia会是一个不错的选择。

    核心概念

    定义store

    在开始学习核心概念之前,我们需要知道store是通过defineStore()方法定义的,它的第一个参数就是一个唯一的名字:

    import { defineStore } from 'pinia'
    
    // useStore 可以定义为其他的名字,比如 useUser, useCart
    //第一个参数是store的名字,在整个app中它必须是唯一的
    export const useStore = defineStore('main', {
      // other options...
    })
    

    名字,可以说是id,它是必填的,pinia就是用名字来连接store和devtools的。使用useXXX名字defineStore返回的函数是一个通用习惯。

    使用store

    我们上面只是定义了store,在setup函数中调用了useStore()时,才会创建store:

    import { useStore } from '@/stores/counter'
    
    export default {
      setup() {
        const store = useStore()
    
        return {
          // 你可以返回store这个对象,然后就可以在template中使用了
          store,
        }
      },
    }
    

    你想定义多少个store都可以,不过你最好给每一个store新建一个文件(这样在打包时可以更好的进行代码分割)。
    如果你不使用setup函数,你可以使用map方式

    在store实例化以后,你就可以获取到store中定义的state、getters和actions了。这些我们后面会学习,并且会有代码自动补全。

    记着store是一个reactive响应式的对象,所以不用写.value。像setup中props一样,我们不可以解构它:

    export default defineComponent({
      setup() {
        const store = useStore()
        // ❌ 这样不可以,因为会失去响应性
        // 和解构props是一样的`
        const { name, doubleCount } = store
    
        name // "eduardo"
        doubleCount // 2
    
        return {
          // 一直是 "eduardo"
          name,
          // 一直是  2
          doubleCount,
          // 这个值是响应式的
          doubleValue: computed(() => store.doubleCount),
          }
      },
    })
    

    为了让解构的值还保持响应式,你需要用到storeToRefs()方法。它会给响应式的数据创建ref。如果你只使用store中的stata不调用action,这么写很简单:

    import { storeToRefs } from 'pinia'
    
    export default defineComponent({
      setup() {
        const store = useStore()
        // `name` 和 `doubleCount` 是响应式的
        // 插件增加的属性也会创建ref
        // 但是会自动跳过action或者不是响应性的属性
        const { name, doubleCount } = storeToRefs(store)
    
        return {
          name,
          doubleCount
        }
      },
    })
    

    state

    大多数时候,state是store的中心。大家一般都是从定义state开始写store的。在pinia中,是调用一个函数来返回初始的state。这样pinia既可以在客户端运行,就可以在服务端运行。

    import { defineStore } from 'pinia'
    
    const useStore = defineStore('storeId', {
      // 推荐使用箭头函数
      state: () => {
        return {
          // 这些属性都会自动推断类型
          counter: 0,
          name: 'Eduardo',
          isAdmin: true,
        }
      },
    })
    

    提示
    如果你使用的是vue2,在初始化state的方式和vue组件中data的方式一样,比如,state对象一定是扁平的,而且如果你要给它加新的属性的话,需要调用Vue.set()

    获取state

    默认情况下,你可以在store实例上直接获取或者修改state:

    const store = useStore()
    
    store.counter++
    

    重置state

    你可以调用$reset()方法来把state恢复为初始值:

    const store = useStore()
    
    store.$reset()
    

    选项API示例

    如果你不使用组合API,而使用computed、methods。。。你可以使用mapState(),获取state的值:

    import { mapState } from 'pinia'
    
    export default {
      computed: {
        // 在组件中可以是用this.counter获取
        // 和使用store.counter获取一样
        ...mapState(useStore, ['counter'])
        // 和上面一样,不过使用了别名this.myOwnName获取
        ...mapState(useStore, {
          myOwnName: 'counter',
          // 你也可以写一个方法
          double: store => store.counter * 2,
          // 可以访问this指针,但是不能自动推断类型了
          magicValue(store) {
            return store.someGetter + this.counter + this.double
          },
        }),
      },
    }
    
    可修改的state

    如果你想修改state里面的属性(比如在你的表单中),你可以使用mapWritableState()。注意不能像使用mapState()一样传递函数:

    import { mapWritableState } from 'pinia'
    
    export default {
      computed: {
        // 可以使用this.counter修改它的值
        // this.counter++
        // 和使用store.counter获取它的值一样
        ...mapWritableState(useStore, ['counter'])
        // 使用别名也一样this.myOwnName
        ...mapWritableState(useStore, {
          myOwnName: 'counter',
        }),
      },
    }
    

    提示
    如果是数组,修改时不必要使用mapWritableState(),除非你要改变它的指针cartItems = [],使用mapState()时,你可以调用数组的方法。

    改变state

    除了直接修改store里的值store.counter++,你也可以是用$patch方法。你可以同时修改多个值:

    store.$patch({
      counter: store.counter + 1,
      name: 'Abalam',
    })
    

    但是,这么写有时不方便,有时太消耗性能,比如说你要修改一个数组时,还得新建一个数组。出于这个原因,$patch方可可以接收一个函数作为参数,来简化改变数组的写法:

    cartStore.$patch((state) => {
      state.items.push({ name: 'shoes', quantity: 1 })
      state.hasChanged = true
    })
    

    这么写还有一个好处,就是这一组改变在devtools中查看时,是一次改变。注意,直接改变state和使用$patch方法,都可以在devtools里面查看,都可以实现时间旅行(在vue3中暂时不能).

    替换state

    你可以通过store的$state属性,整个替换state对象:

    store.$state = { counter: 666, name: 'Paimon' }
    

    你也可以使用pinia实例的state属性替换你的应用的全局state。可以在ssr中使用:

    pinia.state.value = {}
    

    订阅state的改变

    你可以用$subscribe()来侦听state的改变,和vuex的subscribe
    方法类似。

    cartStore.$subscribe((mutation, state) => {
      // import { MutationType } from 'pinia' 改变触发的类型
      mutation.type // 'direct' | 'patch object' | 'patch function'
      // same as cartStore.$id
      mutation.storeId // 'cart'
      // only available with mutation.type === 'patch object'
      mutation.payload // patch object passed to cartStore.$patch()
    
      // 侦听到state变化时,把state存在localStorage中
      localStorage.setItem('cart', JSON.stringify(state))
    })
    

    默认情况下,state侦听会和组件绑定在一起(如果store是在组件的setup中)。这意味着,当组件卸载时,侦听会自动被移除。如果你需要在组件被卸载时,侦听仍然保持,需要给$subscribe()方法传递第二个参数true:

    export default {
      setup() {
        const someStore = useSomeStore()
    
        // 组件卸载后,侦听也会有
        someStore.$subscribe(callback, true)
    
        // ...
      },
    }
    

    提示
    你可以在pinia实例上侦听整个state

    watch(
      pinia.state,
      (state) => {
        // 在state改变时,保存在localStorage中
        localStorage.setItem('piniaState', JSON.stringify(state))
      },
      { deep: true }
    )
    

    Getters

    getters就相当于的state的计算属性。可以在defineStore()方法中的getters属性中定义它们。getter的第一个参数就是state,可以是用箭头函数来定义:

    export const useStore = defineStore('main', {
      state: () => ({
        counter: 0,
      }),
      getters: {
        doubleCount: (state) => state.counter * 2,
      },
    })
    

    大多数情况下,getter只依赖于state的值,但是,有时也会依赖于其他getter。所以,在getter中可以用this指针访问store对象,这是要使用普通的function来定义getter,如果使用ts,记得定义返回值的类型。

    export const useStore = defineStore('main', {
      state: () => ({
        counter: 0,
      }),
      getters: {
        // 可以自动推断返回值类型是数字
        doubleCount(state) {
          return state.counter * 2
        },
        // 这是必须指定返回值类型(ts)
        doublePlusOne(): number {
          return this.counter * 2 + 1
        },
      },
    })
    

    你可以在store实例上直接获取getter:

    <template>
      <p>Double count is {{ store.doubleCount }}</p>
    </template>
    
    <script>
    export default {
      setup() {
        const store = useStore()
    
        return { store }
      },
    }
    </script>
    

    在getter中访问其他getter

    就想计算属性一样,getter也可以通过this指针访问其他的getter。即使你不使用ts,你也可以写JSDoc,这样你的idea就可以知道数据的类型了:

    export const useStore = defineStore('main', {
      state: () => ({
        counter: 0,
      }),
      getters: {
        // 因为没有用this,可以正确推断类型
        doubleCount: (state) => state.counter * 2,
        //  也可以写JSDoc来说明类型
        /**
         * 返回的值是counter乘以2再加1
         *
         * @returns {number}
         */
        doubleCountPlusOne() {
          // 会有代码自动补全 ✨
          return this.doubleCount + 1
        },
      },
    })
    

    给getter传递参数

    getter本来就是计算属性,所以不能给它传递参数。但是,你可以在getter中返回一个函数,这个函数可以接收参数:

    export const useStore = defineStore('main', {
      getters: {
        getUserById: (state) => {
          return (userId) => state.users.find((user) => user.id === userId)
        },
      },
    })
    

    在组件中使用:

    <script>
    export default {
      setup() {
        const store = useStore()
    
        return { getUserById: store.getUserById }
      },
    }
    </script>
    
    <template>
    User 2: {{ getUserById(2) }}
    </template>
    

    需要注意的是,这种情况下,getter的结果不会被缓存了,它们只是你调用的一个函数了。不过你自己手动在你的getter中缓存结果,当然这个做法不是很常见,而且消耗更多性能:

    export const useStore = defineStore('main', {
      getters: {
        getActiveUserById(state) {
          const activeUsers = state.users.filter((user) => user.active)
          return (userId) => activeUsers.find((user) => user.id === userId)
        },
      },
    })
    

    获取其他store实例中的getter

    你可以在getter中直接获取其他store中的getter:

    import { useOtherStore } from './other-store'
    
    export const useStore = defineStore('main', {
      state: () => ({
        // ...
      }),
      getters: {
        otherGetter(state) {
          const otherStore = useOtherStore()
          return state.localData + otherStore.data
        },
      },
    })
    

    在setup中使用

    就行state一样,你可以通过store直接获取getter:

    export default {
      setup() {
        const store = useStore()
    
        store.counter = 3
        store.doubleCount // 6
      },
    }
    

    在选项API中使用

    你可以像之前学习过的使用mapState()方法,获取getter:

    import { mapState } from 'pinia'
    
    export default {
      computed: {
        // 在这个组件中可以这样访问this.doubleCounter 
        ...mapState(useStore, ['doubleCount'])
        // 使用别名 this.myOwnName
        ...mapState(useStore, {
          myOwnName: 'doubleCounter',
          // 也可以写一个function,参数可以获取到store对象
          double: store => store.doubleCount,
        }),
      },
    }
    

    actions

    action相当于组件中的methods。可以在defineStore()方法中定义action。我们应该在业务逻辑定义在action中:

    export const useStore = defineStore('main', {
      state: () => ({
        counter: 0,
      }),
      actions: {
        increment() {
          this.counter++
        },
        randomizeCounter() {
          this.counter = Math.round(100 * Math.random())
        },
      },
    })
    

    就行getter一样,action可以使用this指针访问整个store实例,并且有类型推断和代码补全。不同的是,action支持异步,你可以在action内部去调用后台接口,甚至调用其他action。下面是一个使用Mande调用接口的例子。注意,你用哪个库没关系,只要返回的是一个Promise,你甚至可以直接使用原生的fetch:

    import { mande } from 'mande'
    
    const api = mande('/api/users')
    
    export const useUsers = defineStore('users', {
      state: () => ({
        data: userData,
        // ...
      }),
    
      actions: {
        async registerUser(login, password) {
          try {
            this.userData = await api.post({ login, password })
            showTooltip(`Welcome back ${this.userData.name}!`)
          } catch (error) {
            showTooltip(error)
            // 显示错误提示
            return error
          }
        },
      },
    })
    

    你可以随意的给action定义参数和返回值。调用action时,所有值都会正确地推断类型。
    调用action就像调用methods一样:

    export default defineComponent({
      setup() {
        const main = useMainStore()
        // 调用action就像调用methods一样
        main.randomizeCounter()
    
        return {}
      },
    })
    

    在action中访问其他的store

    可以在action中直接使用其他的store:

    import { useAuthStore } from './auth-store'
    
    export const useSettingsStore = defineStore('settings', {
      state: () => ({
        // ...
      }),
      actions: {
        async fetchUserPreferences(preferences) {
          const auth = useAuthStore()
          if (auth.isAuthenticated) {
            this.preferences = await fetchPreferences()
          } else {
            throw new Error('User must be authenticated')
          }
        },
      },
    })
    

    在setup中使用

    你可以直接调用action,就行调用一个方法一样:

    export default {
      setup() {
        const store = useStore()
    
        store.randomizeCounter()
      },
    }
    

    在选项API中使用

    如果你不使用组合API,而使用computed、methods。。。你可以使用在methods中使用mapAction(),这样就可以调用action了:

    import { mapActions } from 'pinia'
    
    export default {
      methods: {
        // 可以在组件中使用this.increment() 调用
        // 和使用store实例调用是相同的 store.increment()
        ...mapActions(useStore, ['increment'])
        // 使用别名 this.myOwnName()
        ...mapActions(useStore, { myOwnName: 'doubleCounter' }),
      },
    }
    

    订阅action

    可以是用store.$onAction()方法侦听、订阅action的调用与结果。在action调用以前就会调用这个回调。你可以在after回调中改变action的返回结果。在onError回调中你可以处理错误。这样你就可以在运行时追踪错误了,跟vue中类似。
    下面是一个例子,在action调用前和调用后输出了一些内容:

    const unsubscribe = someStore.$onAction(
      ({
        name, // action的名字
        store, // store的实例
        args, // action的参数数组
        after, //action调用完成后 return或者resolved
        onError, // 抛出错误或者reject
      }) => {
        // 在这里可以定义一些这几回调都可以访问的公有变量
        const startTime = Date.now()
        // 在action执行前,会执行这里
        console.log(`Start "${name}" with params [${args.join(', ')}].`)
    
        // action调用成功会执行after
        // 它会等待promise完成
        after((result) => {
          console.log(
            `Finished "${name}" after ${
              Date.now() - startTime
            }ms.\nResult: ${result}.`
          )
        })
    
        // 发送错误或者promise reject时调用
        onError((error) => {
          console.warn(
            `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
          )
        })
      }
    )
    
    // 手动移除侦听
    unsubscribe()
    

    默认情况下,actions的侦听是在组件初始化是加上的(如果store是在一个组件的setup中使用)。这意味着组件卸载时,侦听会自动被移除。如果你想要组件卸载时,不移除侦听,在组件中调用$onAction时加上第二个参数true:

    export default {
      setup() {
        const someStore = useSomeStore()
    
        // this subscription will be kept after the component is unmounted
        someStore.$onAction(callback, true)
    
        // ...
      },
    }
    

    插件

    归功于pinia的low level API,pinia可以支持全面的拓展。下面是你可以扩展的:

    • 给store增加新属性
    • 在定义store时增加新选项
    • 给store增加新方法
    • 包装现有的方法
    • 改变、甚至取消action
    • 实现其他功能,比如本地存储
    • 给特定store添加功能

    使用pinia.use()给pinia实例添加插件。下面是一个最简单的例子,给所有store添加一个静态的属性,这个属性返回一个对象:

    import { createPinia } from 'pinia'
    
    // 在插件被使用后,给所有的store增加了一个secret的属性
    // 这段代码可以放在一个单独的文件里
    function SecretPiniaPlugin() {
      return { secret: 'the cake is a lie' }
    }
    
    const pinia = createPinia()
    // 使用use方法添加插件
    pinia.use(SecretPiniaPlugin)
    
    // 在另外一个文件里定义store
    const store = useStore()
    store.secret // 'the cake is a lie'
    

    这个方式很有用,可以添加全局的路由、模态框或者toast。

    介绍

    pinia的插件就是一个方法,它返回的对象会添加在store对象上。它有一个参数context,作为选项:

    export function myPiniaPlugin(context) {
      context.pinia // 通过`createPinia()`创建的pinia实例
      context.app //  `createApp()` 创建的vueApp的实例(Vue 3 )
      context.store // store的实例
      context.options // 调用`defineStore()`时的选项
      // ...
    }
    

    然后把这个方法传递给pinia.use():

    pinia.use(myPiniaPlugin)
    

    只有在pinia实例传给vue app时,插件才会应用在store中,其他情况它们不会起作用。

    增强store

    在插件里返回一个对象,然后每个store都会增加这个属性:

    pinia.use(() => ({ hello: 'world' }))
    

    你也可以在store对象上直接添加属性,但是还是尽量使用返回对象的方式,因为这样可以被devtools自动追踪:

    pinia.use(({ store }) => {
      store.hello = 'world'
    })
    

    插件返回的任何属性都可以被devtools自动追踪,这样在devtools中就可以看到‘hello’属性了。
    如果是直接在store上定义新属性,一定要调用store._customProperties,这样才能在devtools中调试这个属性:

    pinia.use(({ store }) => {
      store.hello = 'world'
      if (process.env.NODE_ENV === 'development') {
        store._customProperties.add('secret')
      }
    })
    

    注意,store都是被reactive包装的,它会自动解包任何的ref类型(ref(), computed(), ...):

    const sharedRef = ref('shared')
    pinia.use(({ store }) => {
      // 每个store都有独立的hello属性
      store.hello = ref('secret')
      // 自动解包
      store.hello // 'secret'
    
      // 所有的的store都会有shared这个属性
      store.shared = sharedRef
      store.shared // 'shared'
    })
    

    这就是为什么你可以不使用.value访问所有的属性,并且他们是reactive。

    增加新的state

    如果你要给store增加新的state,你有两种方式:

    • 你可以使用store.myState的方式
    • 使用store.$state的方式,可以在devtools和ssr中生效
      记着,这样你可以共享一个ref或者computed属性:
    const globalSecret = ref('secret')
    pinia.use(({ store }) => {
      // `secret` 属性在所有的store中共享
      store.$state.secret = globalSecret
      store.secret = globalSecret
      // 自动解包
      store.secret // 'secret'
    
      const hasError = ref(false)
      store.$state.hasError = hasError
      // 必须这么写
      store.hasError = toRef(store.$state, 'hasError')
    
      // 这时最好不要return ‘hasError’,因为这样在devtools中会显示两次
    })
    

    警告
    如果你使用的是vue2,你需要使用@vue/composition-api的set方法来添加新的属性:

    import { set } from '@vue/composition-api'
    pinia.use(({ store }) => {
      if (!store.$state.hasOwnProperty('hello')) {
        const secretRef = ref('secret')
        // 如果你使用ssr,需要在$state上定义它
        set(store.$state, 'secret', secretRef)
        // 在store对象上直接设置
        // 两种方式: `store.$state.secret` / `store.secret`
        set(store, 'secret', secretRef)
        store.secret // 'secret'
      }
    })
    

    添加外部属性

    当在给pinia添加外部属性时,比如其他库的实例对象,或者不是reactive的属性,你需要用markRaw()方法先包裹它们,然后再传给pinia。下面是吧router对象添加给所有的store:

    import { markRaw } from 'vue'
    import { router } from './router'
    
    pinia.use(({ store }) => {
      store.router = markRaw(router)
    })
    

    在插件内部调用$subscribe

    你可以在插件里面调用store.subscribe和store.onAction方法:

    pinia.use(({ store }) => {
      store.$subscribe(() => {
        // store变化时调用
      })
      store.$onAction(() => {
        // action触发时调用
      })
    })
    

    添加新的选项

    可以在插件中给定义store时增加新的选项。例如,你可以新增一个debounce选项,这样你可以在action调用时实现防抖:

    defineStore('search', {
      actions: {
        searchContacts() {
          // ...
        },
      },
    
      // 这个属性之后会在插件里用到
      debounce: {
        //  给searchContacts这个action加了300毫秒防抖
        searchContacts: 300,
      },
    })
    

    插件可以读取选项,包装action,并且替换原来那个:

    // 导入一个debounce方法
    import debounce from 'lodash/debunce'
    
    pinia.use(({ options, store }) => {
      if (options.debounce) {
        // 我们新的action会替换原来的
        return Object.keys(options.debounce).reduce((debouncedActions, action) => {
          debouncedActions[action] = debounce(
            store[action],
            options.debounce[action]
          )
          return debouncedActions
        }, {})
      }
    })
    

    注意,在使用setup语法时,自定义的选项时通过第三个参数传的:

    defineStore(
      'search',
      () => {
        // ...
      },
      {
        // 这个值会在插件中使用
        debounce: {
          // 给searchContacts这个action加了300毫秒防抖
          searchContacts: 300,
        },
      }
    )
    

    TypeScript

    上面这些代码都可以使用ts来写,所以你就不需要使用any或者@ts-ignore了。

    类型化插件

    类型化插件可以这么写:

    import { PiniaPluginContext } from 'pinia'
    
    export function myPiniaPlugin(context: PiniaPluginContext) {
      // ...
    }
    

    类型化新的store属性

    给store添加属性时,需要使用PiniaCustomProperties接口:

    import 'pinia'
    
    declare module 'pinia' {
      export interface PiniaCustomProperties {
        // 在setter中,允许string类型和ref类型
        set hello(value: string | Ref<string>)
        get hello(): string
    
        // 你也可以这样简单的定义一个值
        simpleNumber: number
      }
    }
    

    这些数据可以安全地修改和获取:

    pinia.use(({ store }) => {
      store.hello = 'Hola'
      store.hello = ref('Hola')
    
      store.number = Math.random()
      // @ts-expect-error:  这个类型不正确
      store.number = ref(Math.random())
    })
    

    PiniaCustomProperties允许你可以拿到store的属性。拿下面的例子来说,我们把原来的options复制一份,叫做$options:

    pinia.use(({ options }) => ({ $options: options }))
    

    我们可以使用PiniaCustomProperties让它们有正确的类型:

    import 'pinia'
    
    declare module 'pinia' {
      export interface PiniaCustomProperties<Id, S, G, A> {
        $options: {
          id: Id
          state?: () => S
          getters?: G
          actions?: A
        }
      }
    }
    

    提示
    当拓展这些基本类型时,它们必须和源码中的命名一致。Id不能命名为id或者I,S不能被命名为State。下面是这些简写对应的单词:

    • S: State
    • G: Getters
    • A: Actions
    • SS: Setup Store / Store

    类型化新的state

    当添加新的state属性属性时(不管是store还是store.$state),你需要在PiniaCustomStateProperties中添加。和PiniaCustomProperties不同,它只接受State类型:

    import 'pinia'
    
    declare module 'pinia' {
      export interface PiniaCustomStateProperties<S> {
        hello: string
      }
    }
    

    类型化新的选项

    在给defineStore()增加新的选项时,你需要使用DefineStoreOptionsBase。和PiniaCustomProperties不同,它只有两个类型:State类型和Store类型,用来让你去限制都可以定义哪些内容。例如,你可以使用action的名字:

    import 'pinia'
    
    declare module 'pinia' {
      export interface DefineStoreOptionsBase<S, Store> {
        // 可以为任何的action定义一个数字
        debounce?: Partial<Record<keyof StoreActions<Store>, number>>
      }
    }
    

    提示
    getter也有一个对应的StoreGetters。你也可以使用DefineStoreOptions、DefineSetupStoreOptions来拓展store的选项。

    Nuxt.js

    在Nuxt中使用pinia时,你需要使用Nuxt plugin。这样你就可以获取pinia实例了:

    // plugins/myPiniaPlugin.js
    import { PiniaPluginContext } from 'pinia'
    import { Plugin } from '@nuxt/types'
    
    function MyPiniaPlugin({ store }: PiniaPluginContext) {
      store.$subscribe((mutation) => {
        // 在store变化时打印
        console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}.`)
      })
    
      return { creationTime: new Date() }
    }
    
    const myPlugin: Plugin = ({ pinia }) {
      pinia.use(MyPiniaPlugin);
    }
    export default myPlugin
    

    注意,上面使用的是ts,如果你使用的是js,你要把PiniaPluginContext和Plugin的类型声明去掉。

    在组件之外使用store

    pinia依靠的是pinia实例来共享同一个store实例。大多数情况下,你可以使用useStore()方法来获取store实例。
    例如,在setup函数中,你不需要再做别的了。但是如果是在组件之外,会有一些不同。其实,useStore()会被自动注入你的app的pinia实例。这意味着,如果pinia实例不能被自动注入时,你必须手动把它传给useStore()。你可以用多种方式解决这个问题,这取决于你在做的应用是什么类型的。

    单页应用

    如果你不使用ssr,在你调用app.use(pinia)后,你就可以直接使用useStore()方法了。

    import { useUserStore } from '@/stores/user'
    import { createApp } from 'vue'
    import App from './App.vue'
    
    // ❌  失败,因为pinia实例还没有
    const userStore = useUserStore()
    
    const pinia = createPinia()
    const app = createApp(App)
    app.use(pinia)
    
    // ✅ 成功,因为pinia实例已经有了
    const userStore = useUserStore()
    

    最简单的方式就是,在确保pinia实例已经有了之后,app.use(pinia)调用后,再去调用useStore()。

    我们看一个使用vue router导航守卫的例子:

    import { createRouter } from 'vue-router'
    const router = createRouter({
      // ...
    })
    
    // ❌取决于pinia和router导入顺序的先后
    const store = useStore()
    
    router.beforeEach((to, from, next) => {
      // 我们想在这里使用store
      if (store.isLoggedIn) next()
      else next('/login')
    })
    
    router.beforeEach((to) => {
      // ✅ 成功,因为router是在router被使用后才喀什导航的,现在pinia肯定也被使用了
      const store = useStore()
    
      if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
    })
    

    服务端渲染的应用

    在使用srr时,你一定要把pinia实例传给useStore()。这样防止里pinia在不同的应用实例中共享同一个全局数据了。后面有完整ssr中使用pinia的例子。

    ssr

    Cookbook

    结束语

    最后两部分ssr和高级内容我就不翻译了,我估计用途也不大,而且对于ssr我也不太熟。
    相信看完官方文档好多小伙伴还是很蒙的,其实官方文档并不适合入门学习
    首先大部分官方文档并不是安装由易到难的顺序编写的,因为它要讲一个模块时,要把这个模块所有内容尽量讲到;其次官方文档是按模块划分内容的,为了是方便查阅,并不适合按这个顺序学习;还有就是我有的地方也不太熟悉,比如ts部分我只是学过一些内容。
    所以我打算后面自己来写比较适合从零入门的教程,目的是可以由简入难的学习,还有就是结合实际工作中会遇到的场景。
    这篇翻译到这里就结束了,完结撒花。

    相关文章

      网友评论

        本文标题:pinia 新一代的vue状态管理

        本文链接:https://www.haomeiwen.com/subject/iahialtx.html