美文网首页
Pinia 正式成为 vuejs 的一员

Pinia 正式成为 vuejs 的一员

作者: BingJS | 来源:发表于2022-08-03 18:22 被阅读0次

    先来看早期 vue 上一个关于 Vuex 5.x 的 RFC :

    • 同时支持 composition api 和 options api 的语法;
    • 去掉 mutations,只有 state、getters 和 actions;
    • 不支持嵌套的模块,通过组合 store 来代替;
    • 更完善的 Typescript 支持;
    • 清晰、显式的代码拆分;

    而 Pinia 正是基于 RFC 所生成的一个玩物。

    它的定位和特点也很明确:

    • 直观,像定义组件一样地定义 store,并且能够更好地组合它们;
    • 完整的 Typescript 支持;
    • 关联 Vue Devtools 钩子,提供更好地开发体验;
    • 模块化设计,能够构建多个 stores 并实现自动地代码拆分;
    • 极其轻量(1kb),甚至感觉不到它的存在 ;
    • 同时支持同步和异步 actions;

    接触

    用 vite 快速起一个 vue 模板的项目:

    yarn create @vitejs/app pinia-learning --template vue-ts
    
    cd pinia-learning
    yarn
    yarn dev 
    

    项目运行起来后,安装 pinia 并初始化一个 store:

    yarn add pinia
    

    在 src/main.ts 下定义引用 pinia 插件:

    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import App from './App.vue'
    
    createApp(App).use(createPinia()).mount('#app') 
    

    了解(State)

    defineStore

    之后就可以定义我们的 store 并在组件中使用,我们新建 src/store/index.ts 文件并定义一个 store:

    import { defineStore } from 'pinia'
    
    export default defineStore({
      id: 'app',
    
      state () {
        return {
          name: '码农'
        }
      }
    }) 
    

    在 App.vue 引入上述文件就可以使用该 store:

    <template>
      <div>{{ store.name }}</div>
    </template>
    
    <script setup lang="ts">
    import useAppStore from './store/index'
    const store = useAppStore()
    console.log(store)
    </script>
    

    defineComponent <==> defineStore、id <==> name、 state <==> setup,直观,像定义组件一样地定义 store 到这里是能够体会到该特性的含义了。上述代码是在 composition api 中 setup 的用法,在 options api 中使用跟 Vuex 类似,通过 mapState 或者 mapWritableState 辅助函数来读写 state:

    <template>
      <div>{{ this.username }}</div>
      <div>{{ this.interests.join(',') }}</div>
    </template>
    
    <script lang="ts">
    import { mapState, mapWritableState } from 'pinia'
    import infoStore from '../store/info'
    
    export default {
      name: 'HelloWorld',
    
      computed: {
        // 只读计算属性
        ...mapState(infoStore, ['interests']),
        
        // 读写计算属性
        ...mapWritableState(infoStore, {
          username: 'name'
        })
      },
    
      mounted () {
        this.interests.splice(1, 1, '足球')
        this.username = 'Jouryjc'
      }
    }
    
    </script>
    

    storeToRefs

    那么后半句是啥意思呢?并且能够更好地组合它们 。举个例子,马上就 11.11 ,当然得往购物车里面塞几本“葵花宝典”,于是乎就需要一个 cart 的 store:

    import { defineStore } from 'pinia'
    
    export default defineStore('cart', {
      state () {
        return {
          books: [
            {
              name: '金瓶梅',
              price: 50
            },
            {
              name: '微服务架构设计模式',
              price: 139
            },
            {
              name: '数据密集型应用系统设计',
              price: 128
            }
          ]
        }
      }
    })
    

    然后在 AppStore 组合 cartStore :

    // store/index.ts
    import { defineStore } from 'pinia'
    import useCartStore from './cart'
    
    export default defineStore('app', {
      state () {
        // 直接使用 cartStore
        const cartStore = useCartStore()
    
        return {
          name: '码农',
          books: cartStore.books
        }
      }
    })
    

    最终被 App 组件消费。⚠️ 直接解构 store 会使其失去响应式,为了在保持其响应式的同时从 store 中提取属性要使用 storeToRefs ,如下述代码所示:

     <template>
      <div>{{ name }}</div>
      <p>购物车清单:</p>
      <p v-for="book of books" :key="book.name">
        书名:{{ book.name }} 价格:{{ book.price }}
      </p>
    </template>
    
    <script setup lang="ts">
    import { storeToRefs } from 'pinia'
    import useAppStore from './store/index'
    
    // ⚠️⚠️ 返回的是一个 reactive 对象,不能直接解构哦,使用 pinia 提供的 storeToRefs API
    const { name, books } = storeToRefs(useAppStore())
    </script>
    

    $patch

    除了直接修改 store.xxx 的值,还可以通过 $patch 修改多个字段信息;下面在例子中添加购买数量、总价,并添加付款人:

    <template>
        <div>{{ name }}</div>
        <p>购物车清单:</p>
        <p v-for="book of books" :key="book.name">
            书名:{{ book.name }} 价格:{{ book.price }} 数量: {{ book.count }}
            <button @click="add(book)">+</button>
            <button @click="minus(book)">-</button>
            </p>
        <button @click="batchAdd">全部加到10本</button>
        <p>总价:{{ price }}</p>
        <button @click="reset()">重置</button>
    </template>
    
    <script setup lang="ts">
    import { storeToRefs } from "pinia";
    import useAppStore from "./store/index";
    import type { BookItem } from "./store/cart";
    
    const store = useAppStore();
    const { name, books, price } = storeToRefs(store);
    
    const reset = () => {
      store.$reset();
    };
    
    const add = (book: BookItem) => {
      // 直接修改 store.book
      book.count++;
    };
    
    const minus = (book: BookItem) => {
      // 直接修改 store.book
      book.count--;
    };
    
    const batchAdd = () => {
      // 通过 $patch 方法修改 store 多个字段
      store.$patch({
        name: '小I',
        books: [
          {
            name: "金瓶梅",
            price: 50,
            count: 10,
          },
          {
            name: "微服务架构设计模式",
            price: 139,
            count: 10,
          },
          {
            name: "数据密集型应用系统设计",
            price: 128,
            count: 10,
          },
        ],
      });
    };
    </script>
    

    例子中添加了 “全部加到10本”和 “重置”按钮,点击前者会将全部书籍数量添加到 10 本,点击后者会重置成 0,下面是执行效果:



    假如你只想将《微服务架构设计模式》的数量修改成10,通过 $patch 传对象的方法需要这么操作:

    <template>
     <button @click="batchAddMicroService">微服务加到10本</button>
    </template>
    
    <script>
     const batchAddMicroService = () => {
          store.$patch({
            name: '小I',
            books: [
              {
                name: "金瓶梅",
                price: 50,
                count: 0,
              },
              {
                name: "微服务架构设计模式",
                price: 139,
                count: 10,
              },
              {
                name: "数据密集型应用系统设计",
                price: 128,
                count: 0,
              },
            ],
          });
        }
    </script>
    

    可以看到,就算你只是修改数组(集合)的第二项,还是需要将整个 books 数组传入,于是就产生了将函数作为 $patch 参数的写法:

    <script>
    const batchAddMicroService = () => {
      store.$patch((state) => {
        state.books[1].count = 10;
      });
    }
    </script>
    

    上述代码重写了 batchAddMicroService 方法。

    $subscribe

    该方法跟 vuex 的 subscribe 类似,用于监听 state 及其 mutation 动作。上述例子中我们订阅 appStore 的状态:

    const store = useAppStore();
    
    store.$subscribe((mutation, state) => {
      console.log(mutation);
      console.log(state);
    });
    

    Getters

    Getters 就是 store 的计算属性(computed)。大部分时候,Getter 通过 state 值去做计算,这种情况下 TypeScript 能够正确的推断出类型。例如:

    export default defineStore('app', {
      state: () => {
        const userInfoStore = useUserInfoStore()
        const cartStore = useCartStore()
    
        return {
          name: userInfoStore.name,
          books: cartStore.books
        }
      },
    
      getters: {
        price: (state) => {
          return state.books.reduce((init: number, curValue: BookItem) => {
            return init += curValue.price * curValue.count
          }, 0)
        }
      }
    })
    

    我们将 state 和 getters 都改成箭头函数,这样就能在 App.vue 中正确推断出 price 的类型。

    如果在 getters 中使用 this 去访问 state 的话,需要显式声明返回值才能正确标记类型,我们来试试:

    export default defineStore('app', {
      // ...
      getters: {
        price () {
          return this.books.reduce((init: number, curValue: BookItem) => {
            return init += curValue.price * curValue.count
          }, 0)
        }
      }
    }) 
    

    我们给 price 显示声明返回类型:

    export default defineStore('app', {
      // ...
      getters: {
        price (): number {
          return this.books.reduce((init: number, curValue: BookItem) => {
            return init += curValue.price * curValue.count
          }, 0)
        }
      }
    })
    

    此时又能正确地提示 price 的类型。
    Getters 其他用法比如组合 Getters、在 setup 或 options api 中使用、传参等等都跟 State 类似,本节就不展开细述。

    Actions

    Actions 相当于组件里的 methods。双 11 买东西当然免不了折扣,商家也在折扣这环节上设计了活动,能够让顾客自己随机一个折扣比率,于是在 store 中的 actions 下定义 changeDiscountRate 方法:

    export default defineStore('app', {
      state: () => {
        const userInfoStore = useUserInfoStore()
        const cartStore = useCartStore()
        const discountRate = 1
    
        return {
          name: userInfoStore.name,
          books: cartStore.books,
          discountRate
        }
      },
    
      actions: {
        changeDiscountRate () {
          this.discountRate = Math.random() * this.discountRate
        }
      }
    }) 
    

    跟 Getters 一样,actions 中也通过 this 去获取整个 store。我们通过异步 actions 让修改折扣有一个延迟效果:

    function getNewDiscountRate (rate: number): Promise<number> {
      return new Promise ((resolve) => {
        setTimeout(() => {
          resolve(rate * Math.random())
        }, 1000)
      })
    }
    
    export default defineStore('app', {
      // ...
      actions: {
        async changeDiscountRate () {
          this.discountRate = await getNewDiscountRate(this.discountRate)
        }
      }
    }) 
    

    $onAction

    当我们想统计 actions 的时间或者记录折扣点击总次数的时候,$onAction 订阅器能够很方便地实现,下面是一个官方的示例:

    // App.vue
    const unsubscribe = store.$onAction(
     ({
        name,
        store,
        args,
        after,
        onError,
      }) => {
        const startTime = Date.now()
        console.log(`Start "${name}" with params [${args.join(', ')}].`)
    
        after((result) => {
          console.log(
            `Finished "${name}" after ${
              Date.now() - startTime
            }ms.\nResult: ${result}.`
          )
        })
    
        onError((error) => {
          console.warn(
            `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
          )
        })
     }
    )
    
    
    function getNewDiscountRate (rate: number): Promise<number> {
      return new Promise ((resolve, reject) => {
        setTimeout(() => {
          // 这里通过reject结束promise
          reject(rate * Math.random())
        }, 1000)
      })
    }
    
    export default defineStore('app', {
      // ...
    
      actions: {
        async changeDiscountRate () {
          try {
            this.discountRate = await getNewDiscountRate(this.discountRate)
          } catch (e) {
            // 示例执行这部分逻辑
            throw Error(e)
          }
        }
      }
    })
    

    相关文章

      网友评论

          本文标题:Pinia 正式成为 vuejs 的一员

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