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