RubyistのためのフロントエンドフレームワークOvto

はじめに

こんにちは。yharaです。みなさんはWebアプリを作るとき何を使っているでしょうか?Ruby界隈だと、Railsと答える人が多そうですね。ではフロント側は?React、Vue.js、Angularなどいろいろありますね。

そんな中で、hyperappというフレームワークを聞いたことはあるでしょうか。hyperappはわずか400行のJavaScriptで実装された「マイクロフレームワーク」ですが、そのサイズからは考えられないほど本格的な機能を持っています。

hyperappを見て私は思いました。これはすごい、たったこれだけでReact+Reduxのかなりの機能が提供できているじゃないか、と。そして、400行しかないのなら、これをまるごとRubyに移植できないだろうか?と。

そうしてできたのがRubyistのためのフロントエンドフレームワーク「Ovto」です。シンプルで高機能なAPIを持つのはhyperappと同じですが、アプリを全てRubyで書けるというのが違うところです:-)

本稿ではOvtoの概要と、簡単なサンプルアプリを作るところまでを解説します。Ovtoは、自分で言うのもなんですが使っていて楽しいフレームワークなので、ぜひ手を動かしてみてください。

Ovtoとは

Ovto(オブト)はReactやVue.js等と同じく、ブラウザ上で動く複雑なアプリケーションを作るためのフレームワークです。サーバ側の機能はないので、例えばDBにデータを保存したりしたい場合はRailsやSinatraなどと組み合わせて使うことを想定しています。

Ovtoロゴ

Ovtoの特徴は以下です。

Rubyで書ける
普通、ブラウザで動くアプリはJavaScriptで書く必要がありますが、OvtoではRubyだけでアプリを作ることができます。Ovtoで作ったアプリは、Opalを使ってJavaScriptにコンパイルすることでブラウザで動かします。
学習が簡単
State, Actions, Componentという3つのクラスを覚えるだけで実用的なアプリが作れます。
仮想DOMベース
Reactと同じく仮想DOMベースで、全体の構造を管理しつつも高速なレンダリングが可能です。
シングルステート
react-reduxのように、アプリは単一の状態を持ち、状態が決まれば画面も決まります。

難しいことを書きましたけど、一番大事なのはOvtoは「楽しい」ということです。Ovtoができたあと、何か実用的なアプリを作ってみようということでVisionというTODOアプリを作ったのですが、その過程はとても楽しかったです。

Ovtoアプリを作る

ここからは実際にOvtoアプリを作っていきます。今回は説明を簡単にするため、静的なhtmlファイルを使いますが、RailsやSinatraと組み合わせる場合もアプリの書き方は同じです。Rails・SinatraアプリにOvtoアプリを埋め込む方法については、以下のサンプルを参考にしてください。

準備

RubyとBundlerはインストールされているものとします。以下のようなGemfileを作り、bundle installします。

source 'https://rubygems.org'

gem 'ovto'
gem 'rake'

同じディレクトリに、以下の3つのファイルを作ります。

まずはindex.html。このファイルはこれで完成、つまり本稿の最後までこのままです。ほとんどdivタグしかないように見えますが、中身はOvtoで作っていくのでこれで良いのです。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <script type='text/javascript' src='app.js'></script>
    <link rel='stylesheet' href='style.css' />
  </head>
  <body>
    <div id='ovto'></div>
    <div id='ovto-debug'></div>
  </body>
</html>

次はstyle.css。スタイルシートです。今回はサンプルコードなので、最小限のスタイルだけ適用します。以下のように書いてください(あとで使います)。

table#board td {
  width: 50px;
  height: 50px;
  border: 1px solid black;
  text-align: center;
}

最後にapp.rbです。このファイルにOvtoのコードを書いていきます。まずは画面にHELLOと出すだけの、最小限のOvtoアプリを用意します。

require 'ovto'

