从 Vue3 看组件开发新写法

2,050 阅读2分钟

这是一篇迟来的文章,原定是 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);

最后

如果文章有书写错误欢迎指出。