從 React 到 Reason

ReasonReact

若是你是一個 React 愛好者,開始在各類站點聽到有人談論 Reason 這個新語言,也看見 Jordan(React 做者)說 ReasonReact 將是將來,但你倒是不知道從哪下手,那麼這篇小教程就是爲你準備的。

ps. 有條件的話仍是儘可能看 ReasonReasonReact 的官方文檔吧javascript

pps. Jared 寫的 A ReasonReact Tutorial 是 ReasonReact 最棒的入門指南。本文也是經由他容許,參考了不少其中的內容。能看的懂英語的都直接去他那裏吧~html

Reason 是什麼?

Reason 是一門基於 OCaml 的語言,它爲 Ocaml 帶來了新的語法和工具鏈。它既能夠經過 BuckleScript 被同編譯爲 JavaScript,也支持直接編譯爲原生的二進制彙編。Reason 提供了和 JavaScript 類似的語法,也可使用 npm 來安裝依賴。長江後浪推前浪,Reason 丟掉了歷史包袱,比 JavaScript 多了可靠的靜態類型,也更快更簡潔!前端

爲何要學 Reason ?

「爲啥我要花時間學一門全新的語言呢?是 JavaScript 哪裏很差仍是大家要求過高?」

錯!Reason 不是一門全新的語言,事實上 80% 的語義均可以直接對應到現代的 JavaScript 上,反之也差很少。你只須要丟棄掉一丟丟的 JavaScript 邊角語法,再學一點點好東西,就能夠得到也許 ES2030 纔有的特性。對於大部分人來講,學習 Reason 也不會比學習 JavaScript 和一個其餘的類型系統(好比 Flow)來的慢。java

不相信的話,先本身去看看 JS -> Reason 速查表,而後去 playground 體驗一下吧。node

從哪開始?

若是你體驗了一下,仍是提不起興趣,你能夠再出門右轉逛逛隔壁家 elmClojureScript 試試。但若是你以爲 ok,殊不知道從哪下手,那不妨和我同樣,從我們熟悉的 React 開始。Jordan 從新發起了 ReasonReact 這個新項目,讓咱們能夠換一種更簡單優雅的方式寫 React。react

ReasonReact

ReasonReact 提供了一些和 React 腳手架相似的工具,好比 reason-scripts。不過爲了理解的深刻一點,不妨從零開始搭起咱們的第一個 ReasonReact 項目。新建一個項目目錄,名字隨意,讓咱們開始吧~ 固然,你也能夠直接 clone 已經準備好了的 simple-reason-react-demo 項目來參考。webpack

首先,初始化 package.jsongit

{
  "name": "simple-reason-react-demo",
  "version": "0.1.0",
  "scripts": {
    "start": "bsb -make-world -w",
    "build": "webpack -w"
  },
  "dependencies": {
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "reason-react": "^0.3.0"
  },
  "devDependencies": {
    "bs-platform": "^2.1.0",
    "webpack": "^3.10.0"
  }
}

而後安裝一下依賴:es6

npm install --registry=https://registry.npm.taobao.org

項目裏安裝了最新的 React 和 ReactDOM,以及額外的 ReasonReact。而編譯工具使用了前端業界標準 Webpack 和 張宏波 開發的 bs-platform。你可能暫時還弄不清 BuckleScript 在這裏將要扮演怎樣的角色,不過不要緊,暫時你只要把他理解成 Reason -> JavaScript 的編譯器就行了,就像 Babel 把 ES2016 編譯成了 ES5 同樣。github

而後,咱們添加一個 BuckleScript 的配置文件 bsconfig.json

{
  "name" : "simple-reason-react-demo",
  "reason" : {"react-jsx" : 2},
  "refmt": 3,
  "bs-dependencies": ["reason-react"],
  "sources": "src"
}

能夠大概猜出來,項目用到了 reason 的 react-jsx 語法,依賴了 reason-react,源代碼存放在 src 目錄。時間有限,就先不展開研究了,詳細配置能夠查看 bsconfig.json 結構。再建立下 src 目錄,咱們的項目應該長成這樣了

.
├── bsconfig.json
├── src
├── node_modules
└── package.json

你好,ReasonReact

是否是很容易的就到這裏了,讓咱們正式開始寫 Reason 吧!在 src 裏新建 Main.re 文件,寫下 Hello World

ReactDOMRe.renderToElementWithId(
  <div>(ReasonReact.stringToElement("Hello ReasonReact"))</div>,
  "root"
);

幾乎和 React 代碼同樣不是麼?而後咱們運行編譯命令

