likes
comments
collection
share

Vue3 + TS 最佳实践

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

导读

笔者有 3 年多 React + TS 的实践经验,深刻体会到 TS 对生产效率的提升作用。最近换了新团队,技术栈是 Vue3 + TS,目前还只是个架子,离深度应用还有一段距离。所以 Vue3 + TS 的最佳实践是笔者最近研究的方向,现将阶段性成果总结成文,供大家参考。

注:本文的 demo 场景与部分代码会复用笔者另一篇文章 Vue 3 和 React 16.8 到底能多像 中的内容,下文会有说明,不会造成太多的额外阅读成本

背景

其实尤大在 Vue 3.2 发布的时候已经在微博给出了最佳实践的解决方案:

<script setup> + TS + Volar = 真香

Volar 是个 VS Code 的插件,个人认为其最大的作用就是解决了 template 的 TS 提示问题。注意,使用它时,要先移除 Vetur,以避免造成冲突。

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 Typescript 声明 props 和发出事件。
  • 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
  • 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。

详见官方文档 单文件组件 <script setup>

其实上述内容已经基本解决了绝大部分的诉求,但是还有一些需求并没有被满足,主要是 2 点。

一是目前 defineProps 还不支持使用从其他文件导入的 TS 类型,官网的原文是:

现在还不支持复杂的类型和从其它文件进行类型导入。理论上来说,将来是可能实现类型导入的。

我们需要给出一个统一的参考解决方案,指导实践开发。

二是没有给出 JSX 模式的最佳实践。虽然有了基本方案后,必须使用 JSX 去充分利用 TS 的场景已经不多了,但是还是能有最好。为使主题更聚焦,本文将只简单说一下思路,细节将另起一文。

除了以上两点之外,尤大虽然给出了最佳实践的方向,但是想要指导团队的实践,还需要补充一些细节,尤其是案例。本文会分别就以上三点进行展开。

目标

首先明确下最佳实践想要达到的效果:

  1. 任何场景下,TS 友好的编码提示和自动补全
  2. 组件使用时传入 props 的校验和提示。如 <input :value="value" /> 组件,要能校验 value 是否为 string 类型。
  3. TS 的流转不断层。如 Vue 的模板语法也要能顺利的承接 TS 的各种功能。

最佳实践

细节补充

我们还是用表单场景来举例,为了节省篇幅,请移步 Vue 3 和 React 16.8 到底能多像 查看 “场景说明” 与 “API & Types” 部分,获取上下文。另外,本小节只使用 Form 的例子就已足够具有代表性。

Demo - Form

<template>
  <div>
    <div>
      <div>Name</div>
      <input type="text" v-model="name" />
    </div>
    <div>
      <div>Sex</div>
      <input type="radio" name="sex" :checked="sex === Sex.male" @click="() => sex = Sex.male" />Male
      <input type="radio" name="sex" :checked="sex === Sex.female" @click="() => sex = Sex.female" />Female
    </div>
    <p>
      <button @click="handleSubmit">Submit</button>
    </p>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { Sex, fetchUserInfo, updateUserInfo } from "../services";
const name = ref("");
const sex = ref(Sex.male);

onMounted(() => {
  fetchUserInfo("id-xxx").then((res) => {
    name.value = res.name;
    sex.value = res.sex;
  });
});

const handleSubmit = () => {
  const params = { name: name.value, sex: sex.value };
  updateUserInfo(params).then((res) => {
    if (res) alert(JSON.stringify(params));
  });
};

</script>

以上实现照普通的 setup + template 的实现有几处明显的提升

  1. 避免了冗长的 return 值,使代码量明显减少。实际上,

当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用 —— Vue3 官方文档

也就是说不用再声明 components 了。这进一步减少了代码。

  1. 顺带的,import 进来的 TS enum 类型(Sex),也可以方便的在 template 中使用了。
  2. 如果装了 Volar,在 template 中对于 Sex 的代码提示、校验也是非常的友好。

可以看到,面对普通的场景,<script setup> 在开发体验上已经有了明显的提升。接下来就是补充上文提到的另外两块内容了。

