在Vue.js中使用TypeScript的原因(附实例)

242 阅读7分钟

Vue.js是一个出了名的灵活的框架,其应用范围从逐步增强静态页面到建立完整的单页面应用程序。同样地,Vue也提供了灵活的开发方式,既可以使用普通的JavaScript,也可以使用更严格的类型安全对应的TypeScript。

这就提出了一个问题:你是否应该在你的Vue.js驱动的应用程序中使用TypeScript?在这篇文章中,让我们来看看你可能选择这样做的一些原因。

在编码时捕捉错误

由于TypeScript是一种强类型语言,它能够在你的IDE中检测出更多的问题。与此不同的是,只有在代码在浏览器中运行后才能发现上述问题。这可能意味着在人工测试过程中或在终端用户报告后才发现问题。

例如,假设你想把一个9位数的电话号码格式化为破折号,以便显示。

<script setup>
import { ref, computed } from "vue";
const phone = ref("55566677777");
const [match, one, two, three] = phone.value.match(/^(\d{3})(\d{3})(\d{4})$/);

// 555-666-7777
const formatted = computed(() => `${one}-${two}-${three}`);
</script>
<template>Phone: {{ formatted }}</template>

到目前为止,我故意不使用lang="ts" ,以显示使用普通JavaScript会发生什么。现在,上面的方法会像预期的那样工作,但也许你很匆忙,或者电话号码实际上在另一个文件中,或者它是一个函数的结果。长话短说,如果电话号码最终是一个实际的数字而不是一个字符串呢?

const phone = ref(55566677777);

在我的IDE中工作,没有迹象表明有问题。

这张截图是直接从VS Code中截取的,我看不出有什么问题。没有看到红色的斜线,我甚至在使用ESLint。我们确实在匹配下有一条黄色的斜线,但这只是因为我们没有使用这个变量,这并不是错误的原因。

phone number type doesn't cause error in JS

但是,如果我跳到浏览器上,事情就会立即中断。整个应用程序停止工作,我在控制台得到这个错误。

error phone.value.match is not a function

问题是,数字不像字符串那样有一个匹配方法。好吧,这是个很容易解决的问题。在IDE中,我们可以再次用引号包住数字,但在这样做的时候,我们不小心给它加上了另一个数字。

const phone = ref("555666777777");

做完修改后,我们回到浏览器中,通过刷新页面再次运行东西。这一次我们得到了另一个错误。

error phone.value.match is null

这一次的匹配是空的,因为字符串不符合重码,而且我们不能把空的东西分散到一个数组中。

即使是我们正在处理的这一小段代码,这个过程已经变得有点烦人了,但对于更大的应用程序,它可能不仅仅是烦人。它可能导致重复的、耗时的、需要多次点击和在浏览器中等待的测试,以及完全错过一些边缘情况。

如果我不需要等待运行代码来捕捉这种情况呢?这就是TypeScript的用武之地。在组件的脚本部分添加lang="ts" ,可以立即在我的IDE中显示出错误,并且将鼠标悬停在错误上,提醒我匹配的结果可能是一个数组,但也可能是空的。

TypeScript hint shows that the result of match could be an array or null

有了这些信息,我们现在可以很容易地处理这种边缘情况,如果匹配结果为空,则默认为空数组,如果匹配结果为未定义,则为我们的格式化电话号码显示 "Invalid Phone "字符串。

<script setup lang="ts">
import { ref, computed } from "vue";
const phone = ref("555666777777");

// default to empty array if result of .match is null
// (making match, one, two, and three undefined)
const [match, one, two, three] =
  phone.value.match(/^(\d{3})(\d{3})(\d{4})$/) || [];

// 555-666-7777
const formatted = computed(() =>
    // check if match is undefined
  match ? `${one}-${two}-${three}` : "Invalid Phone"
);
</script>
<template>Phone: {{ formatted }}</template>

