前言

pinia的作者最近在博客上放出了一篇pinia的使用文章:《My Top 5 Tips for using Pinia》,里面讲述了5个一般不怎么知道的使用技巧,但是由于讲的很浅,我这边自己理解了一下,做了一些解释,顺带记个笔记。

不要创建无用的 getter

这点我感同身受,在最初学习vuex的时候,前辈们总是会说,getter有缓存,性能会更好,于是,在大多数的教程中,我们总是能看到这种示例写法:

export default Vuex.Store({
    state: () => ({
        counter: 0
    }),
    getters: {
        // Completely useless getter
        getCount: state => state.counter,
    },
})

为此我也很长一段时间去模仿这种写法,给每个state属性都增加了getter,然后取值的时候总是通过getter取。

但是当项目的内容增加时,你会发现你的vuex仓库文件有着巨长巨长的属性,而且由于options的原因,常常state的属性在文件上部分,getter、Mutation、Action对应state中属性的操作,被分散在文件上中下各个部分,你不得不来回滚动文件来查看具体的代码内容。

这一度让我非常痛苦,但是好像没有什么更好的办法,特别是有些人不通过getter直接取用state中的属性时,仿佛被人打了一巴掌一样难受(我优化了什么啊~~~)。

但是事实就是,这些getter都是无用的,在pinia中,也是不推崇这种写法,如果你需要的值是一个依赖多个数据源的响应式数据,那么你可以使用getter,这非常正确,如果仅是将某一个属性getter化了,那实在没有必要了。

const counterStore = useCounterStore()
counterStore.counter // 0 ✅

作者还表示,如果没有必要,不需要使用storeToRefs()toRef()来对pinia仓库中的数据进行响应式处理,直接使用数据才是正确的。

在选项库中使用可组合项

这个标题有点拗口,其实意思就是除了使用vue提供的reactiveref的方式声明一个响应式对象,还可以使用其他可以响应式的方式,这点在option写法和setup写法中应该都是适用的。

比如第三方库:@vueuse/core

里面会有一个方法也能创建响应式对象,这些也能被pinia使用。

import { useLocalStorage } from '@vueuse/core'

const useAuthStore = defineStore('auth', {
  state: () => ({
    user: useLocalStorage('pinia/user/login', 'alice'),
  }),
})
import { refDebounced } from '@vueuse/core'

const useSearchStore = defineStore('search', {
  state: () => ({
    user: {
      text: refDebounced(/* ... */),
    },
  }),
})

作者只提供了options的示例代码,setup的不太确定,但是我认为是可以的,因为useLocalStorage的类型是基于vue的Ref类型做的二次声明,显然底层是相同的。

具有设置存储的复杂可组合项

这点和第二点差不多太多,都是在pinia的仓库中可以使用一些第三方插件提供的响应式对象,官方的示例是setup语法,使用的也是@vueuse/core。

import { useWebSocket } from '@vueuse/core'

export const useServerInfoStore = defineStore('server-info', () => {
  const { status, data, send, open, close } = useWebSocket('ws://websocketurl')
  return {
    status,
    data,
    send,
    open,
    close,
  }
})

我们简单看一下类型定义:

/**
 * Reactive WebSocket client.
 *
 * @see https://vueuse.org/useWebSocket
 * @param url
 */