# 至關於以前寫好的 'bsb -make-world -w'
npm start

一切正常的話,能夠看到編譯成功的提示,不然就要辛苦你按錯誤提示排查一下了,注意 bsb 的輸出對咱們的很重要,一些錯誤提示和類型檢查的信息都要經過它來看。由於咱們開啓了 -w 的 watch 模式,接下來還要用到,就先不用退出了。bsb 將代碼編譯到了 lib 目錄下

lib
├── bs
└── js
    └── src
        └── Main.js

目前咱們要關注一下的是 lib/js/src/Main.js,打開它咱們能夠看到編譯好的 JavaScript 代碼,很是漂亮是吧?這都是 BuckleScript 的功勞。爲了讓代碼能在瀏覽器裏運行,咱們還須要用 Webpack 打包一下模塊化,這些你都應該很是熟悉了。

建立 public/index.html

<!doctype html>
<meta charset=utf8>
<title>你好</title>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>

以及 webpack.config.js

const path = require('path');

module.exports = {
  entry: './lib/js/src/Main.js',
  output: {
    path: path.join(__dirname, "public"),
    filename: 'bundle.js',
  },
};

Webpacck 配置裏入口是 bsb 編譯生成的 './lib/js/src/Main.js'。再打開一個終端運行 npm run build,咱們的準備工做就所有就緒了。咱們只利用 webpack 作很簡單的打包,因此你基本能夠忽略這個終端的輸出,仍是把精力放在剛剛的 start 命令上。接下來直接在瀏覽器裏打開 index.html 文件,就能夠看到 「Hello ReasonReact」 了~

第一個組件

讓咱們開始第一個組件的開發,一個只能加加減減的步進器。新建一個組件文件:src/Stepper.re

let component = ReasonReact.statelessComponent("Stepper");

let make = (children) => ({
  ...component,
  render: (self) =>
    <div>
      <div>(ReasonReact.stringToElement("I'm a Stepper! "))</div>
    </div>
});

ReasonReact.statelessComponent 會返回一個默認的組件定義,裏面包含了你熟悉的那些生命週期函數以及其餘一些方法和屬性。這裏咱們定義了 make 方法,目前它只接受一個 children 參數,返回了一個組件。咱們利用了相似 es6 的 ... 對象展開操做符 重寫了 component 中的 render 方法。神奇的是這段代碼竟然徹底符合 JavaScript 的語法...接下來,讓咱們再修改一下 Main.re,讓他渲染這個 Stepper 組件

ReactDOMRe.renderToElementWithId(<Stepper />, "root");

刷新下瀏覽器,你應該能夠看到剛寫好的組件就這麼成功的 render 出來了。

你可能很好奇爲何這裏沒有寫 require()import。這是由於 Reason 的跨文件依賴是自動從你的代碼中推導出來的,當編譯器看到 Stepper 這個在 Main.re 中並無定義的量,它就會自動去找 Stepper.re 這個文件並引入該模塊。

熟悉 ReactJS 的同窗都應該知道,jsx 並非什麼特殊的語法,只是會被編譯成普通的函數調用,好比

<div>Hello React</div>
// to
React.createElement(
  "div",
  null,
  "Hello React"
);

而在 ReasonReact 中,jsx 會被翻譯成

<Stepper />
/* to */
Stepper.make([||]) /* [|1,2,3|] 是 Reason 中數組的語法 */

意思是調用 Stepper 模塊的 make 函數,參數是一個空的數組。這就和咱們以前寫好的 Stepper.re 中的 make 函數對應上了,這個空數組就對應於 make 的參數 children。再讓咱們看眼咱們的第一個組件

let component = ReasonReact.statelessComponent("Stepper");

let make = (children) => ({
  ...component,
  render: (self) =>
    <div>
      <div>(ReasonReact.stringToElement("I'm a Stepper! "))</div>
    </div>
});

不一樣於 ReactJS 中組件的 render,這裏的 render 方法須要一個參數:self,暫且你能夠把它比做 this,由於咱們的 Stepper 是一個 stateless 組件,因此咱們還用不到它。render 方法裏返回的一樣是虛擬 DOM 節點,不一樣的是節點必須符合 ReasonReact 要求的節點類型。咱們不能再直接寫 <div>Hello</div>,而得使用 ReasonReact 提供的 stringToElement 包裝一層。嫌函數名太長?先忍着吧...

加上 state

