ReasonML 可以用于生产级 React 应用吗?🤔(第一部分)我们要构建什么?设置项目和编辑器 迭代 #1:有一个可以输入文字的输入框

2025-06-10

ReasonML 可以用于生产环境的 React 应用吗?🤔(第一部分)

我们要建造什么?

设置项目和编辑器

迭代#1:有一个可以输入文本的输入框

ReasonML 是一种函数式编程语言,具有智能推断的严格类型,可编译为 JavaScript。ReasonReact 是 ReactJS 的 Reason 绑定(也就是 ReactJS 的 ReasonML 版本)。它最近有了很大的改进,甚至在几天前的版本中增加了对 hooks 的支持。

在本系列文章中,我将使用 ReasonReact 构建应用程序,并尝试完成我通常使用 ReactJS 完成的大部分任务。在每篇文章中,我都会分享我在 Reason 中构建 React 应用程序的优缺点。目标是确定 ReasonML 在构建严肃的 React 应用程序方面是否已做好准备。

我们要建造什么?

我决定从一个简单的应用程序开始。我们将构建一个具有以下功能的小型字数统计器:

  • 有一个输入框,我可以在其中输入文字。
  • 当我写文本时,字数统计会更新。
  • 有一个按钮可以清除文本。
  • 有一个按钮可以复制文本。

最终结果


最终结果

您可以在此处找到最终的源代码。由于我们将以迭代的方式构建应用程序,因此每次迭代都有一个分支。

设置项目和编辑器

首先,让我们下载 Reason 到 JavaScript 编译器bs-platform (BuckleScript):

npm install -g bs-platform
Enter fullscreen mode Exit fullscreen mode

该软件包附带bsb,这是一个 CLI 工具,可基于模板快速启动 Reason 项目。
让我们基于 react-hooks 模板生成我们的项目:

bsb -init words-counter -theme react-hooks
Enter fullscreen mode Exit fullscreen mode

我们也使用 VSCode 作为代码编辑器,并下载reason-vscode 。这是ReasonML官方推荐的编辑器插件

为了利用格式化功能,让我们在编辑器的设置中启用“保存时格式化”选项:

在 VSCode 上启用“保存时格式化”选项


在 VSCode 上启用“保存时格式化”选项

我喜欢👍

  • 入门体验非常好。BuckleScript 构建工具 (bsb) 比create-react-appyeoman快得多。

  • 编辑器工具也很棒:

    • 它格式化代码样式和语法(就像使用 Prettier 配置 ESLint 一样)。
    • 当鼠标悬停在值上时,它还会提供有关类型的信息。

迭代#1:有一个可以输入文本的输入框

在第一次迭代中,我们只想要一个带有标题的漂亮文本区域来写入文本并将其存储在状态变量中:

迭代#1:有一个可以输入文本的输入框


迭代#1:有一个可以输入文本的输入框

/* src/App.re */

[%bs.raw {|require('./App.css')|}];

[@react.component]
let make = () => {
  let (text, setText) = React.useState(() => "");

  let handleTextChange = e => ReactEvent.Form.target(e)##value |> setText;

  <div className="App">
    <div className="header">
      <h3> {"Words Counter" |> ReasonReact.string} </h3>
    </div>
    <textarea
      placeholder="Express yourself..."
      value=text
      onChange=handleTextChange
    />
  </div>;
};
Enter fullscreen mode Exit fullscreen mode

我不喜欢👎

  • 访问表单事件的目标值需要一点开销。
  • 即使组合运算符有一点帮助,但必须使用ReasonReact.string每个值也需要一些时间来适应。string|>
  • useState需要一个函数。虽然这在进行昂贵的初始状态计算时很有用,但大多数情况下是不必要的。我更喜欢这个钩子的两种形式(一种接受值,一种接受函数),并使用不同的名称。

我喜欢👍

  • 使用 CSS 创建一个简单应用非常容易。虽然引用 CSS 文件的语法有点奇怪,但整体体验仍然很棒。

  • DOM 元素是完全类型化的,这有两个好处:

    • 你可以在运行前知道是否为 prop 赋了错误的值:告别拼写错误!这就像为所有 DOM 元素的属性内置了 propTypes 一样。
    • DOM 元素是自文档化的。您可以将鼠标悬停在某个元素上,立即查看它所接受的属性(无需再 Google 搜索)。

迭代#2:当我写文本时,字数会更新

在这次迭代中,我们想要显示到目前为止输入的单词数:

迭代#2:当我写文本时,字数会更新


迭代#2:当我写文本时,字数会更新

首先,让我们创建一个返回字符串输入中的单词数的函数:

let countWordsInString = text => {
  let spacesRegex = Js.Re.fromString("\s+");

  switch (text) {
  | "" => 0
  | noneEmptyText =>
    noneEmptyText
    |> Js.String.trim
    |> Js.String.splitByRe(spacesRegex)
    |> Js.Array.length
  };
};
Enter fullscreen mode Exit fullscreen mode

