前言
前端发展至今,项目的技术框架花样繁多,刨除 bundler,CSS 等,个人认为对 DX(developer experience)影响最大的主要有二: TypeScript / Hooks , 对应编程的基本要素:类型 / 上下文。
对于业务逻辑,TS 完备记录类型;对于封装的复用逻辑,Hooks 提供可跳转追溯的上下文。这两点构成项目最基本的可维护性。
而老旧 Vue2 项目这两者往往都不具备,随意嵌套的类型 和 截断上下文无可追溯的 mixin / vuex map,时刻提醒着接盘者自己似乎不是在编程,写脚本罢了。而继续堆砌屎山,有损尊严,所以一点技术升级很有必要。
Vue 2 引入 Composition API
引入工具函数
Vue > 2.7 已内置, 小于则使用 github.com/vuejs/compo… vue 2 各版本都能用。
yarn add @vue/composition-api
//entry
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
//component.vue
import { ref, reactive } from '@vue/composition-api'
本人项目为微前端子项目,Vue 版本为 2.6 且无法修改,同时基座 $t 有问题导致只能写 .vue 文件,同时基座问题导致后续 SWR 框架无法使用熟悉的 tanstack ,只能改用没用过的 swrv ,即使这样在后续的开发中也比较顺利,所以不用担心兼容性问题。
提醒
如使用 @vue/composition-api , 则全部工具需从此包引,不从 vue 包引,否则类型对不齐。
// error!!!
import { defineComponent } from '@vue/composition-api'
import { PropType } from 'vue'
//correct
import { defineComponent, PropType } from '@vue/composition-api'
另外 @vue/composition-api 包的响应式实现无法使用深度修改引用类型的嵌套属性来获得响应修改,但在任何时候 setup 写法和 option API 都可以混用,所以影响不大,依然用 option API 写就好了。
<script lang="ts">
export default defineComponent({
// defineComponent 只提供类型,对祖传代码没有类型,直接写 setup() 即可,不套用 defineComponent。
setup() {
const { isLoading: timeLoading } = usePageStatus()
const { isLoading: arLoading } = useUserDetail()
return {
timeLoading,
arLoading
}
},
computed: {
pageLoading(): boolean {
// 随意混用,和旧 vuex/mixin 等逻辑可直接对接
return this.timeLoading || this.arLoading
}
}
})
</script>
引入 JSX 周边
解锁各种 JSX , FC 写法,如果只能写 .vue 文件, <script lang=”tsx”> 即可。
yarn add @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props
// babel config
module.exports = {
presets: [
[
'@vue/babel-preset-jsx',
{
compositionAPI: true,
vModel: true,
functional: true,
vOn: true
},
],
],
}
// Test.vue
<script lang="tsx">
import { defineComponent } from '@vue/composition-api'
export const TestFc = (props: { msg: string }) => <div>{props.msg}</div>
export default defineComponent({
setup(props: any) {
return () => <div>{props.msg}</div>
}
})
</script>
使用 SWR
外链目录
Tanstack,个人最熟悉, AKA react-query,目前有多框架版本 : tanstack.com/query/lates…
以 SWR 命名,Tanstack 竞品,Vercel 出品,只有 react版本: github.com/vercel/swr
SWRV ,SWR 的 vue 版本,支持 Vue 2 低版本,Kong 出品,业务项目中最终使用的框架:github.com/Kong/swrv
codesandbox 示例(个人版),使用 tanstack 的 Vue2 版本:codesandbox.io/p/sandbox/t…
codesandbox 示例(tanstack 官网版):tanstack.com/query/lates…
虽然如上有四个库,但只有框架和 API 的不同,内核思想和用法是完全一致的。
无论是 tanstack 还是 vercel/swr , 它们首先是 client data fetcher 框架,提供了 hooks 式的用法和 API (缓存管理,自动refetch等功能),只是说 SWR 是一种最好的模式,所以是这两个框架都实现了它并设为默认行为。
什么是 SWR
The name “SWR” is derived from
stale-while-revalidate
, a cache invalidation strategy popularized by HTTP RFC 5861. SWR first returns the data from cache (stale), then sends the request (revalidate), and finally comes with the up-to-date data again.
请求以自定义 `keystring${props.query}` 为标识符,没有cache时显示loading => data, 对应 key 有cache 时直接显示已 cache 的 data 不 loading,同时发起请求,当请求回来时,替换 cache 值和前端展示。
使用个人示例点点更明白:
第一次点击 post2 , 出现loading
then 显示内容
然后切换回 post1 然后切换回post2 , 不显示loading, 直接显示内容。如果后端数据变化,待接口返回后替换页面内容。
即 Visited 过的 item 都直接显示内容,不 loading,如果远程内容有变化,会先显示cache内容,后显示新内容。当然默认行为可以定制化,比如自动重试,缓存过期时间之类。
典型场景是页面前进后退列表,或者表单下拉框异步数据,这种行为都能避免出现 loading ,提供类似客户端 APP 的感觉,提升用户体验。
取代前端状态管理库
这种库的真正力量在于可以替换掉项目中全部的 data fetching + 状态管理的场景,SWR + Hooks 的开发体验,可以像管理本地数据一样管理远程数据,见代码:
fetcher 封装:
// promise
const fetcher = async (id: number): Promise<Post> =>
await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).then(
(response) => response.json()
);
//hooks
export const usePostItem = (postId: Ref<number>) => {
const enabled = computed(() => postId.value !== -1);
const { isLoading, isError, isFetching, data, error } = useQuery({
queryKey: ["post", postId],
queryFn: () => {
return fetcher(postId.value);
},
enabled,
});
return { isLoading, isError, isFetching, data, error };
};
组件内使用:
<script lang="ts">
export default defineComponent({
setup(props) {
const { isLoading, isError, isFetching, data, error } = usePostItem(
toRef(props, "postId")
);
return { isLoading, isError, isFetching, data, error };
},
});
</script>
<template>
<div>
<div v-if="isError">An error has occurred: {{ error }}</div>
<div v-else-if="data">
<h1>{{ data.title }}</h1>
<div>
<p>{{ data.body }}</p>
</div>
<div v-if="isFetching" class="update">Background Updating...</div>
</div>
<div v-else-if="isLoading" class="update">Loading...</div>
<div v-else>no data</div>
</div>
</template>
这种力量来自 Hooks 编程,对比一下如果用 Vuex ,这将会很繁琐:,
<script lang="ts">
import { mapActions, mapGetters } from 'vuex'
export default {
computed: {
...mapGetters('Address', ['userAddress'])
},
methods: {
...mapActions('Address', ['getUserAddress'])
},
created(){
// 由于不知道 userAddress 是否有值,值是否最新,不得不手动调用 getUserAddress
// 且假如组件树多个层级组件都用这个值,都这么写, API 也会多次调用,造成浪费,除非 fetcher 内再次特殊处理
this.getUserAddress()
}
}
</script>
- 需要手动调用来保持值到最新状态
- 爷孙父子组件的 API 会额外多次调用,冗余且错误,要优化则需要再手动管理 data fetching 的写法。
- mapGetter,mapAction 会截断上下文,类似 mixin ,无法直接追溯到源码,开发体验糟糕
而使用 SWR
setup(props) {
const { isLoading, isError, isFetching, data, error } = usePostItem(
toRef(props, "postId")
);
return { isLoading, isError, isFetching, data, error };
},
只需一行代码:
- data 永远是一个可信赖的值,会由 undefine 变成目标值,并直接更新页面;如远程状态更新时会走 SWR 的默认行为,即最优化的远程状态。同时有工具的 isLoading, isError, isFetching 等获得各种状态,这是 hooks 编程的力量,使得像管理本地数据一样管理远程数据。
- 即使爷孙父子层级节点在同一次渲染中多次调用 usePostItem , 真实 API 也只会调用一次,这是库内部做的优化。
Does React Query replace Redux, MobX or other global state managers?
由此,使用 SWR 可以替换掉 Vue2 项目中大部分 mixin ,vuex 的状态管理,data fetching,使得项目从农业时代进入工业时代。
彩蛋:Vue composition API 的心智负担
React 的心智负担即闭包问题, 想必大家极其熟悉了,可见个人文章。
没深度用过 composition API 时,以为它是白莲花,用过才知道它的心智负担可一点不小。
现在来做个题,
下面直接代码来自 Tanstack 官方示例:
setup(props) {
const { isLoading, isError, isFetching, data, error } = useQuery({
queryKey: ['post', props.postId],
queryFn: () => fetcher(props.postId),
})
return { isLoading, isError, isFetching, data, error }
},
给你20秒,即使你是 react + vue 老鸟,能看出这段代码的一个明显错误吗?
================================ 分隔线 ====================================
给个提示,症状表现为postId 改变,但下面content 保持为第一次的返回值不变了。
================================ 分隔线 ====================================
下面是正确写法:
const { isLoading, isError, isFetching, data, error } = useQuery({
queryKey: ["post", toRef(props, "postId")],
queryFn: () => {
return fetcher(props.postId);
},
});
queryKey 处不能传 props.postId,会丢失响应性。这里就很反直觉了 ,咱明明都给面子没解构赋值了,但防不胜防,这里是作为 useQuery 的入参,给函数入参传入 props.postId, 和解构赋值一样,仍然丢失了响应性。
需要使用 toRef(props, “postId”),来重获响应性。怎么样,如果不是 composition API 用的非常熟,很容易犯错误吧!连 tanstack 官方文档都犯这个错了。至于文档效果为什么看上去正常,因为 demo 里每次切换都手动 unmount 了组件,强行刷新了 props,修改下写法就露馅了。正确写法见 个人示例
也就是说,composition API 的心智负担主要在于任何时候你心里都要将全部的变量当做 Proxy 对象对待,小心呵护它的响应性。这里其实体现出 React 的 DX 优势了,写 React 总是符合 JS 语言的直觉,而写 Vue 总是反这种直觉。
个人能想到的规避方式就是使用 TS 来要求函数入参是 Ref,也是上文的封装方式,通过类型检查来保证响应式:
export const usePostItem = (postId: Ref<number>) => {
const enabled = computed(() => postId.value !== -1);
const { isLoading, isError, isFetching, data, error } = useQuery({
queryKey: ["post", postId],
queryFn: () => {
return fetcher(postId.value);
},
enabled,
});
总结
项目技术升级后经历了几个迭代,运行良好。本项目的加载方式是老版本 Vue 的微前端加载方式,可以说限制极大了,tanstack 用不了,现学现卖的改用了 swrv,也成功完成了改造,可见兼容性挺不错的,还等什么,动手吧。
写文章本身也是一个学习的过程,也请读者能指出文章中的疏忽错漏之处。如果本文对你有所帮助,欢迎点赞收藏。