class MyApp < Ovto::App
  class State < Ovto::State
  end

  class Actions < Ovto::Actions
  end

  class MainComponent < Ovto::Component
    def render(state:)
      o 'div' do
        o 'h1', "HELLO"
      end
    end
  end
end

MyApp.run(id: 'ovto')

コンパイルする

3つのファイルが用意できたら、さっそく動かしてみましょう。以下のコマンドを実行すると、app.rbがapp.jsに変換されます。

$ bundle exec opal -c -g ovto app.rb > app.js

index.htmlをブラウザで開くと、HELLOと表示されたはずです。

これでOvtoアプリを作る環境が整いました。試しに「HELLO」の部分を適当な文字列に変えて、もう一度コンパイルしてみてください。ブラウザをリロードすれば文字が変わるはずです。(変わらない場合はブラウザのキャッシュが効いているかもしれません。例えばGoogle Chromeの場合は、開発者コンソールを開いて「Disable Cache」にチェックを入れることで、一時的にキャッシュを無効化できます)

State, Actions, Component

app.rbには、State, Actions, MainComponentという3つのクラスが出てきました。Ovtoではこの3つのクラスを使ってアプリを書いていきます。

Stateはアプリケーションの状態を保持するクラスです。MainComponentはビューの定義で、stateを受け取ってどんな画面を表示したいかを記述します。

Actionはstateに変更を加えるものです。アプリケーションの動作中は、アプリの状態は刻一刻と変化していきますが、MainComponentから直接stateを書き換えることはできません。stateを書き換えたいときは、Actionsクラスに定義したメソッド(以下では単に「アクション」と呼びます)を経由する必要があります。一見めんどくさそうですが、このような制約を設けることにより、「状態がどのように変化するのか」「状態がどこで変化するのか」を調べるのがとても簡単になります。

以下では「カウンター」というデモを通して、3つのクラスの具体例を見ていきます。

State

最初に、Stateクラスを以下のようにします。

class MyApp < Ovto::App
  class State < Ovto::State
    item :count, default: 0
  end

itemメソッドはStateクラスの要素を宣言します。ここではcountという要素を宣言しています。要素が複数あるときはitemメソッドの呼び出しを並べます。default:はデフォルト値の指定です。

StateオブジェクトはState.newで作れます。引数には各要素の値を指定します。

state = MyApp::State.new(count: 15)
state.count  #=> 15

引数を省略した場合はデフォルト値で初期化されます。

state = MyApp::State.new
state.count  #=> 0

MainComponent

次にMainComponentを書いてみましょう。以下のように書き換えてみてください。

  class MainComponent < Ovto::Component
    def render(state:)
      o 'div' do
        o 'h1', 'Counter'
        o 'div', state.count
      end
    end
  end

これを実行すると以下のようになります。

Counter 0

開発者コンソールを開くと、<div id='ovto'>内に以下のようなHTMLが生成されていることがわかります。

<div>
  <h1>Counter</h1>
  <div>0</div>
</div>

MyApp.runでOvtoアプリを起動すると、まずMyApp::Stateクラスのオブジェクトが自動的に作成され、それを引数としてMainComponentクラスのrenderメソッドが呼ばれます。引数のstate:というのは見慣れないかもしれませんが、キーワード引数の初期値を省略した形です。Ovtoではキーワード引数を多用するので、この機会に慣れてください。1

renderメソッドは、「このようなDOMを生成してほしい」という依頼を返さなくてはなりません。この依頼を作成するのがoメソッドです。

oメソッド

ぱっと見では箇条書きみたいに見えますが、「o」はOvto::Componentクラスが提供する1文字メソッドです。以下のように括弧をつければ、メソッド呼び出しであることがわかりやすいでしょうか。

        o('h1', 'Counter')

oメソッドは以下の引数を取ります。

   o(tag_name, attrs={}, content, &block)

tag_nameは'div'など、HTMLのタグ名を指定します。attrsはタグの属性値をハッシュで指定します(省略可)。contentはタグの中身を文字列で指定します。

