1_5JY0_41OTfKe5ZGNTl_zCA.png

简介

前端开发是一个热闹的地方,有很多新技术不断涌现。在过去的十年中,我们看到网页的开发方式发生了明显的变化。我们不再需要直接接触DOM API或玩弄笨拙的Angular依赖注入。目前,我们大量使用不同的构建工具来在代码中包含资产,处理CSS等等。

我们从几十个文件中构建FE应用程序,我们使用捆绑器来为我们生产生产代码。这使得我们在最终产品中编译我们的代码库,这与类型化语言的做法相当相似。但有一个很大的区别:我们使用的是JavaScript,一种超级灵活但非常容易出错的语言,没有任何类型。通过文件编译(或者说是TypeScript的捆绑和转译),没有什么能阻止我们用能产生JavaScript的东西来替代JavaScript。

市场上有很多关于如何实现前端开发的类型系统的想法。仅举几例:
TypeScript、ClojureScript、PureScript、Scala.JS、Elm、JS_of_ocaml、ReScript,甚至可以通过WebAssembly和JS绑定(如wasm-bindgen)用C或Rust编写前端。

在这篇文章中,我想强调ReScript的开发和商业优势。我希望在讲座结束后,你会有一个论点和反论点的清单,帮助你下定决心在你的应用程序中使用什么技术。

TLDR。
JavaScript没有类型系统,这使它成为快速建立原型的优秀工具。同时,由于缺乏类型,每一个改变都有可能在运行时破坏代码。更广泛的代码库将使错误规模更大,这严重影响了维护和上市时间。像 ReScript 这样的强类型语言有助于解决这些问题,这可以对业务产生积极影响,并使开发更加顺利。

除此以外,ReScript:

  • 编译速度极快
  • 具有消除死代码的功能 - 更小的生产包尺寸
  • 具有JS开发人员易于理解的语法
  • 易于在现有项目中实施并与JS代码库集成
  • 具有使开发更容易的功能特性

ReScript的功能可以帮助开发者更顺畅地创建代码:

  • 管道操作符
  • 缺省情况下的currying
  • 可以携带值和模式匹配的变体类型
  • 一切都是表达式
  • 默认情况下是不可变的数据结构(期待数组)
  • 模块(超过对象)