此外,如果我们不小心再次使用一个真实的数字而不是字符串,TypeScript也会立即提醒我们。

TypeScript shows hint that number doesn't have a match method

充满信心的重构

在Vue.js项目中使用TypeScript的另一个好处是,你可以更加自信地进行重构。

假设你有一个博客应用程序的PostForm组件,在普通的JavaScript中看起来是这样的。

// PostForm.vue
<script setup>
import { ref } from "vue";
defineEmits(["create"]);
const title = ref("");
const body = ref("");
</script>
<template>
  <form @submit.prevent="$emit('create', { body, title })">
    <input v-model="title" />
    <textarea v-model="body"></textarea>
    <button>Create Post</button>
  </form>
</template>

它记录了一个标题和一个正文作为反应式引用。它还在表单提交时发出一个创建事件,该对象包括作为属性的正文和标题。

现在我们假设你想把这个事件重构为它自己的处理函数,这样你就可以用它做更多的事情(比如做一些验证,在提交后重置表单数据,或者其他什么)。

// PostForm.vue
<script setup>
import { ref } from "vue";
defineEmits(["create"]);
const title = ref("");
const body = ref("");
const handleSubmit = () => {
  $emit("create", { body, title });
};
</script>
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="title" />
    <textarea v-model="body"></textarea>
    <button>Create Post</button>
  </form>
</template>

在我的项目中启用了ESLint,我至少在这里得到了通知:$emit 没有被定义。

ESLint gives hint that $emit is undefined

为了解决这个问题,我们应该捕获从defineEmits宏中返回的emit函数,然后用它替换处理函数中的emit。

const emit = defineEmits(["create"]);
//...
const handleSubmit = () => {
  emit("create", { body, title });
};

这样一来,就我的IDE所知,一切都好办了。然而,实际上还有一个问题隐藏在重构的过程中。你能发现它吗?

<script setup>
import { ref } from "vue";
const emit = defineEmits(["create"]);
const title = ref("");
const body = ref("");
const handleSubmit = () => {
  emit("create", { body, title });
};
</script>
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="title" />
    <textarea v-model="body"></textarea>
    <button>Create Post</button>
  </form>
</template>

别担心,如果你没发现。这是很棘手的!这个问题是,以前我们发射的是正文和标题的字符串。但现在,由于body和title不再被自动解包,我们要发射的是reactive refs。

因此,为了保持我们的组件界面与重构前相同,我们应该改变emit,使其包括body.valuetitle.value

emit("create", { body: body.value, title: title.value });

呜呼!这是个很好的例子。可以理解的是,你在自己的重构中可能会错过这个问题。不幸的是,结果很可能是浪费了几分钟甚至更多的时间,在浏览器中测试完之后,试图找出出错的原因。

现在让我们来看看同样的重构在TypeScript中是如何进行的。

首先,如果你在使用TypeScript,你会有输入自定义事件的习惯。因此,你已经定义了创建事件的有效载荷的类型,就像这样。

defineEmits<{
  (e: "create", payload: { body: string; title: string }): void;
}>();

对于TypeScript的新手来说,这只是将payload对象中的body和title属性的类型定义为字符串。

然后在你处理了$emitemit 的问题之后,你会得到下面的代码。

<script setup lang="ts">
import { ref } from "vue";
const emit = defineEmits<{
  (e: "create", payload: { body: string; title: string }): void;
}>();
const title = ref("");
const body = ref("");
const handleSubmit = () => {
  emit("create", { body, title });
};
</script>
<template>
  <form @submit="handleSubmit">
    <input v-model="title" />
    <textarea v-model="body"></textarea>
    <button>Create Post</button>
  </form>
</template>

不过这一次,你的IDE会让你知道有些东西很可疑。VS Code非常直接地用红色的斜线呼出事件有效载荷的body和title属性。将鼠标悬停在每一个属性上,可以准确地显示出问题所在:也就是说,一个反应式引用和一个字符串不是一回事。

