使用 ReScript 可以实现更友好的 API。
拥抱外部
在 JavaScript 的世界里,许多 API 的设计方式与函数式开发者的设计方式截然不同。尽管近年来得益于 React 等技术的出现,情况有所改善,但大多数浏览器 API 仍然偏向面向对象或底层设计。
ReScript(前身为 BuckleScript)是一个静态类型且可靠的语言、编译器和构建系统,能够生成可读性极高的 JavaScript 代码。因此,对于那些无法忍受类型漏洞的人来说,它是 TypeScript 的绝佳替代方案。
ReScript提供了各种各样的装饰器,使与 JavaScript API 的绑定更加容易。
最近,我需要使用浏览器的mediaDevicesAPI。其中一个最重要的方法是getUserMedia返回一个包含MediaStream对象的 Promise 对象。它还有一个必需参数,constraints即一个对象,用于确定 API 用户请求的是音频、视频还是两者都请求。例如,如果只想请求音频流,可以使用以下 JS 代码:
navigator.mediaDevices.getUserMedia({audio: true, video: false})
仅当需要视频时才进行设置audio: false, video: true,并且只有当两者都需要时才进行设置audio: true, video: true。
在惯用的ReScript中,这样的 API 可能会采用以下几种设计方式之一:
- 可以通过以下
constraints方式:
type constraints = Audio | Video | Both
let getUserMedia: constraints => Js.Promise.t<stream> = ...
/* Calling the function */
getUserMedia(Audio)
getUserMedia(Video)
getUserMedia(Both)
- 或者三个独立的功能
let getUserAudio: unit => Js.Promise.t<stream> = ...
let getUserVideo: unit => Js.Promise.t<stream> = ...
let getUserMedia: unit => Js.Promise.t<stream> = ...
/* Calling the function */
getUserAudio()
getUserVideo()
getUserMedia()
在这种情况下,我更倾向于后一种选择。很快你就会明白为什么了。
拥抱外部
绑定到现有 JS 函数的默认方法是使用 external 关键字,并配合一系列不同的装饰器/注解,这些装饰器/注解总是以@.
注意:使用最新版本的 ReScript 平台(bs-platform 8.3),可以省略
bs注解中的部分内容@bs.as。例如,将仅显示 `<script>`@as、`<script>` 等。请检查您的版本是否足够新,以便编写更简洁的代码。
绑定尝试 1
要绑定到全局值(例如navigator.mediaDevices.getUserMedia),需要两个注解。我们先来看看如何编写绑定,稍后再将其拆解分析。
/* Navigator.MediaDevices module */
type constraints = {
audio: bool,
video: bool,
}
@bs.val @bs.scope(("navigator", "mediaDevices"))
external _getUserMedia: constraints => Js.Promise.t<stream> =
"getUserMedia"
-
type constraints = ...第一部分是对
我们需要传递给函数的约束参数的类型定义getUserMedia。它是一个所谓的记录,看起来像一个对象,也能编译成相同形状的JS对象,但实际上是不同的东西。 -
@bs.val绑定到全局值。全局值是指始终在作用域内的值,例如 `int`navigator或 `int`window。当然,这取决于目标平台(浏览器、Node 等)。 -
@bs.scope(("navigator", "mediaDevices"))当这些值嵌套时,您还需要使用 `.` 定义其作用域@bs.scope。这里,我们使用字符串元组来告诉编译器作用域是多层嵌套的。您可以通过双括号 `.`((和 ` .` 来判断这是一个元组))。外层括号来自作用域函数,内层括号来自元组。或者,您也可以像这样直接使用字符串:@bs.scope("navigator.mediaDevices")`.` -
external _getUserMedia上述注解只能与external关键字一起使用。通过关键字,您可以定义绑定的名称,该名称不一定需要与您要绑定的 JavaScript 方法的名称相同。 -
: constraints => Js.Promise.t<stream>接下来是类型注解。根据 MDN 的说明,它会接收前面提到的约束对象,并返回一个 MediaStream 类型的 Promise,该 Promise 会转换为内置的ReScript类型Js.Promise.t。<stream>遗憾的是,Promise 包装的类型并非内置类型,但可以相应地进行类型声明(留给读者作为练习 😉)。 -
= "getUserMedia"最后,这里显示的是函数的实际名称。注意拼写错误!
这里我们还定义了一个辅助记录。它看起来像一个对象,编译后也会生成一个JS对象,但实际上是不同的。
现在我们有了绑定getUserMedia,但是我们真的想Navigator.MediaDevices._getUserMedia({audio: true, video: true}在代码库的每个地方都调用这个丑陋的方法吗?还是我们可以实现一个更友好的 API?
让我们编写一些辅助函数:
/* Navigator.MediaDevices module continued */
let getUserAudio = () => _getUserMedia({audio: true, video: false})
let getUserVideo = () => _getUserMedia({audio: false, video: true})
let getUserMedia = () => _getUserMedia({audio: true, video: true})
然后可以通过以下方式从代码库中的任何位置调用这些函数:
Navigator.MediaDevices.getUserAudio()
Navigator.MediaDevices.getUserVideo()
Navigator.MediaDevices.getUserMedia()
注意:这是一个简化的示例,在实际应用中,您可以使用
Js.Promise.then_类似方法来检索流本身。
太好了,看起来好多了。但现在我们需要生成一些额外的代码,
例如这个函数:
function getUserAudio(param) {
return navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
}
绑定尝试 2
我们能否做得更好,同时仍然使用默认参数填充函数?我认为可以,使用这个巧妙的技巧就行了。
@bs.as此外,当需要为实体指定一个复杂的名称(而这个名称通常无法直接创建)时,注解也非常有用。例如,在记录字段中,通常-不允许使用某些特定名称,而此注解可以解决这个问题。
@bs.as它接受一个字符串作为参数。巧合的是,JSON 本质上就是一个复杂的字符串,因此我们也可以将一些复杂的对象注入到默认配置中。
只需写
@bs.as(json`{your-config-object}`)
后面加上一个省略号_,以便在调用函数时省略该值。
此外,我们还添加了一个最终unit参数(该函数的唯一“参数”)。
/* Navigator.MediaDevices module */
@bs.val @bs.scope(("navigator", "mediaDevices"))
external getUserAudio: (
@bs.as(json`{"audio": false, "video": true}`) _,
unit,
) => Js.Promise.t<stream> = "getUserMedia"
不过,我称之为“ as-JSON技巧”的方法也有其局限性。它仅适用于符合JSON规范的值,例如数组、对象、字符串、数字和布尔值。例如,函数就无法使用。
我们可以对另外两个可能的函数进行同样的操作:
/* Navigator.MediaDevices module continued */
@bs.val @bs.scope(("navigator", "mediaDevices"))
external getUserVideo: (
@bs.as(json`{"audio": true, "video": false}`) _,
unit,
) => Js.Promise.t<stream> = "getUserMedia"
@bs.val @bs.scope(("navigator", "mediaDevices"))
external getUserMedia: (
@bs.as(json`{"audio": true, "video": true}`) _,
unit,
) => Js.Promise.t<stream> = "getUserMedia"
最终得到的 API 与第一次尝试相同。同样,我们可以像以前一样调用函数:
Navigator.MediaDevices.getUserAudio()
Navigator.MediaDevices.getUserVideo()
Navigator.MediaDevices.getUserMedia()
但这一次没有构建产物,第二次尝试的成本为零🎉。
经验法则:lets 会生成额外的 JS 代码,而externals 则不会。
感谢阅读,希望这个技巧对大家有所帮助。如需更简洁的示例,请查看本文所基于的ReScript Playground 示例。
文章来源:https://dev.to/fhammerschmidt/nicer-apis-with-rescript-361