タグの中身はブロックで渡すこともできます。タグをネストさせたいときはブロックを使います。

Actions

stateを表示する方法がわかったので、次はstateを書き換えられるようにしましょう。

前述のように、Componentから直接stateを書き換えることはできません。書き換えたいときは必ずActionsを通す必要があります。ということで、Actionsクラスに「カウントを増やす」というメソッドを定義します。

  class Actions < Ovto::Actions
    def increment_count(state:, num:)
      return {count: state.count + num}
    end
  end

Actionsクラスのメソッドは、stateをキーワード引数で受け取り、stateのどの要素がどう変化するかをハッシュで返します。

アクションを呼ぶ

次にMainComponentを編集してこのアクションを呼ぶボタンを設置してみましょう。

  class MainComponent < Ovto::Component
    def render(state:)
      o 'div' do
        o 'h1', 'Counter'
        o 'div', state.count
        # カウントを増やすボタン
        o 'input', type: 'button', onclick: ->(e){ actions.increment_count(num: 1) }, value: '+1'
        o 'input', type: 'button', onclick: ->(e){ actions.increment_count(num: 3) }, value: '+3'
      end
    end
  end

上記を実行してボタンを押すと、どうなるでしょうか?そう、画面に表示されるカウントが変化したはずです。

Counter 26 +1 +3

Ovtoはstateが変化すると、自動的に新しいstateで画面を再描画します。これは、jQueryを使ったプログラミングと一味違うところです。jQueryの場合は「画面をどう変化させるか」を常に考えなければいけないので、「○○画面を開いて最初のボタンをクリックして二番目のフォームを…」のように複雑なアプリになると大変になってきます。

一方Ovtoの場合は「各状態がどういう画面になるか」をしっかり書いておけば、あとは状態を変化させるだけで自動的に画面ができあがります!

イベントハンドラ

「ボタンが押されたとき」のようなイベントハンドラの指定もoメソッドを使います。以下のonclick:で渡しているのがイベントハンドラです。

        o 'input', type: 'button', onclick: ->(e){ actions.increment_count(num: 1) }, value: '+1'

oメソッドの第二引数にハッシュを指定した場合、タグの属性値の指定になりますが、いくつか特殊な指定があります。

  • onxx(onclick, onchange等): イベントハンドラの指定
  • style: CSSの指定(文字列ではなくハッシュを渡す)
  • oncreate/onupdate/onremove/ondestroy: タグのライフサイクルイベント(生成・削除等)をフックする
  • key: キーの指定

上のコードでは、onclickを使ってボタンが押されたときの処理を記述しています。実行したい処理はProcオブジェクトで指定します。->(){ ... }はProcオブジェクトを作るRubyの文法です。

イベントハンドラ内では、actionsメソッドを経由してアクションを呼び出すことができます。アクションを実行すると、stateが更新され、新しいstateを引数にしてrenderが呼び出されて、画面が書き換わります。2

Ovtoで三目並べを作る

以上で、Ovtoの根幹であるStateとActionsとMainComponentについて解説できました。ここからはもう少し複雑なアプリケーションとして、三目並べゲームを作ってみます。

Stateを考える

Ovtoでアプリを作るときは、まずStateから設計します。まずは盤面データが必要ですね。これは3x3の二次元配列にすれば良さそうです。各セルは「○」「×」「空」のいずれかなので、それぞれ0, 1, nilで表すことにしましょう。

あとは今どっちの手番かも覚えておく必要があります。とすると、こんな感じでしょうか。

  class State < Ovto::State
    # 盤面データ(0または1またはnil)
    item :board, default: [
      [nil, nil, nil],
      [nil, nil, nil],
      [nil, nil, nil],
    ]
    # 現在のプレイヤー(0または1)
    item :player, default: 0
  end

盤面を描画する

