大家好,这里是大家的林语冰。本期《前端翻译计划》共享的是来自"Pinia 之父"E.S.M. 的一篇博客,本文科普了使用 Pinia 时的若干常见错误及其修正方案。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请临幸 Top 5 mistakes to avoid when using Pinia。
Pinia 乃 Vue 3 的官方状态管理技术方案,刚满 4 周岁!这意味着,时间既检验了其实际应用,也见证了其成败兴衰。我想与您共享在使用 Pinia 的项目中我邂逅的某些最常见的错误及其修正方案。
这是太长不看:
- 在错误的地方调用
useStore()
- YOLO 映射空对象
- 对可替换对象使用
reactive()
- 对大型集合使用深度响应性
- 在
store
中存储 URL 状态
让我们深入了解这些错误及其修正方案!
在错误的地方调用 useStore()
这条消息是否引起了 PTSD(创伤后应激障碍)?
md
[🍍]: "getActivePinia()" 在没有 Pinia 的地方被调用。您是否忘记安装 Pinia?
const pinia = createPinia()
app.use(pinia)
这在生产环境中会爆炸。
在 Pinia 中,所有 store
都用 defineStore()
定义,它不会像我们在 Vuex 中那样返回一个 store
实例:
js
import { createStore } from 'vuex'
const store = createStore({
state: () => ({
isHidden: true
}),
mutations: {
SET_HIDDEN(state, isHidden) {
state.isHidden = isHidden
}
}
})
该 store
对象可以立即使用,并且可以在组件中通过 $store
访问。在 Pinia 中,defineStore()
返回一个需要我们调用的函数来获取 store
实例:
js
import { defineStore } from 'pinia'
const useModalStore = defineStore('exit-modal', {
state: () => ({
isHidden: true
})
// ...
})
此函数实际上是一个组合式函数,它透露了它应该被调用的位置:在组件的 setup()
中。等等,就这?不不不,不仅如此!就技术而言,您也可以在其他组合式函数中调用它,只要它们在组件的 setup()
中调用即可。但您也可以在其他 store
和某些特殊函数中调用它们:
举个栗子,在 store
中调用它:
js
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
!
js
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()
函数:
js
const pinia = createPinia()
app.use(pinia)
useStore() // ✅ 奏效
话虽如此,我建议您遵守上述规则,而不是依赖它在 SPA 中工作的事实。这将使您的代码更可预测和易于维护。
YOLO 映射空对象
在过去一年里,我经常看到此错误。这是一个易错点,它不仅与 Pinia 有关,还与 TS 有关。很多时候,您的对象会初始化为 undefined
,但在您使用它们之前,它们就会被填充。举个栗子,身份验证 store
中的 user
对象:
js
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 熟练度,您可能会这样做:
ts
const user = ref({} as User)
啪的一下很快啊,错误消失了!但我们刚刚做了什么?我们只是告诉 TS,用户始终有被定义,即使它不是。吾愿称之为 YOLO 映射(YOLO cast)。我的个人心证是,这比使用 as any
或 @ts-ignore
还猪头,因为有了这些,我们接受了我们无法妥当对某些东东类型注解的事实。因此,我们只是决定我们仍然需要发布该功能,稍后我们会回来修复它(但我们永远不会这样做)。虽然但是,将一个空对象映射到一个类型上,简直就是在自欺欺人。空对象就在那里,它显然不是用户。我们使 TS 无法检测到某些运行时错误,比如嵌套对象读取:
md
// ! JS 错误: 无法从 undefined 读取其 'dashboard' 属性
正确的方法是将 User
类型传递给 ref()
:
ts
const user = ref<User>()
这会让 user.value
的类型变为 User | undefined
,TS 将强制我们在访问其属性之前检查用户是否已定义。另一个可能的版本是与显式初始值一起使用 ref<User | null>(null)
。
对可替换对象使用 reactive()
reactive()
真的很精简,因为它允许我们不用到处写 .value
。但它也仅限于对象,您无法替换对象本身。我的意思是:
js
export const useTodosStore = defineStore('todos', () => {
const items = reactive([])
function addTodo(todo: Todo) {
items.push(todo) // 无需使用 .value 🙌
}
// ! 语法错误 👍
items = []
// ...
return { items }
})
如果我们尝试给 items
重新赋值,我们将得到一个语法错误,这简直棒棒哒!该错误不会被忽视。当我们在 store
外使用它时,真正的问题就来了:
js
const todos = useTodosStore()
// 没有语法错误,没有 TS 错误 🤔
todos.items = []
这应该清除待办事项列表,但事实并非如此。最糟糕的是,我们不会收到任何错误😱。起初,它似乎能奏效,但我们最终所做的是让 todo.items
与位于 store.$state.items
的单一数据源失联。这将破坏 Devtools(开发者工具)、SSR Hydration 和插件。总而言之,我们最终会得到某些难以追踪的错误。
我的建议是坚持对数组和对象使用 ref()
。您仍然可以与 Set
和 Map
之类的集合使用 reactive()
,因为它们不太可能被替换,这要归功于它们的 clear()
方法。当然了,您也可以坚持使用 ref()
!
对复杂数据使用深度响应性
在 Vue 中,深度响应性是默认的。这很方便。问题不大。虽然但是,在处理复杂数据(比如永不更改的大型集合)时,这以牺牲性能为代价。一个常见的例子是请求大数据集,比如我们在页面上展示的产品。它们通常作为一个整体请求:
js
const products = ref([])
const products.value = await fetchProducts()
虽然开销在大多数情况下可以忽略不计。当数据较大且用户使用较慢的设备时,它可能会有问题。选择退出 Vue 深度响应式更改是一个极简更改,它归结为使用一个浅层等价物或 marRaw()
辅助函数。
js
const products = shallowRef([])
与 shallowRef()
梦幻联动时,当且仅当 .value
会触发响应性。能力有限,但在某些情况下已经够用了。
在 store 中存储 URL 状态
假设我们有一个展示产品列表的页面。我们希望允许用户按类别过滤产品。用户在页面上选择一个过滤器,我们在 store
中使用该过滤器:
js
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:
js
const category = computed({
get: () => route.query.category,
set(category) {
// 是的没错,我们只是传递了查询字符串!
router.push({ query: { category } })
}
})
就是这样!现在,用户可以与它们的朋友共享链接,它们将看到相同的产品。它们还可以重新加载页面,并且仍会保留类别。这是一个极简示例,但它可以应用于要保留在 URL 中的任何其他状态。您甚至可以使用 VueUse 的 useRouteQuery()
或您自己的组合式函数来处理其他类型的值,比如数字、布尔值和数组。
完结撒花
避免这些常见错误可确保使用 Pinia 实现更丝滑、更易于维护的 App 开发过程。我希望这些技巧对您的项目有所助益。
您现在收看的是《前端翻译计划》,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~