跳至主要內容

pinia

ourandream大约 7 分钟front-end

pinia 是 vue 官方推荐的存储管理库,可用于管理全局的数据.

基础

import { createPinia } from "pinia";

app.use(createPinia());

这样我们就创建了一个 pinia(root store)并注册为 vue 插件.

pinia中,store指的是一个存放 stateopen in new window, gettersopen in new window and actionsopen in new window的实体,它们分别对应data, computed and methods.

定义store:

import { defineStore } from "pinia";

// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const useStore = defineStore("main", {
  // other options...
});

使用:

import { useStore } from "@/stores/counter";

export default {
  setup() {
    const store = useStore();

    return {
      // you can return the whole store instance to use it in the template
      store,
    };
  },
};

注意 store 使用的是 vue 的reactive,故若想使用 destruction 语法,需要:

import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    const store = useStore()
    // `name` and `doubleCount` are reactive refs
    // This will also create refs for properties added by plugins
    // but skip any action or non reactive (non ref/reactive) property
    const { name, doubleCount } = storeToRefs(store)
    // the increment action can be just extracted
    const { increment } = store

    return {
      name,
      doubleCount
      increment,
    }
  },
})

state

在 pinia,我们通过一个函数定义定义state:

import { defineStore } from "pinia";

const useStore = defineStore("storeId", {
  // arrow function recommended for full type inference
  state: () => {
    return {
      // all these properties will have their type inferred automatically
      counter: 0,
      name: "Eduardo",
      isAdmin: true,
    };
  },
});

修改:

const store = useStore();
//重置为初始值
store.$reset();
//合并多个修改
store.$patch({
  counter: store.counter + 1,
  name: "Abalam",
});
cartStore.$patch((state) => {
  state.items.push({ name: "shoes", quantity: 1 });
  state.hasChanged = true;
});
//替换所有state
store.$state = { counter: 666, name: "Paimon" };

我们还可以监视更改:

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()

  // persist the whole state to the local storage whenever it changes
  localStorage.setItem("cart", JSON.stringify(state));
});

使用$subscribe而不是 watch 的好处是它在更改发送后只会触发一次.

默认情况下它被限制在 component 的范围,如果想要让 component 取消挂载后仍有些,需要设置detached:

export default {
  setup() {
    const someStore = useSomeStore();

    // this subscription will be kept after the component is unmounted
    someStore.$subscribe(callback, { detached: true });

    // ...
  },
};

如果是通过前面的 composition api 的形式使用store,我们直接通过store访问 state,不过如果使用 option api(不使用 setup)的话,需要使用辅助的函数.

只读:

import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'

export default {
  computed: {
    // gives access to this.counter inside the component
    // same as reading from store.counter
    ...mapState(useCounterStore, ['counter'])
    // same as above but registers it as this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'counter',
      // you can also write a function that gets access to the store
      double: store => store.counter * 2,
      // it can have access to `this` but it won't be typed correctly...
      magicValue(store) {
        return store.someGetter + this.counter + this.double
      },
    }),
  },
}

读写:

import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'

export default {
  computed: {
    // gives access to this.counter inside the component and allows setting it
    // this.counter++
    // same as reading from store.counter
    ...mapWritableState(useCounterStore, ['counter'])
    // same as above but registers it as this.myOwnName
    ...mapWritableState(useCounterStore, {
      myOwnName: 'counter',
    }),
  },
}

getter

定义getter(可带参数也可以不带而使用this):

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

注意如果使用了其他的不使用 arrow function 定义getter,且我们使用了this,则在 typescript 中我们必须指明返回值类型:

export const useStore = defineStore("main", {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // automatically infers the return type as a number
    doubleCount(state) {
      return state.counter * 2;
    },
    // the return type **must** be explicitly set
    doublePlusOne(): number {
      // autocompletion and typings for the whole store ✨
      return this.doubleCount + 1;
    },
  },
});

我们可以通过返回一个函数的形式使得getter可以带参数:

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

但注意这种情况下它不会被缓存.

使用其他 store 的 getter 只需要使用它的 store 即可:

import { useOtherStore } from "./other-store";

export const useStore = defineStore("main", {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore();
      return state.localData + otherStore.data;
    },
  },
});

在 option api 中使用 getter 和之前一样使用mapstate:

import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'

export default {
  computed: {
    // gives access to this.doubleCounter inside the component
    // same as reading from store.doubleCounter
    ...mapState(useCounterStore, ['doubleCount'])
    // same as above but registers it as this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'doubleCounter',
      // you can also write a function that gets access to the store
      double: store => store.doubleCount,
    }),
  },
}

action

定义action:

import { mande } from "mande";

const api = mande("/api/users");

export const useUsers = defineStore("users", {
  state: () => ({
    userData: null,
    // ...
  }),

  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password });
        showTooltip(`Welcome back ${this.userData.name}!`);
      } catch (error) {
        showTooltip(error);
        // let the form component display the error
        return error;
      }
    },
  },
});

注意它可以是异步的.

action一般可以直接通过 store instance 访问.

当使用不用 setup 的 option api 时:

import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counterStore'