Stateが決まったら、それを使って画面を作ります。MyApp::MainComponentを以下のように書き換えてください。

  class MainComponent < Ovto::Component
    PLAYER_MARK = {0 => '○', 1 => '×'}

    def render(state:)
      o 'div' do
        o 'div#player' do
          "PLAYER: #{PLAYER_MARK[state.player]}"
        end
        o 'table#board' do
          state.board.each do |row|
            o 'tr' do
              row.each do |cell|
                o 'td' do
                  PLAYER_MARK[cell]
                end
              end
            end
          end
        end
      end
    end
  end

ゲームボードとプレイヤー情報が出るようになりました。tdタグに枠線が付いているのは、最初に説明したstyle.cssのおかげです。

Player: ○ 空のゲーム盤

アクションを書く

ボードが出たので、思わずクリックしたくなりますが、今はクリックしても何も起きません。tdタグのonclickイベントを使って、○×を置けるようにしましょう。○×を置くということはstate.boardを変更するということなので、まずはセルに○×を置くアクションが必要です。

セルの内容を更新するアクションということで、update_cellという名前にしましょうか。引数としてどのセルなのか(x, y)を受け取る必要がありそうです。あとは○と×のどっちを置くかという情報も必要ですが、これはstate.playerを見ればわかるので引数にはしなくて良いでしょう。

  class Actions < Ovto::Actions
    def update_cell(state:, x:, y:)
      # 新しい盤面を作る
      new_board = state.board.map{|row| row.dup}
      new_board[y][x] = state.player
      # 新しいプレイヤーは、現在と逆のプレイヤー(0なら1、1なら0)
      new_player = 1 - state.player
      return {board: new_board, player: new_player}
    end
  end

ここで一つOvtoの大事なルールを紹介します。それはstateに入っているオブジェクトを破壊的に変更してはいけないということです。例えば、盤面を更新するのにstate.board[y][x] = ...とするのではなく、新しい3x3のArrayを作ってやらないといけないということです。

というのはOvtoでは効率のため、stateが変化していない(==が真を返す)場合は画面を更新しないからです。そのため、現在のstateは変化前のものとして触らないでおく必要があるのです。

上記ではnew_board = state.board.map{|row| row.dup}のようにしてArray全体を複製しています。

アクションを呼ぶ

アクションができたので、tdにonclickイベントのハンドラを追加し、セルがクリックされたらこのアクションを呼ぶようにします。そのときにどのセルがクリックされたかを渡す必要があるので、2箇所の.each.with_indexを付けて、xyが取れるようにします。

        o 'table#board' do
          state.board.each.with_index do |row, y|
            o 'tr' do
              row.each.with_index do |cell, x|
                o 'td', onclick: ->(e){ actions.update_cell(x: x, y: y) } do
                  PLAYER_MARK[cell]
                end
              end
            end
          end
        end

これで、クリックすると○×が置けるようになりました。また、プレイヤー欄も○から×にちゃんと変化していますね。

ゲーム中

バグ修正

上のコードにはバグがあるのですが、お気づきでしょうか?update_cellでは指定された座標に○または×を置いていますが、そこに今何が入っているかはチェックしていませんね。なので、なんと相手の手を上書きすることができてしまいます!

これは以下の1行を追加して、指定された場所にすでに何か入っている場合はすぐにreturnするようにすれば直ります。アクションがnilを返した場合、stateは更新されません。

    def update_cell(state:, x:, y:)
      # そこには置けない
      return if state.board[y][x] != nil
      ...
    end

勝者を判定する

これで対戦ができるようになりましたが、○か×を3つ並べてもそのままゲームが続いてしまいます。勝利条件が満たされたら勝者を表示するようにしましょう。そのあと、クリックでゲーム状態をリセットして次のゲームを開始できるといいですね。

あ、三目並べの場合は決着が付かずに引き分けになる場合もありますね。この場合もリセットボタンが表示されてほしいですね。

