文档 / 语言手册 / 从 JS 转换到 ReScript
Edit

从 JS 转换到 ReScript

ReScript 提供了一种独特的项目转换方式,这种转换方式:

  • 确保对你的团队成员造成最小的干扰(非常重要!)。

  • 消除了典型的验证转换正确性与保证性能的分歧。

  • 不强制你寻找别人编写的预制绑定库。ReScript 不需要等价于 TypeScript 的 DefinitelyTyped 东西

步骤 1:安装 ReScript

在你的项目中运行 npm install rescript --save-dev,然后像我们在新项目中的工作流程一样,将 bsconfig.json 添加到项目根目录。接着执行 npx rescript build -w

步骤 2:复制粘贴整个 JS 文件

让我们转换 src/main.js 这个文件。

JS
const school = require('school'); const defaultId = 10; function queryResult(usePayload, payload) { if (usePayload) { return payload.student; } else { return school.getStudentById(defaultId); } }

首先,使用 %%raw JS 嵌入技巧,将整个文件的内容复制到新文件 src/Main.res 中:

ReScriptJS Output
%%raw(`
const school = require('school');

const defaultId = 10;

function queryResult(usePayload, payload) {
  if (usePayload) {
    return payload.student;
  } else {
    return school.getStudentById(defaultId);
  }
}
`)

将下面的内容添加到 bsconfig.json

JSON
"sources": { "dir" : "src", "subdirs" : true },

在编辑器中为 src/Main.bs.js 打开一个选项卡。执行命令 diff -u src/main.js src/Main.bs.js。除了空格以外,你应该只能看到一些极小的、微不足道的差别。你已经完成了三分之一的工作!

始终确保在每个步骤中保持打开 ReScript 输出的 .bs.js 文件,以便与现有的 JavaScript 文件进行比较。我们的编译输出与你手写的 JavaScript 非常接近;你可以通过简单地观察差异来发现转换 bug!

步骤 3:将部分内容提取到 ReScript 中

让我们将变量 defaultId 变成 ReScript 的 let 绑定:

ReScriptJS Output
let defaultId = 10

%%raw(`
const school = require('school');

function queryResult(usePayload, payload) {
  if (usePayload) {
    return payload.student;
  } else {
    return school.getStudentById(defaultId);
  }
}
`)

检查输出。比较差异。代码仍然正确。那就继续前进!提取函数:

ReScriptJS Output
%%raw(`
const school = require('school');
`)

let defaultId = 10

let queryResult = (usePayload, payload) => {
  if usePayload {
    payload.student
  } else {
    school.getStudentById(defaultId)
  }
}

格式化代码:./node_modules/.bin/rescript format src/Main.res

我们得到了一个类型错误:“The record field student can't be found”。没关系!始终优先确保你代码的语法是有效的。稍后再修复类型错误。

步骤 4:添加 external,修复类型

前面的类型错误是因为没有找到 payload 的记录声明(它应该包含 student 字段)造成的。因为我们想尽快地完成转换,让我们使用对象特性来避免类型体操:

ReScriptJS Output
%%raw(`
const school = require('school');
`)

let defaultId = 10

let queryResult = (usePayload, payload) => {
  if usePayload {
    payload["student"]
  } else {
    school["getStudentById"](defaultId)
  }
}

现在这触发了下一个类型错误,即找不到 school。让我们使用 external 来绑定这个模块:

ReScriptJS Output
@module external school: 'whatever = "school"

let defaultId = 10

let queryResult = (usePayload, payload) => {
  if usePayload {
    payload["student"]
  } else {
    school["getStudentById"](defaultId)
  }
}

我们匆忙地将 school 的类型变成了一个多态的 'whatever,并通过下面的用法推断其类型。这个推断在技术上是正确的,但因为 school 的值来自 JavaScript,这有些危险。这只是我们在 external 页面中展示的互操作技巧。

总之,这个文件再次通过了类型检查器。检查 .bs.js 的输出,和原始的 .js 文件进行比较;我们现在将文件转换为了 ReScript!

现在,你可以删除原始的、手写的 main.js 文件,然后 grep 导入了 main.js 的文件,将它们修改为导入 Main.bs.js

(可选)步骤 5:清理

如果你喜欢更高级、类型更严格的 payloadschool,可以这样做:

ReScriptJS Output
type school
type student
type payload = {
  student: student
}

@module external school: school = "school"
@send external getStudentById: (school, int) => student = "getStudentById"

let defaultId = 10

let queryResult = (usePayload, payload) => {
  if usePayload {
    payload.student
  } else {
    school->getStudentById(defaultId)
  }
}

我们已经:

  • schoolstudent 引入了一种不透明的类型,以防止误用它们的值

  • 将 payload 声明为一个仅有 student 字段的记录

  • getStudentById 声明为 student 的唯一方法

检查 .bs.js 的输出是否有变化。JavaScript 代码的类型严格程度取决于你;我们建议不要将它们的类型写的太详细;有时这就像是一个无底洞,产生的回报也会越来越少,特别是要考虑到过于精细设计可能会让你的一些潜在的队友不爽。

技巧和诀窍

本着同样的想法,抑制为要转换的 JS 代码编写包装函数的冲动。使用 external,它可以保证不会出现在输出中。并且避免尝试将 JS 的数据结构转换为 ReScript 特定的数据结构,像是变体(variant)或列表(list)。现在不是做这个的时候

一旦你在输出中产生了额外的转换代码,你那充满怀疑的队友的心智模型可能会从“我认识这个输出”变为“这个转换引入的问题可能比解决的问题多,我们为什么要再测试一遍 ReScript 代码?”。那么你就输了。

总结

  • 将 JS 代码作为嵌入的原生 JS 代码粘贴到一个新的 ReScript 文件中。

  • 编译并保持输出文件打开。检查代码并与原始 JS 文件进行比较。这是免费的回归测试。

  • 始终确保你的文件在语法上是有效的。在此之前不要担心修复类型的问题。

  • (滥)用对象访问来快速地完成转换。

  • 整理类型(可选)以提高稳健性。

  • 不要做的太过火而使你的老板与队友反感。

  • 给你的队友展示异常熟悉的输出,自豪地表明你在转换中保留了语义与性能特征。

  • 以更安全、更成熟的方式引入一项新技术,从而获得晋升。