likes
comments
collection
share

Pinia 的五大反向操作(译)

作者站长头像
站长
· 阅读数 6

大家好,这里是大家的林语冰。本期《前端翻译计划》共享的是来自“Pinia 之父”E.S.M. 的一篇博客,本文科普了使用 Pinia 时的若干常见错误及其修正方案。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 Top 5 mistakes to avoid when using Pinia

Pinia 的五大反向操作(译)

Pinia 乃 Vue 3 的官方状态管理技术方案,刚满 4 周岁!这意味着,时间既检验了其实际应用,也见证了其成败兴衰。我想与您共享在使用 Pinia 的项目中我邂逅的某些最常见的错误及其修正方案。

这是太长不看:

  • 在错误的地方调用 useStore()
  • YOLO 映射空对象
  • 对可替换对象使用 reactive()
  • 对大型集合使用深度响应性
  • store 中存储 URL 状态

让我们深入了解这些错误及其修正方案!

在错误的地方调用 useStore()

这条消息是否引起了 PTSD(创伤后应激障碍)?

[🍍]: “getActivePinia()” 在没有 Pinia 的地方被调用。您是否忘记安装 Pinia?

const pinia = createPinia()
app.use(pinia)

这在生产环境中会爆炸。

在 Pinia 中,所有 store 都用 defineStore() 定义,它不会像我们在 Vuex 中那样返回一个 store 实例:

import { createStore } from 'vuex'

const store = createStore({
  state: () => ({
    isHidden: true
  }),
  mutations: {
    SET_HIDDEN(state, isHidden) {
      state.isHidden = isHidden
    }
  }
})

store 对象可以立即使用,并且可以在组件中通过 $store 访问。在 Pinia 中,defineStore() 返回一个需要我们调用的函数来获取 store 实例:

import { defineStore } from 'pinia'

const useModalStore = defineStore('exit-modal', {
  state: () => ({
    isHidden: true
  })
  // ...
})

此函数实际上是一个组合式函数,它透露了它应该被调用的位置:在组件的 setup() 中。等等,就这?不不不,不仅如此!就技术而言,您也可以在其他组合式函数中调用它,只要它们在组件的 setup() 中调用即可。但您也可以在其他 store 和某些特殊函数中调用它们:

举个栗子,在 store 中调用它:

import { defineStore } from 'pinia'

const useModalStore = defineStore('documents', () => {
  const auth = useAuthStore

  async function createDocument() {
    if (!auth.isAuthenticated) {
      throw new Error('You need to be authenticated to create a document')
    }
    fetch('/api/documents', {
      method: 'POST',
      headers: {
        // 创建当前用户拥有的文档
        Authorization: `Bearer ${auth.token}`
      }
    })
  }
  // ...
})

着实有用,对不!

但是这些特殊函数是什么鬼物?通常,它们来自其他库,这些库通过一个不错的高级 API runWithContext() 连接到 Vue App。我确信您不需要在你的 App 中使用此方法,但它允许像 Vue Router 和 Pinia 这样的库使用依赖于 inject/provide 的组合式函数。这包括在 store 中使用路由和在导航守卫中使用 store

import { defineStore } from 'pinia'

router.beforeEach((to, from) => {
  // ✅ 在导航守卫中使用 store
  const auth = useAuthStore()
  if (to.meta.requiresAuth && !auth.isAuthenticated) {
    return '/login'
  }
})

但是请稍等一下,我以前在其他地方使用过该 store,且问题不大。此限制当且仅当在处理 SSR(服务器端渲染)时才重要。在 SPA(单页应用程序)中,不存在跨状态污染的风险(花哨的说法是 App 不安全)。因此,您可以在安装 pinia 插件后随时调用 useStore() 函数:

const pinia = createPinia()
app.use(pinia)
useStore() // ✅ 奏效

话虽如此,我建议您遵守上述规则,而不是依赖它在 SPA 中工作的事实。这将使您的代码更可预测和易于维护。

YOLO 映射空对象

在过去一年里,我经常看到此错误。这是一个易错点,它不仅与 Pinia 有关,还与 TS 有关。很多时候,您的对象会初始化为 undefined,但在您使用它们之前,它们就会被填充。举个栗子,身份验证 store 中的 user 对象:

import { defineStore } from 'pinia'

const useAuthStore = defineStore('auth', () => {
  const user = ref()
  async function updateAvatar(url: string) {
    // ! TS 错误: Object 可能是 'undefined'。
    fetch('/api/user/' + user.value.id, {
      method: 'PATCH',
      body: JSON.stringify({ url }),
    })
  }
  return { user }
})

此处的错误完全正常,且实际上是一件好事:它强制在调用 updateAvatar() 之前验证用户是否经过身份验证。但这也很头大,因为我们知道用户会在我们调用 updateAvatar() 之前被定义。因此,根据您的 TS 熟练度,您可能会这样做:

