JSX

想要在 ReScript 中使用一些 HTML 语法吗?如果不需要,请迅速跳过本节,假装什么也没看到!

ReScript 支持 JSX 语法,与 ReactJS 中的语法相比,有一些细微的差别。ReScript 的 JSX 并不与 ReactJS 绑定,它们会转化为正常的函数调用:

使用 ReasonReact 的读者请注意:这不是 ReasonReact 的 JSX 的转换结果,更多信息参见用法部分。

大写标签

ReScriptJS Output
<MyComponent name={"ReScript"} />

会变成

ReScriptJS Output
@JSX MyComponent.createElement(~name="ReScript", ~children=list{}, ())

非大写标签

ReScriptJS Output
<div onClick={handler}> child1 child2 </div>

会变成

ReScriptJS Output
@JSX div(~onClick=handler, ~children=list{child1, child2}, ())

Fragment

ReScriptJS Output
<> child1 child2 </>

会变成

ReScriptJS Output
@JSX list{child1, child2}

子节点

ReScriptJS Output
<MyComponent> child1 child2 </MyComponent>

这是传递拥有两个元素的列表到子节点位置的语法。去除语法糖后,它会被转换为一个包含 child1child2 的列表:

ReScriptJS Output
@JSX MyComponent.createElement(~children=list{child1, child2}, ())

再次注意,这不是 ReasonReact 做的转换;ReasonReact 将最终的列表变成一个数组,但是这样的思路没问题。

所以很自然地,<MyComponent> myChild </MyComponent> 被转换为 @JSX MyComponent.createElement(~children=list{myChild}, ())。也就是说,无论你做什么,传递到子节点位置的参数都会被包裹在一个列表中。如果你不想这样呢?如果你想直接传递 myChild,而不进行额外的包装呢?

子节点展开

为了解决上面的问题,我们引入了

ReScriptJS Output
<MyComponent> ...myChild </MyComponent>

这将传递 myChild 的值,而不把它包装到一个列表中(在 ReasonReact 的情况下是数组),也就是转换为:

ReScriptJS Output
@JSX MyComponent.createElement(~children=myChild, ())

这在 myChild 已经是一个列表的情况下是非常有用的,你可以直接转发而不需要额外包装它(这将是一个类型错误)*。它还允许你在 children 位置传递任意的数据结构(记住,JSX 的 children 实际上只是一个常规的 prop)。

ReScriptJS Output
<MyComponent> ...((theClassName) => <div className=theClassName />) </MyComponent>

<MyForm> ...("Hello", "Submit") </MyForm>

用法

参见 ReasonReact JSX 了解 JSX 的应用实例,它将上面的调用转化为 ReasonReact 的特定调用。

这是一个展示了大部分特性的 JSX 标签。

ReScriptJS Output
<MyComponent
  booleanAttribute={true}
  stringAttribute="string"
  intAttribute=1
  forcedOptional=?{Some("hello")}
  onClick={handleClick}>
  <div> {React.string("hello")} </div>
</MyComponent>

与 JS JSX 的不同之处

  • 属性和子节点并没有强制要求 {},但为了便于学习,我们还是写出来了。一旦你格式化了你的文件,它们中的一些就会消失,一些变成了括号

  • 没有对 JSX prop 展开的支持,比如 <Comp {...props}}. />。但我们有子节点展开:<Comp> ...children </Comp>

  • 双关(punning)!

双关

“双关”是指标签和值的名称相同时的语法简写。例如,在 JavaScript 中,不用写 return {name: name},可以用 return {name}

Reason JSX 支持双关,<input checked /> 只是 <input checked=checked /> 的简写。格式化工具将帮助你尽可能地格式化到双关语法,这在有很多 prop 需要传递的情况下非常实用:

ReScriptJS Output
<MyComponent isLoading text onClick />

这样一来,Reason JSX 组件可以在需要额外的库来避免传递 prop 之前,塞入更多的 prop。

注意这和 ReactJS JSX 不同,ReactJS JSX 没有双关。它的 <input checked /> 会转换为 <input checked=true />,以便符合 DOM 的习惯用法,同时也为了向后兼容。

技巧和诀窍

对于想要利用 JSX 的库作者来说:上面的 @JSX 属性是潜在的 ppx 宏的一个钩子,可以定位想要格式化为 JSX 的函数。一旦你定位了这个函数,你可以把它变成任何其他表达式。

这样,每个人都能从 JSX 语法中受益,而不需要使用特定的库,例如 ReasonReact。

JSX 调用也同样支持标签参数特性:可选参数,显式传递的可选参数和默认的可选参数。

设计决策

* 你可能会想,为什么在 ReactJS 中从来不需要这样的子节点展开呢;ReactJS 使用了一些特殊的运行时逻辑 + 特殊的语法转换 + 变量参数检测和标记,来避免大多数的这些情况(看这里)。这样的动态用例使类型系统的检查变得相当复杂。由于我们控制了整个语法和 ReasonReact,我们决定引入子节点展开来区分包装和不包装的情况,这样就没有编译时和运行时开销了!