tlog

主にシステム開発とプログラミングについて

ReactとScala.jsで簡単なプロジェクトを作成してみました

facebookが開発しているReactを使ってみたくて、どうせならAltJSであるScala.jsですべて実装しようと思いましたが、少々Scala.jsのReact実装が現時点では使いづらかったので、View層を素のReactで、サーバとの通信をScala.jsで実装したサンプルプロジェクトを作成してみました。

Reactのクライアントから、Scala.jsでjavascriptへ変換されたAjax APi(autowire)を用いてサーバとJsonデータを透過的にやり取りしています。

https://github.com/MomoPain/scalajs-react-crud

プロジェクトの構成は、Scala.jsのCrossProjectを用いています。

-shard(クライアントとサーバで利用するコード:モデルとデータ通信API)

-client(Scala.jsでjavascriptに変換されるデータ通信API)

-server(autowireのサーバサイド実装、Scalatraでjsonデータを受け付けている)

クライアントであるView層は、Reactのチュートリアルであるコメント情報をCRUD操作するアプリケーションを参考にしています。

var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function (comment) {
      return (
        <Comment key={comment.key} index={comment.key}author={comment.author} comment={comment.text}>
          
        </Comment>
      );
    });
    return (
    <table className="table">
        <thead>
        <th>#</th>
        <th>Name</th>
        <th>Comment</th>
        </thead>
        <tbody>
        {commentNodes}
        </tbody>
    </table>
    );
  }
});

var CommentForm = React.createClass({
    handleSubmit: function(e) {
        e.preventDefault();
        var author = React.findDOMNode(this.refs.author).value.trim();
        var text = React.findDOMNode(this.refs.text).value.trim();
        if (!text || !author) {
          return;
        }
        this.props.onCommentSubmit({author: author, text: text});
        React.findDOMNode(this.refs.author).value = '';
        React.findDOMNode(this.refs.text).value = '';
        return;
      },
      render: function() {
        return (
        <form className="form-inline" onSubmit={this.handleSubmit}>
          <div className="form-group">
            <input type="text" className="form-control" ref="author" placeholder="Your Name"/>
          </div>
          <div className="form-group">
            <input type="text" className="form-control" ref="text" placeholder="Say someting..."/>
          </div>
          <button type="submit" className="btn btn-success">Post</button>
        </form>
    
        );
      }
    });


var CommentBox = React.createClass({
      loadCommentsFromServer: function() {
          //call ajax api that is generated from scala.js
          client.CommentAction().list(this);
      },
      handleCommentSubmit: function(comment) {
          // call ajax api that is generated from scala.js
          client.CommentAction().update(comment, this);
          var comments = this.state.data;
          var newComments = comments.concat([comment]);
          this.setState({data: newComments});
      },
      getInitialState: function() {
        return {data: []};
      },
      componentDidMount: function() {
        this.loadCommentsFromServer();
        setInterval(this.loadCommentsFromServer, this.props.pollInterval);
      },
      render: function() {
        return (
          <div>
            <div className="panel panel-info">
              <div className="panel-heading">
                <h3 className="panel-title">Put comments.</h3>
              </div>
              <div className="panel-body">
                <CommentForm onCommentSubmit={this.handleCommentSubmit} />                
               </div>
               <CommentList data={this.state.data} />         
            </div>
            
          </div>
        );
      }
    });

var Comment = React.createClass({
      render: function() {
        // var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
        return (
          <tr>
          <td>{this.props.index}</td>
          <td>{this.props.author}</td>
          <td>{this.props.comment}</td>
          </tr>         
        );
      }
    });

React.render(
  <CommentBox url="url" pollInterval={10000} />,
  document.getElementById('content')
);

また、ReactコンポートへScala.jsからアクセスするためには、アクセス用のトレイトを定義しておく必要があります。

package client

import scala.scalajs.js

trait ReactComponent extends js.Object {
  def setState(obj: js.Object): Unit = js.native
}

クライアントとサーバがやりとりするコメントのモデルクラスです。

package shared.model

case class Comment(var id: Int, var author: String, var text: String) {
   def this(author: String, text: String) {
    this(0, author, text)
  }
}

サーバとのデータ通信は、autowireを利用して実装されたシンプルなAPIを用いてjsonデータをやり取りしています。

package shared.api

import shared.model.Comment

trait CommentApi {
  def list(): Seq[Comment]
  def update(model: Comment): Seq[Comment]
  def delete(id: Int): Seq[Comment]
}


package servlets.api

import shared.model.Comment
import shared.api.CommentApi
import scala.collection.mutable.Map

object CommentApiImpl extends CommentApi {

  var idCounter = 3

  val comments = Map[Int, Comment](
    1 -> Comment(1, "Pete Hunt", "comment"),
    2 -> Comment(2, "Body Geen", "comment comment"))

  def list(): Seq[Comment] = comments.values.toSeq.sortBy { c => c.id }

  def update(c: Comment): Seq[Comment] = {
    c.id = idCounter
    comments.put(idCounter, c)
    idCounter = idCounter + 1
    list()
  }

  def delete(id: Int): Seq[Comment] = {
    comments.remove(id)
    list()
  }
}

クライアントのAjaxApiの実装です。CommentAPIへReactコンポーネントからアクセスします。

package client

import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow
import scala.scalajs.js
import scala.scalajs.js.Dynamic._
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.annotation.JSExport
import autowire._
import shared.api.CommentApi
import shared.model.Comment
import upickle.MapW
import scala.scalajs.js.annotation.JSExportAll

@JSExport
object CommentAction {

  def setComments(comp: ReactComponent, comments: Seq[Comment]) {
    if (comments.isEmpty) {
      comp.setState(js.Array())
    } else {
      val data = comments.map(t => literal("key" -> t.id, "author" -> t.author, "text" -> t.text)).toJSArray
      comp.setState(literal("data" -> data))
    }
  }
  @JSExport
  def list(comp: ReactComponent) {
    PostClient[CommentApi].list().call().onSuccess {
      case comments => setComments(comp, comments)
    }
  }

  @JSExport
  def update(c: js.Dictionary[String], comp: ReactComponent) {
    val comment = new Comment(c.getOrElse("author", ""), c.getOrElse("text", ""))
    PostClient[CommentApi].update(comment).call().onSuccess {
      case comments => setComments(comp, comments)
    }
  }

  @JSExport
  def delete(id: Int, comp: ReactComponent) = {
    PostClient[CommentApi].delete(id).call().onSuccess {
      case comments => setComments(comp, comments)
    }
  }
}

ReactコンポーネントからScala.jsで変換されたCommentAction(データ通信呼び出しとコンポーネントへの値の設定)とCommentAPIでサーバサイドとCommentオブジェクトをやり取りしています。

ReactとScala.jsを利用することにより、JqueryなどでガリガリとAjaxやDOMの操作を実装していた時に比べてずいぶんシンプルなコードで実装できていると思います。

Scala.jsやReactを用いることにより、コンポーネントベースのシングルページウェブアプリケーションが従来に比べて容易に開発できるようになってきたのではないでしょうか。