const user = ref({} as User)

啪的一下很快啊,错误消失了!但我们刚刚做了什么?我们只是告诉 TS,用户始终有被定义,即使它不是。吾愿称之为 YOLO 映射(YOLO cast)。我的个人心证是,这比使用 as any@ts-ignore 还猪头,因为有了这些,我们接受了我们无法妥当对某些东东类型注解的事实。因此,我们只是决定我们仍然需要发布该功能,稍后我们会回来修复它(但我们永远不会这样做)。虽然但是,将一个空对象映射到一个类型上,简直就是在自欺欺人。空对象就在那里,它显然不是用户。我们使 TS 无法检测到某些运行时错误,比如嵌套对象读取:

// ! JS 错误: 无法从 undefined 读取其 'dashboard' 属性

正确的方法是将 User 类型传递给 ref()

const user = ref<User>()

这会让 user.value 的类型变为 User | undefined,TS 将强制我们在访问其属性之前检查用户是否已定义。另一个可能的版本是与显式初始值一起使用 ref<User | null>(null)

对可替换对象使用 reactive()

reactive() 真的很精简,因为它允许我们不用到处写 .value。但它也仅限于对象,您无法替换对象本身。我的意思是:

export const useTodosStore = defineStore('todos', () => {
  const items = reactive([])

  function addTodo(todo: Todo) {
    items.push(todo) // 无需使用 .value 🙌
  }

  // ! 语法错误 👍
  items = []

  // ...
  return { items }
})

如果我们尝试给 items 重新赋值,我们将得到一个语法错误,这简直棒棒哒!该错误不会被忽视。当我们在 store 外使用它时,真正的问题就来了:

const todos = useTodosStore()
// 没有语法错误,没有 TS 错误 🤔
todos.items = []

这应该清除待办事项列表,但事实并非如此。最糟糕的是,我们不会收到任何错误😱。起初,它似乎能奏效,但我们最终所做的是让 todo.items 与位于 store.$state.items 的单一数据源失联。这将破坏 Devtools(开发者工具)、SSR Hydration 和插件。总而言之,我们最终会得到某些难以追踪的错误。

我的建议是坚持对数组和对象使用 ref()。您仍然可以与 SetMap 之类的集合使用 reactive(),因为它们不太可能被替换,这要归功于它们的 clear() 方法。当然了,您也可以坚持使用 ref()

对复杂数据使用深度响应性

在 Vue 中,深度响应性是默认的。这很方便。问题不大。虽然但是,在处理复杂数据(比如永不更改的大型集合)时,这以牺牲性能为代价。一个常见的例子是请求大数据集,比如我们在页面上展示的产品。它们通常作为一个整体请求:

const products = ref([])
const products.value = await fetchProducts()

虽然开销在大多数情况下可以忽略不计。当数据较大且用户使用较慢的设备时,它可能会有问题。选择退出 Vue 深度响应式更改是一个极简更改,它归结为使用一个浅层等价物或 marRaw() 辅助函数。

const products = shallowRef([])

shallowRef() 梦幻联动时,当且仅当 .value 会触发响应性。能力有限,但在某些情况下已经够用了。

在 store 中存储 URL 状态

假设我们有一个展示产品列表的页面。我们希望允许用户按类别过滤产品。用户在页面上选择一个过滤器,我们在 store 中使用该过滤器:

import { defineStore } from 'pinia'

const useProductsStore = defineStore('products', () => {
  const products = ref([])
  const category = ref('')

  const filteredProducts = computed(() =>
    products.value.filter(product => product.category === category.value)
  )

  return { products, category, filteredProducts }
})

虽然此解决方案当用户在页面上时有效,但如果它们重新加载页面或与朋友共享链接,那么其所选类别会完全丢失。这真是太可惜了,因为修复是如此简单,而且它极大地改善了用户体验!

store 中,我们可以用 useRoute() 获取当前路由,用 useRouter() 获取路由实例。这允许我们创建一个计算属性,该属性从 URL 返回类别,也可以设置为推送到 URL

const category = computed({
  get: () => route.query.category,
  set(category) {
    // 是的没错,我们只是传递了查询字符串!
    router.push({ query: { category } })
  }
})

就是这样!现在,用户可以与它们的朋友共享链接,它们将看到相同的产品。它们还可以重新加载页面,并且仍会保留类别。这是一个极简示例,但它可以应用于要保留在 URL 中的任何其他状态。您甚至可以使用 VueUse 的 useRouteQuery() 或您自己的组合式函数来处理其他类型的值,比如数字、布尔值和数组。

完结撒花

避免这些常见错误可确保使用 Pinia 实现更丝滑、更易于维护的 App 开发过程。我希望这些技巧对您的项目有所助益。

您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~

Pinia 的五大反向操作(译)