ということで、「ゲームが決着したかを返すメソッド」と「勝者がどちらかを返すメソッド」を作りたいと思います。これはどのクラスに定義することもできますが、いずれも状態から決まる情報なので、Stateクラスに定義するのが良いでしょう。

以下ではそれぞれgame_over?winnerという名前でメソッドを定義しています。3つ並びがあるかの判定はいろいろな書き方ができると思いますが、今回はこんな感じにしてみました。

  class State < Ovto::State
    item :board, default: [
      [nil, nil, nil],
      [nil, nil, nil],
      [nil, nil, nil],
    ]
    item :player, default: 0

    # ゲームが終了しているとき真を返す
    def game_over?
      # 勝者が決まったらゲーム終了
      return true if winner 
      # 盤面が全部埋まったらゲーム終了
      return true if board.all?{|row| row.all?{|cell| cell != nil}}
      # それ以外の場合はゲーム中
      return false
    end

    # 勝者(0または1)を返す。勝者がいないときはnilを返す
    def winner
      # 横一列が作られたかをチェック
      board.each do |row|
        winner = check_winner(*row)
        return winner if winner
      end
      # 縦一列が作られたかをチェック
      board.transpose.each do |col|
        winner = check_winner(*col)
        return winner if winner
      end
      # 斜めの列が作られたかをチェック
      winner = check_winner(board[0][0], board[1][1], board[2][2])
      return winner if winner
      winner = check_winner(board[0][2], board[1][1], board[2][0])
      return winner if winner
      # 勝者がいない(=まだ試合が続いているか、引き分けで終わった)
      return nil
    end

    private

    # a,b,cが等しいときその値を返す
    def check_winner(a, b, c)
      if a == b && b == c
        return a
      else
        return nil
      end
    end
  end

これを使って、勝者表示を実装してみましょう。MainComponentのPLAYER表示の下あたりに以下のif式を入れます。

        o 'div#player' do
          "PLAYER: #{PLAYER_MARK[state.player]}"
        end
        # 勝者を表示する
        if state.game_over?
          o 'div#winner' do
            "WINNER: #{PLAYER_MARK[state.winner]}"
          end
        end

○の勝利

○が横一列に並んだので、「WINNER: ○」と出ていますね。

バグ修正

これだけだとWINNERが出たあともセルをクリックできてしまうので、tdのonclickに「ゲーム終了でない場合」という条件を追加しておきましょう。

                o 'td', onclick: ->(e){ actions.update_cell(x: x, y: y) unless state.game_over? } do
                  ...

あとはWINNERが決まったのにPLAYER欄が出ているのは変なので、ゲーム終了時はdiv#playerを描画しないよう、unlessで囲みましょう。

        unless state.game_over?
          o 'div#player' do
            "PLAYER: #{PLAYER_MARK[state.player]}"
          end
        end

リセットボタンを作る

だいぶゲームらしくなってきましたね。最後にリセットボタンを付けて、ゲームが終わったあと次のゲームをプレイできるようにしましょう。

まずはゲームの状態をリセットするアクションが要りそうです。MyApp::Actionsにメソッドを追加しましょう。

    # ゲームをリセットする
    def reset_game(state:)
      new_board =  [
        [nil, nil, nil],
        [nil, nil, nil],
        [nil, nil, nil],
      ]
      new_player = case state.winner
                   when 0 then 1
                   when 1 then 0
                   else state.player
                   end
      return {board: new_board, player: new_player}
    end

boardはアプリケーション開始時と同じく、3x3の空の配列にしています。playerは今回のゲームで負けた方を選ぶようにしてみました。

ビューの方は、WINNER表示の下にaタグを追加して、クリックされたら上のreset_gameアクションを呼ぶようにします。

        if state.game_over?
          o 'div#winner' do
            "WINNER: #{PLAYER_MARK[state.winner]}"
          end
          # リセットボタン
          o 'a', href: '#', onclick: ->{ actions.reset_game } do
            "RESET"
          end
        end

これでリセットボタンが出るはずです。試しに○を3つ並べてみると…