export default {
  methods: {
    // gives access to this.increment() inside the component
    // same as calling from store.increment()
    ...mapActions(useCounterStore, ['increment'])
    // same as above but registers it as this.myOwnName()
    ...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }),
  },
}

我们还可以监视action:

const unsubscribe = someStore.$onAction(
  ({
    name, // name of the action
    store, // store instance, same as `someStore`
    args, // array of parameters passed to the action
    after, // hook after the action returns or resolves
    onError, // hook if the action throws or rejects
  }) => {
    // a shared variable for this specific action call
    const startTime = Date.now();
    // this will trigger before an action on `store` is executed
    console.log(`Start "${name}" with params [${args.join(", ")}].`);

    // this will trigger if the action succeeds and after it has fully run.
    // it waits for any returned promised
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      );
    });

    // this will trigger if the action throws or returns a promise that rejects
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      );
    });
  }
);

// manually remove the listener
unsubscribe();

默认情况下这种监视会限制在组件的范围,想在组件取消挂载时仍然可用:

export default {
  setup() {
    const someStore = useSomeStore();

    // this subscription will be kept after the component is unmounted
    someStore.$onAction(callback, true);

    // ...
  },
};

plugin

pinia 中 plugin 为一个函数,通过pinia.use()使用,它有如下功能:

  • Add new properties to stores
  • Add new options when defining stores
  • Add new methods to stores
  • Wrap existing methods
  • Change or even cancel actions
  • Implement side effects like Local Storageopen in new window
  • Apply only to specific stores

该函数可以带参数:

export function myPiniaPlugin(context) {
  context.pinia; // the pinia created with `createPinia()`
  context.app; // the current app created with `createApp()` (Vue 3 only)
  context.store; // the store the plugin is augmenting
  context.options; // the options object defining the store passed to `defineStore()`
  // ...
}

plugin只在 pinia 传给 app 后创建的 store 起效.

为 store 添加 property:

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

也可以通过参数:

// from the example above
pinia.use(({ store }) => {
  store.hello = "world";
  // make sure your bundler handle this. webpack and vite should do it by default
  if (process.env.NODE_ENV === "development") {
    // add any keys you set on the store
    store._customProperties.add("hello");
  }
});

注意后者为了让 devtools 可跟踪多了一个步骤.

store 会自动unwrapping,不需要加.value:

const sharedRef = ref("shared");
pinia.use(({ store }) => {
  // each store has its individual `hello` property
  store.hello = ref("secret");
  // it gets automatically unwrapped
  store.hello; // 'secret'

  // all stores are sharing the value `shared` property
  store.shared = sharedRef;
  store.shared; // 'shared'
});

添加 state 则需要在以下两个地方添加:

  • store以通过 store 访问
  • store.$state所以在 devtools 中可用,在 SSR 过程中会被serialized
const globalSecret = ref('secret')
pinia.use(({ store }) => {
  // `secret` is shared among all stores
  store.$state.secret = globalSecret
  store.secret = globalSecret
  // it gets automatically unwrapped
  store.secret // 'secret'

  const hasError = ref(false)
  store.$state.hasError = hasError
  // this one must always be set
  store.hasError = toRef(store.$state, 'hasError')

  // in this case it's better not to return `hasError` since it
  // will be displayed in the `state` section in the devtools
  // anyway and if we return it, devtools will display it twice.

注意在 plugin 中的修改不会被subscriptions跟踪到,因为它发生在 store 可用之前.

当加入外部的不是 reactive 的属性:

import { markRaw } from "vue";
// adapt this based on where your router is
import { router } from "./router";

pinia.use(({ store }) => {
  store.router = markRaw(router);
});

在 plugin 可正常地使用$subscribeonAction.

我们还可以创建新的 option 然后在 plugin 中使用:

defineStore("search", {
  actions: {
    searchContacts() {
      // ...
    },
  },

  // this will be read by a plugin later on
  debounce: {
    // debounce the action searchContacts by 300ms
    searchContacts: 300,
  },
});

// use any debounce library
import debounce from "lodash/debounce";

pinia.use(({ options, store }) => {
  if (options.debounce) {
    // we are overriding the actions with new ones
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      );
      return debouncedActions;
    }, {});
  }
});

pinia 有完整的 typescript 支持,故 plugin 也可以很好地组织类型.

参数:

import { PiniaPluginContext } from "pinia";

export function myPiniaPlugin(context: PiniaPluginContext) {
  // ...
}

添加新 store property:

import "pinia";

declare module "pinia" {
  export interface PiniaCustomProperties {
    // by using a setter we can allow both strings and refs
    set hello(value: string | Ref<string>);
    get hello(): string;

    // you can define simpler values too
    simpleNumber: number;
  }
}

PiniaCustomProperties是一个 generic type,当我们需要使用 store 的类型时可使用:

import "pinia";

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

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

对于新的 state:

import "pinia";

declare module "pinia" {
  export interface PiniaCustomStateProperties<S> {
    hello: string;
  }
}

对于新的 option:

import "pinia";

declare module "pinia" {
  export interface DefineStoreOptionsBase<S, Store> {
    // allow defining a number of ms for any of the actions
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>;
  }
}