在缺点方面:

  • ReScript的代码格式化是一种相当不愉快的体验,它使代码更难阅读(希望这在未来可能会改变)。
  • ReasonML的一些伟大功能在ReScript中不再有效(比如单参数的趣味函数或被废弃的管道
  • ReScript没有Redux的模拟。构建多存储、多子应用、强类型的软件需要大量的工作。
  • 如果一个项目需要对许多具有复杂API的库进行开箱即用的支持,并不太适合。

Shiny Rescript的优势

1 使用类型系统守护的更安全的开发

在我成为JavaScript开发者的第一年,我并不理解在编译时抓取bug的必要性。构建一个JavaScript应用程序是相当快的,尤其是在热模块替换时。开发者几乎可以立即在浏览器中手动测试结果。有几个开发工具可以追踪JavaScript运行时的错误。随着应用程序的范围越来越大,发现错误的途径也越来越长,捕捉错误也越来越难。

有了类型系统,开发者可以从编译器那里得到即时的反馈,并有描述性的错误信息。

TypeScript类型系统在某些方面解决了这些问题。然而,它并不保证能解决这些问题。我写TypeScript已经有几年了,我相信有某种类型系统总比没有好。直到我接触到Rust语言。Rust是一种非常强类型化的语言,使你几乎100%确信,如果代码被编译,它就会运行。TypeScript与此相去甚远,所以我希望在前端有与Rust类似的体验。我找到了ReScript(当时是ReasonML),并决定有一个好的类型系统比任何类型都好。

ReScript也是一种强类型语言,包含一个强大的OCaml类型系统,在大多数情况下都可以进行干扰。

我打算简单比较一下ReScript和TypeScript而不是JavaScript,因为

  1. JavaScript中没有静态类型系统。
  2. TypeScript是目前前端使用最广泛的类型化语言。

与TypeScript相比,ReScript最显著的优势。
只有一种方法可以用记录来定义对象等价物

  1. type test = {
  2. name: string
  3. }

以及TypeScript中的多种潜在方式。

  1. type Test = {
  2. name: string
  3. };
  4. interface Test2 {
  5. name: string
  6. }
  7. class Test3 {
  8. name: string;
  9. constructor(value: string){
  10. this.name = value;
  11. }
  12. }

你应该在不同情况下使用其中的3种。但TS的构建允许所有这些类型,使事情变得有点混乱。

另外,类型不仅仅是为了编译器。它们也是用来记录代码的。对于TypeScript,构建抽象可能会导致在定义站点上失去类型检查,而在调用站点上仍然得到它。这给编译器提供了保证,但可能描述性较差,调试起来也比较混乱。如果不明确地传递类型,并使用上面提供的所有3个例子,可能需要花费一些精力来了解实际使用的是哪种类型。

在 ReScript 中,一切都有其类型

在 ReScript 中,所有变量、记录、函数,甚至模块都有类型。这意味着编译器总是在你试图传递不适合的东西时告诉你。与此相反,TypeScript 有一个叫做 any 的东西,这违背了类型系统的全部意义。当接口不符合当前的要求时,TypeScript的新人经常过度使用any。这就把编译器的警惕性降低了,并发出信号说,如果实现类型太难,代码结构可能有问题。

可以将TypeScript配置为不接受any,但tsconfig本身只允许禁用隐式使用。你将需要一个linter配置来禁止任何。但是这将会导致不同类型的错误(linting错误而不是编译器错误),并且会在某种程度上不允许实现抽象性。

ReScript中没有null或undefined

类型系统并不能解决所有问题。它不会修复我们代码中的逻辑错误,但在少数情况下它可以帮助解决这些问题。空值和未定义往往会潜入JavaScript代码,并导致意外行为。TypeScript也不能解决这个问题。相反,ReScript不允许未定义或空值。必须始终有正确的类型。对于可能没有值,或者某些表达式可能返回错误的情况,ReScript使用变体类型。通常情况下,option(Haskell Maybe)和result携带一个值,关于它的不存在或错误的信息。此外,ReScript 编译器将迫使我们涵盖负面情况。

这是思考代码的一个绝对开关,迫使我们避免可能以意外行为结束的逻辑。

ReScript 变体类型与 Rust enum 非常相似。如果你想了解更多,我推荐这个关于该主题的Rust文档。
变量类型也是构建我们的还原器和行动的绝佳方式,因为其语法比TS条件类型要紧凑得多。

卓越的类型推理

TypeScript确实有类型推理,但突出了前面的几点,任何、未定义和null的存在,开发者往往必须明确地进行类型声明。
此外,它对干扰匿名函数并不总是很有效,而且在定义参数的类型签名时,它可能很麻烦。

相比之下,ReScript 编译器在推断类型方面非常出色,在大多数情况下都能做到。这意味着需要声明类型的地方更少。这意味着更少的模板代码,意味着更愉快的开发。所有这些都还在编译器防护的范围内。

所有这些点可能听起来不多,但结合起来在实践中会产生巨大的差异。TypeScript试图用类型系统来吸引开发者,而ReScript则强制使用类型,并防止用户出现大量的运行时错误。由于没有任何和类型的强制,这对JavaScript开发者来说是一个 “基于类型的开发 “的心理转换。

#2 快速编译时间和消除死代码

TypeScript 3的速度越来越快,但仍落后于ReScript。ReScript的编译速度很快。对于小型和中型代码库,通常是几毫秒的事。对于广泛的应用程序,可能需要几秒钟,但编译器几乎立即响应成功或错误。

我必须提到,ReScript输出不是我们的生产捆绑。ReScript编译器会返回与原始文件相对应的JavaScript文件。这意味着我们仍然需要用Webpack等工具来捆绑ReScript输出。

幸好我们的输出是清晰可读的JS文件,没有JSX。这使得捆绑更快,而且不需要JSX翻译。在实践中,具有热模块替换功能的Webpack Dev Server重新加载网页的速度非常快,在应用程序或工作区之间切换时甚至可能没有人注意到。

JavaScript输出文件也被清理掉了任何未使用的代码。

#3 易于在现有项目中实施

有几件事使 ReScript 易于添加到现有项目中。

  1. 在使用 ReScript 时,您最有可能与 ReScript-react 一起工作。这不是一个新的框架。相反,它为React提供了绑定,正如我之前提到的,ReScript将为你输出.res组件的.JS版本。这意味着你可以在生产捆绑中使用一个React运行时。
  2. 你可以使用genType库将ReScript类型输出为TypeScript类型,或者在你的代码中使用TS类型。
  3. 由于ReScript的绑定,使用现有的JavaScript和TypeScript代码库相对容易。用 ReScript 绑定来包装现有的 JS 库也是毫不费力。
  4. 由于绑定和JavaScript输出,很容易使用webpack等工具来管理样式预处理器或资产加载器。
    5.ReScript的语法与JavaScript的语法非常相似,对于JS开发者来说应该非常容易掌握。

#4 带有函数式的开发更容易

你可能会把 “函数式编程 “与纯函数、管理副作用、声明式编程、漏斗、单体等联系起来。我不想深究这些细节,因为已经有很多关于FP的绝对精彩的材料了(Haskell书,scala课程等)。此外,我也不想说服你进行纯粹的函数式开发。我只是想强调,与严格意义上的FP语言相比,ReScript的实用味道如何对开发产生积极影响。

TypeScript不是一种函数式语言。事实上,它几乎就像浏览器的鸭子类型的Java。它并没有阻止你实现功能化的特性,但它使事情的要求大大增加,导致复杂的代码和过度工程。它使事情变得更难,而函数式编程应该使事情变得更简单。

此外,你可以尝试实现Maybe monad(ReScript选项)或使用尴尬的union类型,引入currying或用库强制实现不变性。但是,TypeScript/JavaScript的语法仍然会是你的敌人。

对我来说,函数式编程考虑的是数据流而不是要声明什么变量或对象。

ReScript为我们提供了处理函数式代码的可靠工具,我将简要介绍使编写代码变得顺利和愉快的最主要方式。

Pipe 操作符

在JavaScript & TypeScript中,函数最重要的问题之一是如何将一个函数的结果传递给另一个。

如果没有管道运算符,有3种选择。

1. 将每一个返回值分配给一个变量。

  1. let res1 = run1(value);
  2. let res2 = run2(res1);

This can end up with a lot of unnecessary assignments. With many variables, we can easily skip thinking about our data and spend most of the time managing names. This also results in more imperative code.

2. 用另一个函数的执行来包裹函数的执行:

  1. run4(run3(run2(run1(value))));

这很快就会失去控制,增加更多的括号。这也很难重构,而且执行的顺序是从右到左,这可能有点混乱,不直观。

3. 用方法返回对象。

  1. class Obj1 {
  2. value: number;
  3. constructor(value: number) {
  4. this.value = value;
  5. };
  6. public addOne() {return this.value + 1};
  7. };
  8. class Obj2 {
  9. static multiplyByTwo(value: number) {return new Obj1(value * 2)}
  10. };
  11. Obj2.multiplyByTwo(2).addOne();

这是一种常见的没有管道操作员的链式操作方法。这种方法至少有几个缺点。

首先,它需要定义我们想要返回的结构(在我们的例子中是obj1)。这意味着我们不能把注意力放在函数上。我们需要一次完成整个链条。

第二,在我们研究Obj1的定义之前,multiplyByTwo返回什么并不明显。

第三,使用这个意思,我们不使用纯函数,因为我们使用可能会改变的对象上下文,我们可以用变异对象属性来改变它。这是一个有很多潜在bug和难以调试的地方。

用pipe操作符。

  1. value
  2. -> MathOperation.multiplyByTwo
  3. -> MathOperation.addOne

这使得事情更容易管理。函数是纯粹和直接的。此外,执行顺序也很直观。由于合格的导入(通过导入和使用整个模块’MathOperation’而不是函数multiplyByTwo & addOne),可以清楚地知道哪些函数的来源和相关功能。
不幸的是,在从ReasonML重塑为ReScript之后,核心团队决定废弃三角管|>,只使用->

|>将我们的值作为最后一个参数,而->则将其作为第一个参数。问题是,在我看来,->的语法有点不幸,因为箭头在许多语言中也被用来描述类型注释。

默认情况下的库里

在ReScript中,你定义的每个函数都是默认的咖喱。这意味着每次你没有传递所有的参数,该函数就会返回一个接受其余参数作为参数的函数。

简单的例子

  1. let add = (a,b) => a + b;
  2. add(2,2); // returns 4
  3. let addTwo = add(2); // function that adds 2 to passed argument
  4. addTwo(3); // returns 5

好吧,这是很基本的,所以让我们再看看几个活生生的例子。

比方说,我们有。

  • 一个元素的列表,为简单起见,它将是一个字符串的列表
  • 一个变量,用于存储所选择的元素,在我们的例子中是一个字符串。
    我们想把这个列表映射到React组件上,并高亮显示选中的部分。

首先。
检查一个元素是否被选中的函数,如果被选中,则添加一个css类。

  1. let selectedOrNot = (value, isSelected) =>
  2. isSelected == value
  3. ? <p className="selected">{React.string(value)}</p>
  4. : <p>{React.string(value)}</p>;

而在JSX中我们可以这样做

  1. <div>
  2. {
  3. ourList
  4. -> List.map(selectedOrNot(selectedValue))
  5. -> ReScriptReact.list;
  6. }
  7. </div>

多亏了currying,我们在List.map每次对列表元素运行回调时,都在closure中使用捕获的值。

这仍然是一个简单的例子,但我希望它能帮助我们更好地了解咖喱的作用,以及它与管道的结合是多么强大。

变体类型和模式匹配

我第一次在Rust中遇到了类似的结构,那就是枚举。一旦学会了,就很难再离开它了。变体类型在一些情况下是有帮助的。如前所述,选项和结果有助于以类型的方式处理不存在的值或错误。变量类型可能是定义自定义错误的一把好手,以后可能会被用作结果的第二个类型参数。

  1. type inputError =
  2. | BadInputType
  3. | EmptyInput
  4. | ParsingError(string);

那么结果类型可能有一个接口:result

令人费解的是,大多数Redux + TypeScript的在线教程都使用类似这样的东西来描述动作和还原器。

  1. const MY_MAGIC_STRING_TYPE = "MY_MAGIC_STRING_TYPE";
  2. cosnt exampleAction = value => ({type: MY_MAGIC_STRING_TYPE, value});
  3. const reducer = (state, action) => {
  4. switch (action.type) {
  5. case: MY_MAGIC_STRING_TYPE:
  6. ...
  7. }
  8. }

这种方法很容易出错。字符串并不是描述动作类型的最佳解决方案。

  • 用字符串很容易出错
  • 相同的字符必须输入两次
  • 有无限的字符串组合,这意味着我们不能创建一个详尽的开关语句。

这些问题可能会用TypeScript的枚举或联合体来解决。然而,这是一大堆模板代码,而且过多地关注于创建数据结构,而不是考虑我们的数据流。

使用变体类型,我们可以清楚地表达动作类型是什么以及它携带的值。

使用TypeScript

  1. interface Login {
  2. type: "Login",
  3. value: string
  4. }
  5. interface Logout {
  6. type: "Logout"
  7. }
  8. type SignIn = Login | Logout | null;

使用ReScript

  1. type login =
  2. | Login(option<string>)
  3. | Logout;

这就是我们行动的全部定义。我们想在状态中登录并记住一个用户名,然后注销。我们不需要对我们的动作进行任何包装。此外,如果我们试图在switch子句中匹配一个动作而不涵盖每一种情况,ReScript编译器会抱怨。我们有一个强类型的动作和一个帮助构造还原器的编译器。

有了这些,我们可以描述一个简单的还原器

  1. let reducer = (state, action) =>
  2. switch action {
  3. | Login(Some(name)) => {name} // assuming name is option(string) type
  4. | Login(None) => {name: No user”}
  5. | Logout => {name: No user”}
  6. }

省略 “登录 “或 “注销 “的臂膀会产生一个警告。

遗憾的是,ReScript 模式匹配在默认情况下不是详尽的。如果我们没有涵盖每一种情况,它就会抛出一个警告。这是有可能改变的,但这需要调整配置。这一改变将确保我们的还原器涵盖每一种可能的情况,使我们的应用程序更加健壮,并且更容易在未来发展。

一切都是表达式

在JavaScript和TypeScript中,有一种非常恼人的情况是将代码块的结果分配给一个变量。

比方说,我们想把if/else块的结果分配给一个变量。

在JS或TS中,我们需要做这样的事情。

  1. let value;
  2. if (condition === expectation) {
  3. value = 1
  4. } else {
  5. value = 2
  6. }

我们需要首先初始化这个变量,然后再重新赋值,这意味着我们在突变它。此外,如果我们忘记了else块,我们很快就会出现意外的行为,不得不处理未定义的值。

与此相反,这是ReScript的代码。

  1. let value = if condition === expectation {
  2. 1
  3. } else {
  4. 2
  5. }

注1:ReScript中的let声明了不可更改的变量。

注2:在ReScript中块代码会返回其最后的表达式。如果你什么都不返回,你仍然返回单元类型(),而返回类型有太多的if/if else/else臂。

在ReScript中,所有东西都是表达式,所以你可以分配如果块切换块,甚至只是任何块。比如说:

  1. let value = {
  2. 2 * 2
  3. }

不可变的数据

不可变的数据是函数式编程中最关键的概念之一。

在命令式编程中,它可能听起来像一个骗局(你必须以某种方式从if/else块中分配一个值,对吗)。从单个代码块的角度来看,它可能看起来很有局限性,但应用程序通常是由许多块代码组成的。

我们通常采取我们的数据并将其传递给几个函数和方法。

在JavaScript和TypeScript中,你可以定义一个对象,把它从一个函数传到另一个函数,并在每一步中修改它的一部分。如果出了问题,祝你调试代码,深入到执行的函数中去。

不可变的数据更可靠,更容易测试。使用不可变的数据更容易保证一个函数在相同的输入下总是返回相同的输出。

此外,在JS和TS中,你可以用let、const和var来声明变量,这也让人困惑。

在ReScript中,你只需使用let并默认声明不可变的值(数组除外)。你也可以在记录中定义可变字段,但你必须明确地这样做。

在大多数情况下,你应该通过返回一个带有变化数据的新值,从旧值中创建一个新值。

#6 模块高于对象

这可能是ReScript与JavaScript/TypeScript世界之间最明显的区别之一。在JavaScript和TypeScript中,围绕对象和继承链建立方法是一种典型的模式。虽然对象和方法是可以接受的,但继承很容易导致一个长的、难以调试的类/对象链。

在ReScript中,没有直接等同于对象、对象方法和继承的内容。相反,所有的功能都应该围绕一个模块来构建。

例如,Array 模块提供了与 Array 数据结构有关的所有功能。这使得如何构建应用程序以及所需代码的位置变得很清楚。

对于来自Haskell或Elm等语言的开发者来说,这种方法可能很熟悉,而对于Java或TypeScript的开发者来说,则会感到困惑。

ReScript中的硬核知识:

1 代码格式化

这是一个可能很快就会打击到你的东西,特别是如果你花时间配置你的linters和prettier来获得你的组件代码的一个体面的布局。或者如果你已经习惯了像Elm或Rust这样具有强大格式化功能的语言。

简而言之,ReScript格式器是不可配置的。它强制执行代码风格,包括行长。这意味着当你的管道或组件中的道具列表太短时,你的代码将被强制为单行。

这不仅令人厌烦,而且在很多情况下,会使代码更难读。

2 这不是ReasonML

如果你熟悉ReasonML,ReScript对你来说可能是一个不愉快的惊喜。虽然所有的工具加在一起可能是好的(ReScript + BuckleScript),但也有一些打击。一些语法不再有效(比如模式匹配单数函数的趣味速记),它被废弃了,比如三角管|>。ReScript也不再与Ocaml保持完全同步,变得有点像它自己的语言。这对新人来说可能不是什么大事,但它也得到了OCaml开发者的一些关注。它也可能与围绕ReasonML项目的工具不完全兼容(例如,用Melange文章构建Ahrefs代码库的作者就有这样的情况。

最糟糕的情况可能是 “重塑 “为ReScript之后的情况。不是每个人都对它感到兴奋,而且由于不确定BuckleScript是否还会被开发,看来ReasonML并没有完全死亡。还有一个专注于OCaml兼容性的ReScript分叉,一些开发者回避了JS_of_ocaml。

这造成了巨大的混乱,而且由于整个品牌重塑的过程太长,可能会让开发者不确定哪个项目会成为主导(即使ReScript目前得到了最多的关注和努力)。

3 没有Redux的替代品

对于ReasonML,有一个Reductive项目。但它仍然没有和Redux一样的体验。一个强大的静态类型系统使得开发广泛的应用程序更加困难。有了JS,在组合多个还原器时就会有很多舒适感。有了ReScript和它缺乏的库,它需要编写大量的抽象,以舒适地与monorepo和几个微型应用程序一起工作,这些应用程序既要单独工作,又要全部在一起。

4 作为一个依赖JavaScript库的概念证明,不是一个好的选择。

与ReasonML相比,ReScript对JS绑定的语法进行了改进。前端开发的主要工具是React(用ReScriptReact的React绑定),大多数库对它有很好的支持。考虑到这些,为复杂的库API编写绑定仍然可能需要时间。

例如,为Recharts库中的一些类型和React组件编写绑定可能很容易,但为d3.JS包装绑定可能很艰巨。

总结

在使用无类型的 JavaScript 更新 API 或修复团队中的初级开发人员引入的任何类型的代码之后,使用 ReScript 可能是一种全新的体验。它也可能是朝着为前端提供更多功能性代码迈出的良好一步。

不幸的是,ReScript仍然是一项年轻的技术,像缺乏库/绑定或格式化器的问题可能是显著的缺点。另一方面,在经历了一个艰难的开始(重塑品牌)之后,ReScript正在变得越来越好。在过去的半年中,ReScript 文档得到了极大的改善,成为新人的坚实基础。

那么,您是否应该为您的下一个项目选择 ReScript?我认为这真的取决于您的项目需求。

在选择 ReScript 时,我的快速经验法则是

何时不选择 ReScript

它可能不是最好的选择,如果你。

  • 希望获得纯粹的函数式体验,并且不依赖 JavaScript 库,我建议寻找 Elm。
  • 主要依赖面向对象的编程。你应该选择TypeScript。
  • 来自于OCaml生态系统,并与之严格耦合。
  • 需要围绕具有复杂API的现有库建立JavaScript解决方案的原型。

何时选择ReScript

它可能是一个不错的选择,如果你。

  • 希望获得功能性体验,并强烈依赖 JavaScript 库。
  • 想坚持使用庞大的React生态系统和广泛使用的工具,如Webpack或CRA,并拥有比TypeScript更好的类型保证和更多的功能代码。
  • 使用React Native
  • 已经来自OCaml生态系统,但与之松散耦合。ReScript可能是一个比JS_of_ocaml更好的选择,因为它专注于典型的前端开发,与后端流程和工具不同。