defineProps 无法外部引入 TS

为模拟 props 的场景,我们将 Table 的案例改造一下:userList 不再是从 API 获取的,而是通过 props 传入的,来看下代码。

注:从 API 获取数据的逻辑放在了父组件 TableWrapper 中,其代码将放在文末,以免影响行文节奏。

<template>
  <table :cellPadding="5" :cellSpacing="5">
    <tr>
      <th>Name</th>
      <th>Sex</th>
    </tr>
    <tr v-if="userList.length === 0">
      No Data
    </tr>
    <tr v-else v-for="user in userList" :key="user.name">
      <td>{{ user.name }}</td>
      <td>{{ user.sex === Sex.male ? "Male" : "Female" }}</td>
    </tr>
  </table>
</template>
<script setup lang="ts">
import { defineProps, withDefaults } from "vue";
import { Sex, UserInfo } from "../services";

interface Props {
  userList: UserInfo[];
}
withDefaults(defineProps<Props>(), {
  userList: () => [],
});
</script>

先说最关键的一点,官网文档中说“不支持从其他文件引入”是指 defineProps 所接收的泛型,就是上文的 Props。但是从例子中可以看到,SexUserInfo 都是从外部引入的,实际上是可以使用的。也就是说理论上目前只是限制了组件级别的 Props 的 TS 类型的跨文件复用而已(请仔细理解这句话)。如果只是这样,那还是可以接受的。一是组件间复用 Props 的场景一般不多,二是可以通过类似上例的更细粒度的声明来解决,只是会有一些冗余。

接下来再画几个重点:

  1. defineProps 可以使用运行时声明和类型声明两种方式(详见官方文档),但是不能同时使用。为了使 TS 更好的流转,实践中建议使用上例中的类型声明方式。
  2. withDefaults 用来声明 props 的默认值,使用方式与运行时声明定义 default 的格式一样。
  3. defineEmits 的 TS 类型声明格式请一定参考官网给的例子,如果使用错误,会造成编辑器提示错误,emits 也不会有代码提示,具体格式如下:
// e 为 emit 的名字,第二个参数的 key 可以随意定义
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

JSX 最佳实践思路

上文已经提到,有了以上的最佳实践,需要用到 JSX 的地方已经不多了。但是 JSX 还是有自己的优势(长期使用 React 的惯性思维使然),如:

  1. 更灵活,可以使用任何 JS 语法,见官网案例
  2. props 不需要进行小驼峰与“-”的转换,更直观且不会增加额外记忆负担; 当然,也有人会说它有不好的地方,我们暂不纠结于这一点,将重点聚焦到 JSX 的最佳实践思路上。实际上,functional component 是最适合使用 JSX 实现的,因为它简单的就只剩下了一个函数。那么普通组件要如何实现呢?请允许笔者先准备一下,后续会附上另一篇文章的地址。(2021.09.14 更新:Vue3 + TSX 最佳实践?不存在的

结语

目前来看,Vue3 + TS 的最佳实践的基本框架已经定了,而且看起来效果还是很不错的。根据团队的实际情况,补充一些实践细节的规范,就可以愉快的在项目中实践了。

有一点笔者是比较确定的,那就是 TS 会越来越多的被用到前端项目中,因为它对开发效率以及质量的提升是非常明显的。所以在项目中使用 TS,不管对于团队还是个人来说,都是一件划算的事。当然,迁移成本我们也需要考虑进去,想用 TS 就要升级成 Vue3。如果强行让 Vue2 使用 TS,相信我,你不会喜欢的。

最后,据说“自从用了 TypeScript 之后,会再也不想用 JavaScript 了”。反正我是信了。

参考文献

代码附录

TableWrapper.vue

<template>
  <Table :user-list="userList" />
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { fetchUserList, UserInfo } from "../services";
import Table from "./Table.vue";
const userList = ref<UserInfo[]>([]);

onMounted(() => {
  fetchUserList().then((res) => {
    userList.value = res;
  });
});
</script>