TypeScript hint shows the Vue custom event expects an object with a title and body property set to strings and not reactive references

当然,解决方案和以前一样,但这次我们能够立即修复它,因为反馈是即时的,而且是在IDE的背景下。修复后,红色的斜线消失了。

errors in IDE disappear when strings are properly provided instead of the reactive refs

增强的IDE功能用于组件之间的交流

除了重构时在IDE中增强的错误可见性,TypeScript还将在组件之间的通信中帮助你。这是由于更加集中和准确的自动完成选项以及道具和事件的错误检测而实现的。

让我们来看看上面的同一个例子。作为提醒,在JavaScript中我们会这样写emissions的定义。

// PostForm.vue
const emit = defineEmits(["create"]);

如果你现在去在另一个组件中实际使用PostForm组件,并监听创建事件,你会得到事件有效载荷中没有自动完成选项。

<PostForm @create="$event"/>

IDE shows no autocomplete options for the create custom event

然而,如果你把它切换回类型声明语法,像这样定义发射。

const emit = defineEmits<{
  (e: "create", payload: { body: string; title: string }): void;
}>();

那么你就会得到一个完整的、极其集中的事件可用属性的自动完成列表,以及它们的类型。

TypeScript provides autocomplete options for Vue.js custom events

试想一下,这让组件之间的交流变得多么容易根本不需要去查看你所使用的组件的代码或文档,就可以看到某个特定事件的有效载荷。

同样的好处也适用于道具。如果我们允许我们的PostForm接受一个现有的帖子来进行编辑,看起来会是这样的。

//PostForm.vue
const props = defineProps<{
    // the question mark makes it an optional prop 
    // (since our form could be used for new or existing posts)
  post?: { body: string; title: string };
}>();

//...

const title = ref(props.post?.title || "");
const body = ref(props.post?.body || "");

在这一点上,我们甚至可以将我们的帖子对象的形状规范化为一个可重用的接口。

// PostForm.vue
interface Post {
  body: string;
  title: string;
}
const props = defineProps<{
  post?: Post;
}>();
const emit = defineEmits<{
  (e: "create", payload: Post): void;
}>();

最后,我们应该看看将道具传入组件的实际情况是怎样的。

// App.vue
<script setup lang="ts">
import { ref } from "vue";
import PostForm from "@/components/PostForm.vue";
const existingPost = ref({
  body: "",
  tite: "",
});
</script>
<template>
  <PostForm :post="existingPost" />
</template>

如果你仔细注意,你可能会注意到我在现有的帖子中出现了一个错字。如果你直接在VS代码中工作,你根本不需要密切注意,因为TypeScript会清楚地告诉你。

TypeScript hint shows the Vue prop doesn't contain the correct properties

这不仅有助于捕捉错别字,而且,特别是对于较大的对象,确保所有必要的属性实际存在,并且是正确的类型。换句话说,你可以在你的IDE中获得关于一个道具应该是什么样子的文档。

总结

对于一个JavaScript开发者来说,TypeScript可能是相当可怕的。(我可以这么说,因为对我来说就是这样。)然而,跳过最初的恐惧,即使只是学习最基本的语法,也可以帮助你在Vue.js项目中开始收获TypeScript的好处。

如果你想更深入地了解如何在你的Vue.js项目中使用TypeScript,你应该看看我们的课程。TypeScript基础TypeScript与Vue.js 3

如果你从未接触过TypeScript,或者只是需要复习一下基础知识,那么TypeScript基础将教你经常使用的TypeScript基本原理和语法。

TypeScript与Vue.js 3是为那些已经了解基础知识的人准备的,它教你在一个典型的Vue.js 3项目中使用TypeScript的常见用例和方法。

无论你对TypeScript的了解程度如何,如果你能联系到本文中描述的TypeScript可以帮助解决的问题,那么请查看我们的课程,我们将一如既往地提供帮助!😃