export function useWebSocket<Data = any>(
  url: MaybeRefOrGetter<string | URL | undefined>,
  options: UseWebSocketOptions = {},
): UseWebSocketReturn<Data> {
  const {
    onConnected,
    onDisconnected,
    onError,
    onMessage,
    immediate = true,
    autoClose = true,
    protocols = [],
  } = options

可以看到函数返回了一个UseWebSocketReturn<Data>类型对象,我们再看这个类型:

export interface UseWebSocketReturn<T> {
  /**
   * Reference to the latest data received via the websocket,
   * can be watched to respond to incoming messages
   */
  data: Ref<T | null>

  /**
   * The current websocket status, can be only one of:
   * 'OPEN', 'CONNECTING', 'CLOSED'
   */
  status: Ref<WebSocketStatus>

  /**
   * Closes the websocket connection gracefully.
   */
  close: WebSocket['close']

  /**
   * Reopen the websocket connection.
   * If there the current one is active, will close it before opening a new one.
   */
  open: Fn

  /**
   * Sends data through the websocket connection.
   *
   * @param data
   * @param useBuffer when the socket is not yet open, store the data into the buffer and sent them one connected. Default to true.
   */
  send: (data: string | ArrayBuffer | Blob, useBuffer?: boolean) => boolean

  /**
   * Reference to the WebSocket instance.
   */
  ws: Ref<WebSocket | undefined>
}

可以看到他的数据对象也是Ref类型,所以pinia自然而然可以使用它。

在pinia setup语法中使用 inject() 接收变量

标题原名是:inject() within setup stores,机翻过来非常拗口,为此我自己翻译了一下。

我们先看示例:

import { useRouter } from 'vue-router'

export const useAuthStore('auth', () => {
  const router = useRouter()
  function logout() {
    // logout the user
    return router.push('/login')
  }
  return {
    logout
  }
})

作者使用了vue-router的hooks函数,但是并没有inject这个函数代码,这是因为useRouter函数内部使用了inject。

查看hooks源码:

import { inject } from 'vue'
import { routerKey, routeLocationKey } from './injectionSymbols'
import { Router } from './router'
import { RouteLocationNormalizedLoaded } from './types'

/**
 * Returns the router instance. Equivalent to using `$router` inside
 * templates.
 */
export function useRouter(): Router {
  return inject(routerKey)!
}

/**
 * Returns the current route location. Equivalent to using `$route` inside
 * templates.
 */
export function useRoute(): RouteLocationNormalizedLoaded {
  return inject(routeLocationKey)!
}

可以看到,它引入了routerKey, routeLocationKey这两个值,这两个其实就是个Symbol唯一值。

/**
 * Allows overriding the router instance returned by `useRouter` in tests. r
 * stands for router
 *
 * @internal
 */
export const routerKey = Symbol(__DEV__ ? 'router' : '') as InjectionKey<Router>

/**
 * Allows overriding the current route returned by `useRoute` in tests. rl
 * stands for route location
 *
 * @internal
 */
export const routeLocationKey = Symbol(
  __DEV__ ? 'route location' : ''
) as InjectionKey<RouteLocationNormalizedLoaded>

由于没怎么看vue对于provideinject源码,我大体猜测一下,provide方法会将传入的内容以key value的形式,存放在对应的组件实例的provides对象属性上,通过inject获取时,需要传入对应的key,然后返回provides对象上对应的内容,如果没有则会一层一层往上查找,直到最顶层的app组件,还没有则返回undefined

而pinia中能通过inject拿取到router,我认为是他在use安装的时候,将app实例保留了下来,然后做了一些特殊处理,我们可以理解为只能拿取app上provide挂载的内容,因为在vue-router的源码中,在install中确实通过app.provide挂载了router和route。

官方提供了这个我感觉更适合搞多语言,路由跳转有点代码耦合了,而且什么情况需要跳转,大多数都是请求接口满足什么条件的情况下才会,如果在pinia仓库中调用接口,我是不推荐的,因为你会碰到循环引入的问题,这些有机会再说吧。

私有存储

一般情况下我们在pinia中声明的属性都是公开的,任何人获取到仓库的实例对象都能从中读取所有数据,但是有时候我们可能不希望有些数据被公开,那么如何隐藏这部分数据呢?

export const usePrivateAuthState('auth-private', () => {
  const token = ref<string | null>(null)
  return {
    token,
  }
})
export const useAuthStore('auth', () => {
  const user = ref<User | null>(null)
  const privateState = usePrivateAuthState()
  privateState.token // accessible only within this store
  return {
    user,
  }
})

官方做法是单独建立一个存储,在另一个存储中使用,你甚至都可以不用导出这个私有仓库,这样在外部我们就是一个隐藏的内容,通过公共仓库对外的api方法来实现增删改。

客户端和SSR一起使用

这个是作者特别提供的,告知了一些在SSR情况下使用的方法。

服务器端渲染 (SSR) 是提高应用程序性能的好方法。但是,与仅限客户端的应用程序相比,它带来了一些额外的困难。例如,您无权访问 window 、 或 document 或任何其他特定于浏览器的 API,例如本地存储。
在选项存储中,这要求你使用一个 hydrate 选项来告诉 Pinia 某些状态不应该在客户端上被冻结:
import { useLocalStorage } from '@vueuse/core'

const useAuthStore = defineStore('auth', {
  state: () => ({
    user: useLocalStorage('pinia/user/login', 'alice'),
  }),
  hydrate(state, initialState) {
    state.user = useLocalStorage('pinia/user/login', 'alice')
  },
})
在安装程序存储区中,可以使用 skipHydrate 帮助程序将某些状态标记为仅限客户端:
import { defineStore, skipHydrate } from 'pinia'

const useAuthStore = defineStore('auth', () => {
  const user = skipHydrate(useLocalStorage('pinia/user/login', 'alice'))
  return { user }
})

这两个方法官方文档都有具体的说明,我们可以作为新知识扩展,等有机会用到SSR了就能用上了。

分类: vue 项目实战 标签: injectpiniagetter全局注入私有存储

评论

暂无评论数据

暂无评论数据

目录