该函数的作用如下:

  • 如果文本为空,我们只返回 0。
  • 否则,我们只需修剪文本并使用Js.String.splitByRe正则表达式\s+(基本上意味着 1 个或多个空格后跟任意字符)来拆分它,并返回我们获得的数组的长度。
/* src/App.re */

[%bs.raw {|require('./App.css')|}];

let countWordsInString = text => {
  let spacesRegex = Js.Re.fromString("\s+");

  switch (text) {
  | "" => 0
  | noneEmptyText =>
    noneEmptyText
    |> Js.String.trim
    |> Js.String.splitByRe(spacesRegex)
    |> Js.Array.length
  };
};

[@react.component]
let make = () => {
  let (text, setText) = React.useState(() => "");

  let handleTextChange = e => ReactEvent.Form.target(e)##value |> setText;

  let wordsCountText =
    (text |> countWordsInString |> string_of_int) ++ " words";

  <div className="App">
    <div className="header">
      <h3> {"Words Counter" |> ReasonReact.string} </h3>
      <span> {ReasonReact.string(wordsCountText)} </span>
    </div>
    <textarea
      placeholder="Express yourself..."
      value=text
      onChange=handleTextChange
    />
  </div>;
};
Enter fullscreen mode Exit fullscreen mode

我喜欢👍

  • Reason 的智能推理非常棒:
    • 虽然我没有提供任何类型注释,但该countWordsInString函数是自文档化的。将鼠标悬停在其上,可以看到它接受一个string并返回一个int
    • 有时候,我返回的是拆分后的数组,countWordsInString而不是它的长度。我甚至在浏览器中查看应用程序之前,就能够在构建时捕获这个错误。

迭代#3:有一个按钮可以清除文本

在这次迭代中,我们希望有一个按钮来清除文本:

迭代#3:有一个按钮可以清除文本让我们添加一个按钮来清除文本区域中的文本。


迭代#3:有一个按钮可以清除文本让我们添加一个按钮来清除文本区域中的文本。

在 JavaScript 中,我使用svgr Webpack 加载器直接从相应文件导入 SVG 图标作为 React 组件.svg

由于导入是在 Reason 中输入的,我决定在清除按钮中添加一个图标,以查看将 SVG 图标导入为 React 组件有多痛苦。

由于在下一次迭代中我们将有另一个看起来不同的按钮(剧透警报),因此让我们将按钮作为一个单独的组件,并使其具有两个用于样式目的的类别:

  • 主要:蓝色按钮
  • 次要:灰色按钮
/* src/Button.re */

[%bs.raw {|require('./Button.css')|}];

type categoryT =
  | SECONDARY
  | PRIMARY;

let classNameOfCategory = category =>
  "Button "
  ++ (
    switch (category) {
    | SECONDARY => "secondary"
    | PRIMARY => "primary"
    }
  );

[@react.component]
let make =
    (
      ~onClick,
      ~title: string,
      ~children: ReasonReact.reactElement,
      ~disabled=false,
      ~category=SECONDARY,
    ) => {
  <button onClick className={category |> classNameOfCategory} title disabled>
    children
  </button>;
};
Enter fullscreen mode Exit fullscreen mode

要使用 svgr,我们在 Webpackmodule配置中添加以下规则:

{
  test: /\.svg$/,
  use: ['@svgr/webpack'],
}
Enter fullscreen mode Exit fullscreen mode

在 JavaScript 中,我们可以通过执行以下操作来导入 svg 组件:

import {ReactComponent as Times} from './times';
Enter fullscreen mode Exit fullscreen mode

由于 Webpack 将 svgr 应用于编译 Reason 源代码所产生的 JavaScript,我们只需要让 BuckleScript 将 Reason 导入转换为命名的 es6 导入。

为此,我们首先必须配置/bs-config.json(BuckleScript 编译器的配置文件)以使用 es6 导入:

  "package-specs": [
    {
      "module": "es6",
      "in-source": true
    }
  ],
Enter fullscreen mode Exit fullscreen mode

ReasonReactmake函数会编译为 JavaScript React 组件!这意味着,如果我们想使用用 JavaScript 编写的组件“Foo”,只需:
1- 在 Reason 中创建该组件。2-
将 JS 组件作为makeReason 组件的函数导入,并为其 props 添加注解。

因此在模块中Foo.re,我们将有以下内容:

[@bs.module "./path/to/Foo.js"][@react.component]
external make: (~someProp: string, ~someOtherProp: int) => React.element = "default";
Enter fullscreen mode Exit fullscreen mode

这意味着……我们可以用 svgr 导入一个 SVG 组件!
我们用它来导入./times.svg图标,并注释一下heightprop,因为它是我们唯一会用到的:

[@bs.module "./times.svg"] [@react.component]
external make: (~height: string) => React.element = "default";
Enter fullscreen mode Exit fullscreen mode

我们的 ReasonReact 组件被自动视为模块,因为我们将它们分别创建在单独的文件(Button.re 和 App.re)中。由于 Times 组件非常小(仅两行),我们可以使用 Reason 的模块语法来创建它:

/* src/App.re */

[%bs.raw {|require('./App.css')|}];