思來想去,咱們的步進器還須要一個狀態,就是要顯示的數字。在 Reason 中,咱們須要先定義 state 的類型(type

type state = {
  value: int
};

若是你寫過 flow 或者 typescript,必定不會以爲奇怪,這標識咱們的 state 中包含 int 類型的 value 字段。而後,咱們須要開始把原先的 statelessComponent 替換成 reducerComponent,原先的組件代碼也須要略微改動一下

type state = {
  value: int
};

let component = ReasonReact.reducerComponent("Stepper");

let make = (children) => ({
  ...component,
  initialState: () => {
    value: 0
  },
  reducer: ((), state) => ReasonReact.NoUpdate,
  render: (self) =>
    <div>
      <div>(ReasonReact.stringToElement(string_of_int(self.state.value)))</div>
    </div>
});

聰明的你確定一下就看懂了 initialState 和 ReactJS 的 getInitialState 簡直如出一轍。而在 render 這裏也很相似,組件當前的狀態能夠經過 self.state 獲取,仍是爲了類型匹配咱們套了一層 string_of_intint 類型的 value 轉換成 string。而新增的 reducer 函數可能就有點看不懂了。有意思的地方來啦~

在 ReactJS 中,咱們依靠 setState 去手動的更新 state。ReasonReact 裏則引入了 「reducer」 的概念,看上去很像 Redux 對吧?也許是 Jordan 本身也不是很喜歡 setState 這個非函數式的操做吧 …… ReasonReact 裏更新一個組件狀態分爲兩個步驟,首先發起一個 action,而後在 reducer 中處理它並更新狀態。此時此刻,咱們尚未添加 action,因此 reducer 仍是無操做的,咱們直接返回了一個 ReasonReact.NoUpdate 來標識咱們並無觸發更新。讓咱們繼續加上 action

type state = {
  value: int
};

/* here */
type action =
  | Increase
  | Decrease;

let component = ReasonReact.reducerComponent("Stepper");

let make = (children) => ({
  ...component,
  initialState: () => {
    value: 0
  },
  reducer: (action, state) => {
    /* here */
    switch action {
    | Decrease => ReasonReact.Update({value: state.value - 1})
    | Increase => ReasonReact.Update({value: state.value + 1})
    };
  },
  render: (self) =>
    <div>
      /* and here */
      <button onClick={self.reduce((evt) => Decrease)}>(ReasonReact.stringToElement("-"))</button>
      <div>(ReasonReact.stringToElement(string_of_int(self.state.value)))</div>
      <button onClick={self.reduce((evt) => Increase)}>(ReasonReact.stringToElement("+"))</button>
    </div>
});

首先,咱們定義了 action 類型,它是一個 Variant(變體)。在 JavaScript 的世界裏咱們沒見過這種值,它用來表示這個變體(或者先叫它 "枚舉"?)可能的值。就像在 Redux 中推薦先聲明一堆 actionType 同樣,這個例子裏咱們定義了 +(Increase) 和 -(Decrease) 兩種 action

而後咱們就能夠給 button 增長點擊的回調函數。咱們使用了 self.reduce 這個函數(還記得 dispatch 麼),它接收一個函數 (evt) => Increase 作轉換,能夠把它看做將點擊的 event(在這裏咱們忽略掉了它由於用不到它...)換成一個 action,而這個 action 會被 self.reduce 用於作一個反作用操做來更新 state,更新 state 的操做就在 reducer 中。

reducer 內採用了模式匹配的形式,定義了對於全部可能的 action 須要如何更新 state。例如,對於 Increase 這個類型的 action,返回了 ReasonReact.Update({value: self.state.value + 1}) 去觸發更新。值得注意的是,組件的 state 是不可變的,而目前 state 中只有 value 一個字段,因此咱們沒有 {...state, value: state.value + 1} 這樣去展開它。

若是你熟悉 Redux 的話,應該很是熟悉這一套範式了(雖然這其實來源於 Elm)。不一樣的是,咱們直接擁有不可變的數據,再也不須要過分的使用 JavaScript 的 String 來作 actionType,reducer 也寫的更加優雅簡單了,看着真是舒服~

繼續?

這篇文章到這裏也就暫時結束了,距離能作出通常的組件功能咱們還差了不少東西。目前我也只是在一些我的的小項目中使用 Reason,文章內容很淺,主要是但願能啓發下厲害的你去嘗試 Reason 這個還算新鮮的語言,相信它會讓你眼前一亮的。

對了,既然都看到這裏了,不如再去看看今年兩次 React Conf 上 chenglou 關於 Reason 的精彩演講吧~

相關文章
相關標籤/搜索