RESET

うん、大丈夫そうですね。RESETを押すと新しいゲームが始まります。これで、完成です:grin:

完成

最後に全体像を貼っておきます。ちょうど120行です。

require 'ovto'

class MyApp < Ovto::App
  class State < Ovto::State
    # 盤面データ(0または1またはnil)
    item :board, default: [
      [nil, nil, nil],
      [nil, nil, nil],
      [nil, nil, nil],
    ]
    # 現在のプレイヤー(0または1)
    item :player, default: 0

    # ゲームが終了しているとき真を返す
    def game_over?
      # 勝者が決まったらゲーム終了
      return true if winner 
      # 盤面が全部埋まったらゲーム終了
      return true if board.all?{|row| row.all?{|cell| cell != nil}}
      # それ以外の場合はゲーム中
      return false
    end

    # 勝者(0または1)を返す。勝者がいないときはnilを返す
    def winner
      # 横一列が作られたかをチェック
      board.each do |row|
        winner = check_winner(*row)
        return winner if winner
      end
      # 縦一列が作られたかをチェック
      board.transpose.each do |col|
        winner = check_winner(*col)
        return winner if winner
      end
      # 斜めの列が作られたかをチェック
      winner = check_winner(board[0][0], board[1][1], board[2][2])
      return winner if winner
      winner = check_winner(board[0][2], board[1][1], board[2][0])
      return winner if winner
      # 勝者がいない(=まだ試合が続いているか、引き分けで終わった)
      return nil
    end

    private

    # a,b,cが等しいときその値を返す
    def check_winner(a, b, c)
      if a == b && b == c
        return a
      else
        return nil
      end
    end
  end

  class Actions < Ovto::Actions
    def update_cell(state:, x:, y:)
      # そこには置けない
      return if state.board[y][x] != nil
      # 新しい盤面を作る
      new_board = state.board.map{|row| row.dup}
      new_board[y][x] = state.player
      # 新しいプレイヤーは、現在と逆のプレイヤー(0なら1、1なら0)
      new_player = 1 - state.player
      return {board: new_board, player: new_player}
    end

    def reset_game(state:)
      new_board =  [
        [nil, nil, nil],
        [nil, nil, nil],
        [nil, nil, nil],
      ]
      new_player = case state.winner
                   when 0 then 1
                   when 1 then 0
                   else state.player
                   end
      return {board: new_board, player: new_player}
    end
  end

  class MainComponent < Ovto::Component
    PLAYER_MARK = {0 => '○', 1 => '×'}

    def render(state:)
      o 'div' do
        unless state.game_over?
          o 'div#player' do
            "PLAYER: #{PLAYER_MARK[state.player]}"
          end
        end
        # 勝者を表示する
        if state.game_over?
          o 'div#winner' do
            "WINNER: #{PLAYER_MARK[state.winner]}"
          end
          # リセットボタン
          o 'a', href: '#', onclick: ->{ actions.reset_game } do
            "RESET"
          end
        end
        o 'table#board' do
          state.board.each.with_index do |row, y|
            o 'tr' do
              row.each.with_index do |cell, x|
                o 'td', onclick: ->(e){ actions.update_cell(x: x, y: y) unless state.game_over? } do
                  PLAYER_MARK[cell]
                end
              end
            end
          end
        end
      end
    end
  end
end

MyApp.run(id: 'ovto')

より大きなアプリを作るには

最後にOvtoアプリの拡張方法について説明しておきます。今回の三目並べは120行でできましたが、もっと大きなものを作ろうとすると、Stateクラスの要素が増えたり、Actionsクラスのメソッドが増えたり、MainComponent#renderが長くなったりして大変になると思います。

でも大丈夫、Ovtoではそれぞれについて対処法を考えてあります。

Stateの拡張

