您现在的位置是:首页 > 文章详情

从 React 迁移到 TypeScript:忍受了 15 年的 JavaScript 错误从此走远

日期:2020-05-31点击:305

云栖号资讯:【点击查看更多行业资讯
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!

Beta 版的 Execute Program 是用 Ruby 和 JavaScript 编写的。之后,我们分几步将整个应用完全移植到了 TypeScript 上。本文介绍的是移植的第一步,也就是前端的部分。

在 Execute Program 的原始 JavaScript 前端中,我经常会犯一些小错误。例如,我会将错误的 prop 名称传递给 React 组件,或者遗漏某个 prop,抑或传递错误的数据类型。(Prop 是作为参数发送到 React 组件的数据。组件将一些 props 传递给自己的某个子组件,以此类推,这是很常见的。)

对于像 JavaScript 和 Ruby 这样的动态语言来说,这不是个小问题。过去 15 年来我一直在学习该如何应对这种错误问题。我在之前谈论的是 2011 年代的情况,其中讨论的缓解措施确实有些用途,但它们无法随着系统的发展顺利地扩展下去,而且我们忘掉它们时也没有安全网可用。

我觉得 15 年时间已经够长了。我想回到静态类型系统的怀抱,毕竟这种系统中根本不会出现这类错误。彼时我们有几个选项:Elm、Reason、Flow、TypeScript 和 PureScript。(这里举了一部分例子。)最后我决定使用 TypeScript 是因为:

  • TypeScript 是 JavaScript 的超集,因此移植起来很容易。移植回来也更容易些:只需删除类型定义即可,然后我们又回到了 JavaScript。
  • TypeScript 编译器使用 TypeScript 编写,并作为已编译的 JavaScript 代码分发,因此我们可以在自己的 Web 应用中运行它。我们的 TypeScript 课程正是这样做的:在浏览器中评估用户的 TypeScript 代码,以避免网络延迟。
  • 这一条是我们的业务特有的:TypeScript 比其他选项更受欢迎。这意味着更多的人希望从像我们这样的课程中学习 TypeScript。用 TypeScript 编写 Execute Program,让我们可以制作出更好的 TypeScript 课程。

在 2018 年 10 月,我们用了大约两天时间将前端 JavaScript 代码移植到了 TypeScript。下面的图表显示了在移植前和移植后每种语言拥有的代码量。

image

当时我们还是 pre-beta 版本,所以系统还很小,只有大约 6,000 行。这张图上没有涉及移植后的情况;我们将在以后的文章中具体介绍相关内容。

在这次移植之后,React prop 问题消失了。下面我们会看几个示例,首先是一个简单的例子。以下是渲染“Continue”按钮的代码,这个按钮出现在我们课程的每个文本段落之后:

<Button autofocus={true} icon="arrowRight" onClick={continue} primary > Continue </Button> 

这个 Button 组件的 props 的类型如下所示。当读取诸如 autofocus?: boolean 之类的属性类型时:“autofocus”是属性的名称;“?”表示它是可选的;“:”将属性名称与其类型分开;而“boolean”是类型。最后一个属性类型 onClick 表示“一个不带参数且不返回任何内容的函数”。如果你不熟悉 TypeScript 的函数类型语法,可以在我们的课程中全面了解 TypeScript 的函数类型。

type ButtonProps = { autofocus?: boolean icon?: IconName primary?: boolean onClick: () => void } 

如果将“autofocus”prop 从 true 更改为 1,会发生什么?现在,我们在类型系统期望一个布尔值的地方传递了一个数字值。不到一秒钟后,编译器将在下面显示错误。(这里删除了一些不相关的细节;本系列文章中所有涉及到错误的地方都会这样处理。)

src/client/components/explanation.tsx(13,27): error: Type 'number' is not assignable to type 'boolean | undefined'. 

有害代码在 vim 中也变成了红色。修好它后,红色消失了。解决错误只需要几秒钟。在 Ruby 或 JavaScript 中,我可能会花几分钟的时间手动测试应用程序,并反复浏览它的状态才能知道到底发生了什么事情。我也可以依靠自动化测试,但是我们在另一篇文章中介绍了测试 vs 类型的问题。

这个整数到布尔的更改是对类型系统的一次简单而低风险的测试。Button 的 icon 属性显示了更高级的用法。下面还是 Button 调用:

<Button autofocus={true} icon="arrowRight" onClick={continue} primary > Continue </Button> 

看起来 icon prop 只是一个字符串:“arrowRight”。在运行时,在已编译的 JavaScript 代码中,它将是一个字符串。但是在上面显示的 ButtonProps 类型中,我们将其定义为 IconName,后者是在其他地方定义的。在查看其定义之前,让我们先看看这个类型的作用。假设我们将“icon”prop 更改为“banana”。我们实际上没有名为“banana”的图标。

<Button autofocus={true} icon="banana" onClick={continue} primary > Continue </Button> 

不到一秒钟后,TypeScript 编译器拒绝了这一更改:

src/client/components/explanation.tsx(13,44): error: Type '"banana"' is not assignable to type '"menu" | "arrowDown" | "arrowLeft" | ... 21 more ... | undefined'. 

