这是一篇迟来的文章,原定是 21 年的文章,当时恰逢 Vue3 正式发布已经过去了一段时间,打算介绍一下在 Vue3 中的一些新写法,不过因为种种原因拖延到了今天才动笔。
Hooks
Vue3 最大的提升我觉得在于三点:
- 第一引入了 Proxy 以及编译器的重写,让性能提升一个台阶;
- 第二 TypeScript 的重写让开发过程更加丝滑;
- 第三则是引入了 Composition API,它提供了一种新的组织组件逻辑的方式
这里重点讲一下 Composition,它提供了一种全新组织逻辑的方法,在之前 Vue2 中我们只能对组件来进行复用,但是对里面的逻辑就无能无力,因此在 Vue2 中有两种方法:
- 第一种是 Props 不断的拓展,但是缺点也显而易见,让代码混杂在一起,充满各种判断语句;
- 第二则是使用混入,不过这种会让代码调试起来很麻烦,不知道提供的源头在哪里,且对智能推导也不太友好;
而 Composition 解决的痛点就是组件逻辑这部分,例如我有一个轮播图的组件,在 Vue2 中我想把这个开源出去,我需要考虑很多场景,例如样式拓展、鼠标移动是否禁止下一页、是否支持键盘等,我需要把这个东西做的足够大而全才能覆盖 99%的场景,但是这样带来的问题就是组件很大,逻辑很多。 但是如果在 Vue3 中我们使用 Composition 则有一种新的思路,我不再提供样式和 html,只提供好逻辑部分,用户自己结合所需场景自己来绘制样式,在需要切换的时候调用 Api 即可,下面是一个伪代码。
const { previous, nextPage } = useCarouselMap({ ref: dom });
// 点击下一张
nextPage();
在看一个常见的例子,对于网络请求在 Vue2 中一般放到生命周期 created or mounted 中来调用,不过更推荐在 created 中调用,因为可以时机更早一些,如果需要操纵 dom 相关使用 nextTick 即可,下面是一一个示例。
export {
created() {
this.loading = true;
getuser().then((data) => {
this.user = data;
}).catch((e) => {
this.$message.error(e.message);
}).finally(() => {
this.loading = false;
});
}
}
上面的示例模拟请求了获取用户信息,然后加载 loading,对于错误进行提示。这种代码一天中经常会书写很多遍,但是写的次数很多,会在 data 里面充斥各种 loadingxx 以及 dataxxx 等信息,而且仔细分析代码我们写着么多 then 和 catch 都是对数据进行处理,能不能提炼出这部分关键信息呢?
假设我们新建一个 useRequest,这里不讨论实现细节,只看我们要如何使用它
function getUsername(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Mock.mock("@name"));
}, 1000);
});
}
const { data, error, loading } = useRequest(getUsername);
之后如果发生错误之类的,直接在 watch 里面监听提示即可,结合 useRequest 还可以定制超时、重试、缓存等机制。
在看一个实际的业务场景,对于 b 端项目经常会和表格打交道,通常布局如下:
- 搜索栏,对表格的一些信息进行填写和搜索,通常来说就是一个 Input+重置和查询按钮;
- table,对搜索的信息进行展示
- 分页栏,对信息进行过滤展示
而如果有十几个页面,就算我们封装了一些 hooks 还是会觉得有一些繁琐,这里思考一下上面的三部分我们可不可以继续封装呢?
假设我有一个 useTable 的 hooks,它接受一个 form,以及一些初始的分页信息,在加上一个接口请求的函数,每次在点击查询的时候把 form 的信息传递给查询函数,在分页变化的时候也传递信息给查询函数。 最后这个 hooks 返回 table 的 props 等信息,是不是就完成了上述的任务呢?
继续看一段伪代码
const [form] = Form.useForm();
const { tableProps, submit, reset } = useAntdTable(getTableData, {
defaultPageSize: 5,
form,
});
// tableProps传递给table使用
// submit绑定查询按钮,每次点击的时候查询
// reset绑定重置按钮,调用把页面信息回到初始化
上面只是举了一些例子,但是实际开发中,我们基本上可以把很多重复的地方抽离出来进行封装,例如防抖节流、dom 元素是否出现、监听 dom 元素大小等。
新组件
Vue3 中也出现了一些新的组件,算是和 React 全面对齐了,有了这些新的 Api 在日常开发中可以省略需要额外的步骤。
Teleport
<Teleport to="#popup" :disabled="displayVideoInline">
<video src="./my-movie.mp4">
</Teleport>
看官方文档就是把 slot 元素渲染到指定的位置,那么如果自己想实现一个 message 的时候或者想实现返回顶部等功能的时候就不需要额外借助 dom 来处理之类。
Suspense
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback> Loading... </template>
</Suspense>
Suspense 用于加载异步组件,给定一个提示,按照文档说话,以下两种情况可以考虑使用 Suspense:
- 在 setup 中使用了顶层 await
- setup 函数为 async
不过该语法还在实验阶段,目前能想到的场景可能就是低代码平台下,远程请求组件使用 Suspense 提示用户正在加载中,请耐心等待。
template or jsx
在日常写业务代码时候通常都是 template,因为这样效率足够高,且编译器会对此进行优化。不过在写组件的时候我更推荐使用 jsx,原因有下面几点:
- 在写 jsx 的时候类型提示足够友好;
- 习惯了 React 的 jsx,在写 jsx 有一种很舒服的感觉;
- 对于一些很棘手的操作,template 需要传递 slot,但是在 jsx 中只需要包裹一下就结束了;
- 逻辑很连贯,不需要考虑上下文切换了,可以看下面一个例子;
// 1
export default {
setup() {
return () => <div>...</div>
}
}
//2
<script setup></script>
<template>
<div>...</div>
</template>
其他
在 Vue2 中经常会把功能挂载到 this 上来进行调用,不过这样的问题在于使用不够清晰。Vue3 现在可以创建多个实例,所以对应的挂载挂载操作也变成
app.config.globalProperties.$message = message;
其次,对于经常使用的 message 也发生了一些变化,之前
import { createVNode, render } from 'vue'
const vnode = createVNode(
MessageConstructor,
props,
isFunction(props.message) || isVNode(props.message)
? {
default: isFunction(props.message)
? props.message
: () => props.message,
}
: null
)
vnode.appContext = context || message._context
render(vnode, container)
// instances will remove this item when close function gets called. So we do not need to worry about it.
appendTo.appendChild(container.firstElementChild!)
const vm = vnode.component!
如果在 Vue2 中则是这样写
let MessageConstructor = Vue.extend(Main);
instance = new MessageConstructor({
data: options,
});
instance.$mount();
document.body.appendChild(instance.$el);
最后
如果文章有书写错误欢迎指出。