let countWordsInString = text => {
  let spacesRegex = Js.Re.fromString("\s+");

  switch (text) {
  | "" => 0
  | noneEmptyText =>
    noneEmptyText
    |> Js.String.trim
    |> Js.String.splitByRe(spacesRegex)
    |> Js.Array.length
  };
};

module Times = {
  [@bs.module "./times.svg"] [@react.component]
  external make: (~height: string) => React.element = "default";
};

[@react.component]
let make = () => {
  let (text, setText) = React.useState(() => "");

  let handleTextChange = e => ReactEvent.Form.target(e)##value |> setText;

  let handleClearClick = _ => setText(_ => "");

  let wordsCountText =
    (text |> countWordsInString |> string_of_int) ++ " words";

  <div className="App">
    <div className="header">
      <h3> {"Words Counter" |> ReasonReact.string} </h3>
      <span> {ReasonReact.string(wordsCountText)} </span>
    </div>
    <textarea
      placeholder="Express yourself..."
      value=text
      onChange=handleTextChange
    />
    <div className="footer">
      <Button
        title="Clear text"
        onClick=handleClearClick
        disabled={String.length(text) === 0}>
        <Times height="20px" />
      </Button>
    </div>
  </div>;
};
Enter fullscreen mode Exit fullscreen mode

我不喜欢👎

如果我想创建一个可复用的按钮,并且它应该接受原生 DOM 按钮所具备的所有属性,那么我必须列出所有这些属性。在 JavaScript 中,我只需使用展开操作就可以避免这种情况:

function Button(props) {
    return <button {...props} />
}
Enter fullscreen mode Exit fullscreen mode

但是 ReasonReact 不允许使用扩展运算符。(我想知道是否有办法用 ReasonReact 实现我想要的功能🤔)

我喜欢👍

  • 指定子组件类型的功能非常强大。JavaScript 中的 PropTypes 可以实现这一点,但与 Reason 相比功能非常有限。例如,我们可以指定组件仅接受 2 个子组件(以元组的形式)。
  • 变体对于按钮的分类非常有用。组件分类是经常发生的事情,因此能够使用真正可靠的类型而不是字符串常量来实现这一点是一个巨大的优势。
  • 使用 Webpack 的 svgr 插件导入 SVG 作为组件其实非常轻松。它非常简单,而且由于我们只需要注释类型,因此可以确保类型安全。

迭代#4:有一个复制文本的按钮

在此迭代中,我们希望有一个按钮将文本复制到剪贴板:

迭代#4:有一个复制文本的按钮


迭代#4:有一个复制文本的按钮

为此,我想使用react-copy-to-clipboard,这是一个 React 组件库,可以非常轻松地将文本复制到剪贴板。由于它是一个 JavaScript 库,我们可以使用与上一次迭代相同的导入方法。唯一的区别是,我们将进行命名导入,而不是默认导入。

/* src/App.re */

[%bs.raw {|require('./App.css')|}];

let countWordsInString = text => {
  let spacesRegex = Js.Re.fromString("\s+");

  switch (text) {
  | "" => 0
  | noneEmptyText =>
    noneEmptyText
    |> Js.String.trim
    |> Js.String.splitByRe(spacesRegex)
    |> Js.Array.length
  };
};

module Times = {
  [@bs.module "./icons/times.svg"] [@react.component]
  external make: (~height: string) => React.element = "default";
};

module Copy = {
  [@bs.module "./icons/copy.svg"] [@react.component]
  external make: (~height: string) => React.element = "default";
};

module CopyClipboard = {
  [@bs.module "react-copy-to-clipboard"] [@react.component]
  external make: (~text: string, ~children: React.element) => React.element =
    "CopyToClipboard";
};

[@react.component]
let make = () => {
  let (text, setText) = React.useState(() => "");

  let handleTextChange = e => ReactEvent.Form.target(e)##value |> setText;

  let handleClearClick = _ => setText(_ => "");

  let wordsCountText =
    (text |> countWordsInString |> string_of_int) ++ " words";

  <div className="App">
    <div className="header">
      <h3> {"Words Counter" |> ReasonReact.string} </h3>
      <span> {ReasonReact.string(wordsCountText)} </span>
    </div>
    <textarea
      placeholder="Express yourself..."
      value=text
      onChange=handleTextChange
    />
    <div className="footer">
      <Button
        title="Clear text"
        onClick=handleClearClick
        disabled={String.length(text) === 0}>
        <Times height="20px" />
      </Button>
      <CopyClipboard text>
        <Button
          title="Copy text"
          disabled={String.length(text) === 0}
          category=Button.PRIMARY>
          <Copy height="20px" />
        </Button>
      </CopyClipboard>
    </div>
  </div>;
};
Enter fullscreen mode Exit fullscreen mode

我喜欢👍

导入 JavaScript React 组件库也非常简单,并确保类型安全。

鏂囩珷鏉ユ簮锛�https://dev.to/seif_ghezala/reasonml-for-production-react-apps-part-1-3nfk
PREV
从 Java 初学者到专业 Java 开发人员的 7 个第一步
NEXT
3 个易于应用的 CSS 改进,您现在就可以在项目中使用