ということでいつものシリーズです。今回はちょっと異色かも。
React.rb って何?
Opal を知っていますか? Ruby を JavaScript にコンパイルしてくれるコンパイラです。Opal を使えば JavaScript のアレとかコレとかそういったことにイライラすることはなく、Ruby でさくっと JavaScript を実装できます。Rails や Sinatra にも対応しているので安心です。
そんな Opal 上で動く 100% pure Ruby な React.js の実装が React.rb です。ええ、パクリです。Web サイトもドキュメントも全てパクリです。オリジナルの React.js のサイトやドキュメントが CC BY 4.0 という原作の著作権表示さえしていればパクってもいいっていうライセンスなので、本当にパクっています。サイトデザインだけではなく、HTMLのソースレベルでパクってます。本当にありがとうございました。
そんな Reat.rb ですが、Opal と一緒に使うと、Ruby だけで React.js に相当する素晴らしいビューを実装できます。Rails や Sinatra にも組み込めるので、安心です。これで、JavaScript とはおさらばできるというものです。
チュートリアルの作成
準備する
さっそくチュートリアルを書き換えていきましょう。React Tutorial はあらかじめクローンしておいてください。
本来は Rake でタスクを作ってコンパイルするのですが、今回は inline-reactive-ruby を使います。コレを使うと、Ruby で書いたコードをブラウザ上でコンパイルすることができます。先ほどのサイトに書いてあるリンク先から、 inline-reactive-ruby.js をダウンロードして public/scripts/nline-reactive-ruby.js
としておいて置きます。
index.htmlを書き換える
inline-reactive-ruby.js を追加します。また、jQuery が必要なので、残しておきます。marked.min.js は同じ用途で使うので、これも残しておきます。後はいりません。
script の type は "text/ruby" です。inline-reactive-ruby.js によって、自動的に JavaScript に変換され、読み込まれます。このあたりは Babel を使ったオリジナルと一緒です。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React Tutorial</title>
<!-- Not present in the tutorial. Just for basic styling. -->
<link rel="stylesheet" href="css/base.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
<script src="scripts/inline-reactive-ruby.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/ruby" src="scripts/example.rb"></script>
<script type="text/ruby">
# To get started with this tutorial running your own code, simply remove
# the script tag loading scripts/example.js and start writing code here.
</script>
</body>
</html>
ソースを書く
では、オリジナルの JSX を Ruby に書き換えていきましょう。
# This file provided by Facebook is for non-commercial testing and evaluation
# purposes only. Facebook reserves all rights not expressly granted.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'opal'
require 'browser'
require 'browser/interval'
require 'browser/delay'
require 'browser/console'
require 'opal-jquery'
require 'reactive-ruby'
class Comment < React::Component::Base
param :author, type: String
def raw_markup
{ __html: `marked(#{children.join}, {sanitize: true})` }.to_n
end
def render
div.comment do
h2.commentAuthor { params.author }
span(dangerouslySetInnerHTML: raw_markup)
end
end
end
class CommentBox < React::Component::Base
param :url, type: String
param :pollInterval, type: Numeric
def load_comments_from_server
HTTP.get(params.url) do |response|
if response.ok?
state.data! response.json
else
$console.error(params.url, response.status_code, response.error_message)
end
end
end
def handle_comment_submit(comment)
comments = state.data
# Optimistically set an id on the new comment. It will be replaced by an
# id generated by the server. In a production application you would likely
# not use Date.now() for this and would have a more robust system in place.
comment['id'] = (Time.now.to_f * 1000).to_i
new_comments = comments + [comment]
state.data! new_comments
HTTP.post(params.url, payload: comment) do |response|
if response.ok?
state.data! response.json
else
state.data! comments
$console.error(params.url, response.status_code, response.error_message)
end
end
end
before_mount do
state.data! []
end
after_mount do
load_comments_from_server
every(params.pollInterval) { loadCommentsFromServer }
end
def render
div.commentBox do
h1 { 'Comments' }
CommentList(data: state.data)
CommentForm(
onCommentSubmit: proc { |comment| handle_comment_submit(comment) })
end
end
end
class CommentList < React::Component::Base
param :data
def render
div.commentList do
params.data.each do |comment|
Comment(author: comment['author'], key: comment['id']) do
comment['text']
end
end
end
end
end
class CommentForm < React::Component::Base
param :onCommentSubmit, type: Proc
before_mount do
state.author! ''
state.text! ''
end
def handle_author_change(e)
state.author! e.target.value
end
def handle_text_change(e)
state.text! e.target.value
end
def handle_submit(e)
e.prevent_default
author = state.author.strip
text = state.text.strip
return if text.empty? || author.empty?
params.onCommentSubmit(author: author, text: text)
state.author! ''
state.text! ''
end
def render
form.commentForm do
input(type: 'text', placeholder: 'Your name', value: state.author)
.on(:change) { |e| handle_author_change(e) }
input(type: 'text', placeholder: 'Say something...', value: state.text)
.on(:change) { |e| handle_text_change(e) }
input(type: 'submit', value: 'Post')
end.on(:submit) { |e| handle_submit(e) }
end
end
Element['#content'].render do
CommentBox(url: '/api/comments', pollInterval: 2000)
end
コレで完成です。あとは、好みのサーバを動かして、テストしてみて下さい。なお、server.rb は JSON をパースする処理が抜けているので、動きません。server.js あたりを試して下さい。
解説
Ruby 知っていたら解るよね? そのうち、Opal の記事を書く(ような気がする)から許して。
まとめ
いかがだったでしょうか? Opal はまだ 0.9.2 (2016年3月6日現在) と 1.0 になっていなく、仕様が不安定な部分があります。React.rb も Opal 0.8.0 依存、ドキュメントの多数が工事中、React.js 0.14 相当に未対応など、まだまだ未完成です。しかし、十分実用的なレベルになっているかと思います。この altJS 戦国時代において、注目に値する組み合わせではないでしょうか?