编译器说“icon”不能是任意字符串。它必须是我们定义为 icon 名称的 24 个字符串之一。编译器将拒绝任何使我们引用不存在图标的更改;这不是有效的程序,甚至无法开始执行。

有多种方法可以实现 IconName 类型。一种是编写一种类型,该类型显式列出所有可能的 icon 名称。然后,我们必须使 icon 名称与其在磁盘上的图像文件保持同步。这种类型可能是这样的:

type IconName = "menu" | "arrowDown" | "arrowLeft" | "arrowRight" | ... 

翻译成中文:“这里会静态地保证 IconName 类型的一个值是此处指定的字符串之一,但不能是其他任何字符串。”(这个类型是我们两堂课程涵盖的两个主题的组合:字面量类型和类型联合)

我们的 IconName 未被定义为字面量类型的简单联合。让图标名称列表与文件列表保持同步是很无聊的工作,我们可以让计算机来完成它!相反,我们的 icon.tsx 文件如下所示:

export const icons = { arrowDown: { label: "Down Arrow", data() { return <path ... /> } }, arrowLeft: { label: "Left Arrow", data() { return <path ... /> } }, ... }

实际的 SVG < path/> 标签就在源代码中,在以 icon 名称为键的对象中。(也可以在不将 SVG 内联到源文件中的情况下执行此操作。例如,我们可以使用一些 Webpack 技巧将图像保存在它们自己的文件中,但仍然可以确保列表中的每个图标也都存在于磁盘上。到目前为止,这种简单的解决方案对我们来说是很好用的。)

通过这种方式定义 icon 后,我们可以使用一行代码自动提取其名称的联合类型(union type):

export type IconName = keyof typeof icons 

(这里的意思是,你可以认为该类型表示“每当某物的类型为”IconName”时,它必须是与 icons 对象的键之一匹配的字符串。)

这样就搞定了;并不需要其他类型层面的工作。剩下的代码只是一个简单的 Icon React 组件,它在列表中查找图标并返回其 SVG 路径。这个函数中没有明确的 TypeScript 类型。它看起来像是纯粹的 JavaScript 代码,但它也经过了类型检查。这是一个最小版本,其中删除了所有无关的细节:

export function Icon(props: { name: IconName }) { return <svg> {icons[props.name].data()} </svg> } 

现在,我们可以将 SVG 标签放入这个源文件中,并将新 icon 拖放到“icons”列表中。当我们这样做时,这个 icon 就可以在 Button 组件,以及系统内接受 icon 名称的其他任何部分中使用。如果我们从列表中删除一个 icon,则系统中引用该 icon 的所有部分都将立即无法编译,从而确保没有过时的 icon 引用在运行时导致错误。

这些示例按照静态类型标准来说是很简单的,但我认为它们证明了 Web 应用程序中有多少可以轻松实现的改进之处。一个应用程序中的大多数代码都不涉及高级类型系统功能;多数需求仅仅是“确保我们传递正确的 props”和“确保我们的图标确实存在”之类的简单事情。

我们在整个系统中都做了这种事情。其他的一些示例:

我们在整个系统中使用了一个 Note 组件。它具有一个 tone prop 来确定提示的样式:“info”“warning”“error”等。如果我们不再使用其中某个 tone 选项,则我们将从联合类型中将其删除,并且所有引用这个 tone 的 Note 将出错,直到我们更新它们。

我们链接到的每个 URL 都将静态保证存在。当我们重命名或删除 URL 时,链接到它的每个组件都无法编译,直到我们对其进行更新以匹配为止。

当我们链接到这些 URL 时,类型系统可确保我们填充 URL 中的所有空缺。例如,路径“/courses/:courseId/lessons/:lessonId”具有两个 hole,“courseId”和“lessonId”。如果我们尝试链接到该路径,但忘记提供“courseId”,则代码将无法编译。

我们在客户端上发出的每个 API 请求都会被静态确保与相应服务端 API 端点的负载结构匹配。如果我们在端点中重命名一个属性,哪怕属性在嵌套的 API 对象的内部深处,引用该端点属性的任何代码也都将无法编译,直到我们对其更新以匹配为止。我们在另一篇文章中介绍了细节。

诸如此类的问题经常会出现在编程工作中,尤其是在动态语言中非常常见;但我们无需编写任何自动测试,也用不着什么手动测试,就可以从静态上避免这些问题。有些问题解决起来需要费些功夫。我们的 API 路由器验证写起来很麻烦。但是写多了就顺手了。上面的单行“IconName”类型实际上是问题的完整解决方案。如果将其复制到 TypeScript 文件中,它就能起作用。

将我们的前端代码移植到 TypeScript 仅仅是个开始。那之后,我们又将后端从 Ruby 移植到了 TypeScript,然后在移植后的 9 个月内对其进行了扩展和维护。

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/live

立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-06-01
本文作者:Gary Bernhardt
本文来自:“infoq”,了解相关信息可以关注“infoq

原文链接:https://yq.aliyun.com/articles/763142
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章