Stateについては「Stateを入れ子にする」という方法があります。例えば三目並べの例であれば、盤面に関する部分だけを独立したクラスBoardにするのが良いでしょう。まず以下のようにしてOvto::Stateを継承したクラスBoardを作ります。

  class Board < Ovto::State
    item :cells, default: Array.new(3){ Array.new(3){ nil }}
    ...
  end

次にMyApp::Stateが要素としてBoardオブジェクトを持つようにします。

  class State < Ovto::State
    # 盤面データ(0または1またはnil)
    item :board, default: Board.new
    # 現在のプレイヤー(0または1)
    item :player, default: 0
  end

これでだいぶすっきりするはずです。Boardクラスはboard.rbなど別のファイルに切り出すのも良いでしょう。opalコマンドでコンパイルする場合は-I .オプションを付ければ、require "board"で読み込むことができます。

Actionsの拡張

Actionsのメソッドが増えてきた場合は、moduleを使って分割するのが良いでしょう。普通のRubyクラスを整理する手順と同じです。

  module XxxActions 
    ...
  end
  module YyyActions
    ...
  end
  class Actions < Ovto::Actions
    include XxxActions
    include YyyActions
  end

MainComponentの拡張

MainComponent#renderが長くなった場合は、サブのComponentを定義するのが良いです。例えば三目並べの盤面部分をサブComponentにしてみると、以下のようになります。3

  class Board < Ovto::Component
    def render(state:)
      o 'table#board' do
        state.board.each.with_index do |row, y|
          o 'tr' do
            row.each.with_index do |cell, x|
              o 'td', onclick: ->(e){ actions.update_cell(x: x, y: y) unless state.game_over? } do
                PLAYER_MARK[cell]
              end
            end
          end
        end
      end
    end
  end

MainComponentではoメソッドのタグ名にクラスを指定することで、サブComponentをレンダリングできます。

        o Board

サブComponentのレンダリング時に引数を渡すこともできます。例えば盤面の色を指定できるようにするとしたら、こんな感じでしょうか。

  class Board < Ovto::Component
    def render(state:, color:)
      ...
    end
  end

    ...
        o Board, color: "red"

サブComponentもそれぞれ別のファイルに分けて、本体からrequireするようにするとより良いでしょう。

Ovtoアプリの分割については、以下に実際のアプリケーションでの例があるので参考にしてください。

おわりに

今回は「Rubyistのためのフロントエンドフレームワーク」Ovtoについて紹介しました。Ovtoの楽しさが伝われば嬉しいです。

関連リンク

Ovto公式サイト
https://yhara.github.io/ovto/
Ovtoソースコード(github)
https://github.com/yhara/ovto/
Opalアドベントカレンダー
https://qiita.com/advent-calendar/2018/opal

著者について

yhara (原 悠) twitter: @yhara blog: yhara.jp

Ruby歴18年。↑のブログもRubyで作っています

  1. キーワード引数の初期値を省略した場合は「必須キーワード引数」という扱いになり、メソッド呼び出し時にこのキーワードを指定し忘れていないかチェックしてくれるようになります。フロントエンドのコードは頻繁に書き換えが起こるので、引数名を変えたりしたときに変更漏れがすぐ分かるように、キーワード引数を使うよう設計しました。 

  2. 注意深い読者なら、呼び出し側にstate:がないのが気になったかもしれません。実はactionsが返すのはMyApp::Actionsのインスタンスではなく、Ovto::WiredActionsというクラスのインスタンスです。WiredActionsはアクション実行後のstate更新や画面の再描画などの処理を担当します。現在のstateをActionsのメソッドに渡すのもWiredActionsの仕事です。stateを手で渡してしまうと複数のアクションが同時に実行されたときに値がおかしくなる可能性があるので、WiredActionsの方で自動的に現在のstateを渡すようになっています。 

  3. StateのところでBoardというクラスを作った場合は名前が被ってしまうので、別のクラス名をつけるか、MyApp::State::BoardMyApp::MainComponent::Boardみたいにネストした名前空間に定義するのが良いでしょう。