导读
笔者有 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 的场景已经不多了,但是还是能有最好。为使主题更聚焦,本文将只简单说一下思路,细节将另起一文。
除了以上两点之外,尤大虽然给出了最佳实践的方向,但是想要指导团队的实践,还需要补充一些细节,尤其是案例。本文会分别就以上三点进行展开。
目标
首先明确下最佳实践想要达到的效果:
- 任何场景下,TS 友好的编码提示和自动补全
- 组件使用时传入 props 的校验和提示。如
<input :value="value" />
组件,要能校验 value 是否为 string 类型。 - 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 的实现有几处明显的提升
- 避免了冗长的 return 值,使代码量明显减少。实际上,
当使用
<script setup>
的时候,任何在<script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用 —— Vue3 官方文档
也就是说不用再声明 components 了。这进一步减少了代码。
- 顺带的,import 进来的 TS enum 类型(Sex),也可以方便的在 template 中使用了。
- 如果装了 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
。但是从例子中可以看到,Sex
和 UserInfo
都是从外部引入的,实际上是可以使用的。也就是说理论上目前只是限制了组件级别的 Props 的 TS 类型的跨文件复用而已(请仔细理解这句话)。如果只是这样,那还是可以接受的。一是组件间复用 Props 的场景一般不多,二是可以通过类似上例的更细粒度的声明来解决,只是会有一些冗余。
接下来再画几个重点:
defineProps
可以使用运行时声明和类型声明两种方式(详见官方文档),但是不能同时使用。为了使 TS 更好的流转,实践中建议使用上例中的类型声明方式。withDefaults
用来声明 props 的默认值,使用方式与运行时声明定义default
的格式一样。defineEmits
的 TS 类型声明格式请一定参考官网给的例子,如果使用错误,会造成编辑器提示错误,emits 也不会有代码提示,具体格式如下:
// e 为 emit 的名字,第二个参数的 key 可以随意定义
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
JSX 最佳实践思路
上文已经提到,有了以上的最佳实践,需要用到 JSX 的地方已经不多了。但是 JSX 还是有自己的优势(长期使用 React 的惯性思维使然),如:
- 更灵活,可以使用任何 JS 语法,见官网案例;
- props 不需要进行小驼峰与“-”的转换,更直观且不会增加额外记忆负担; 当然,也有人会说它有不好的地方,我们暂不纠结于这一点,将重点聚焦到 JSX 的最佳实践思路上。实际上,functional component 是最适合使用 JSX 实现的,因为它简单的就只剩下了一个函数。那么普通组件要如何实现呢?请允许笔者先准备一下,后续会附上另一篇文章的地址。(2021.09.14 更新:Vue3 + TSX 最佳实践?不存在的)
结语
目前来看,Vue3 + TS 的最佳实践的基本框架已经定了,而且看起来效果还是很不错的。根据团队的实际情况,补充一些实践细节的规范,就可以愉快的在项目中实践了。
有一点笔者是比较确定的,那就是 TS 会越来越多的被用到前端项目中,因为它对开发效率以及质量的提升是非常明显的。所以在项目中使用 TS,不管对于团队还是个人来说,都是一件划算的事。当然,迁移成本我们也需要考虑进去,想用 TS 就要升级成 Vue3。如果强行让 Vue2 使用 TS,相信我,你不会喜欢的。
最后,据说“自从用了 TypeScript 之后,会再也不想用 JavaScript 了”。反正我是信了。
参考文献
- 谈谈 Vue 模板和 JSX
- Vue3 官方文档(强烈建议仔细阅读,这么好的文档不多见)
代码附录
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>