Knockout.jsをRailsプロジェクトに入れてみた

JSのMV*は何がいいのか論争は最近収束気味な気はしますが、あんまり触ることができていなかったので、きよくらさんの話を聞いて興味を持っていたKnockout.jsを、仕事で作っているプロダクトに導入してみました。

AngularJSは途中から導入するには難しいですし、BackboneJSはなんか面倒臭かった記憶がありました。結局jQueryで頑張ろうと思えばなんとかなるレベルなので、とりあえずjQueryでDOMと戦っていましたが、面倒臭さが勝ち始めてきました。Knockout.jsは、MVVMとしてシンプルな機能を提供してくれているので、途中から導入できそうでした。

ちなみにKnockout.jsとjQueryを合わせて使っているので、プロジェクト内からjQueryを撲滅するとかそういうことはありません。相変わらずjQueryは便利に使っています。

jQueryのみで作ってDOMを掘っていく作業の辛いところは、変更に弱いことと、頭の中でAjaxで取得するであろうhtmlを想像しながら書いていくことです。まぁ普通だと思ってやっていたので、苦行か?と言われたら慣れの問題なのかもしれません。

Knockout.jsでデータバインディングを行うと、今までAjaxで取得して突っ込んでいたものをその場に書いておくことができるので、見通しがよくなりました。

適当なサンプルを書いてみます。

Before

サンプルコードとして想像で書いているので、文法的に間違っている部分があるかもしれません(検証してません)

<div id="result">
  <!-- ここに何が入ってくるかは他のViewを見ないとわからない -->
</div>

よくある、「この#resultなにが入るねん」問題。

<ul id="todo_list">
  <% @todos.each do |todo| %>
    <li>
      <span class="<%= todo.finish ? 'finish' : 'active' %>"><%= todo.title %></span>
      <button type="button" class="btn_finish">完了</button>
      <button type="button" class="btn_delete">削除</button>
    </li>
  <% end%>
</ul>

これがAjaxで取得されるhtmlです。
spanのclassをTodoモデルのfinishを見て出力しています。
また、buttonが2つありますが、ここではボタンが何をするのかわかりません。class名でだいたいの当たりはつけられますが…。

$ ->
  $("#result").load '/foo'
  $(document).on 'click', '.btn_delete', ->
    $(this).parent().remove()
  $(document).on 'click', '.btn_finish', ->
    title = $(this).prev()
    if $(title).hasClass("active")
      $(title).removeClass("active").addClass("finish")
    else
      $(title).removeClass("finish").addClass("active")

Beforeの場合、どういう要素がAjaxで追加されるのかわからないため、わりかし$(document).onが連発で定義されます。また、ToDoの完了時に見た目を調整するためにクラスをつけたりはずしたりするのに、状態チェックとしてクラスを持っているかどうかを確認しています。また、タイトルの見た目を変えたいので、押されたボタンの兄弟要素を取得するために$(this).prev()とかしていますが、要素の場所が変わったら動かなくなります。
削除する場合も、押されたボタンの親に当たるliタグを見つけてDOMを削除ということをしています。DOMを駆け上がらないといけません…。

そりゃあんたのコーディングの仕方が悪いだけだ、みたいな意見もあるかもしれませんがわりかしこういうのはよくあるパターンじゃないかと思って書いています。

After

次はKnockout.jsを使った場合です。
サーバサイドからは基本的にJSONのみ返します。

<div id="result">
  <ul data-bind="foreach: list">
    <li>
      <span data-bind="text: title, css: isFinish"></span>
      <button data-bind="click: toggleFinish">完了</button>
      <button data-bind="click: $parent.delete">削除</button>
    </li>
  </ul>
</div>

data-bindと書くのがKnockout.jsのお作法です。
バインドされたViewModelのlistが展開されてリストが作成されます。#resultの中になにが入るのか一目瞭然です(本当はこの#resultもいらないけれど、比較のために残してます)。また、クリックイベントについてもこちらに書いておけますので、押されたら何が起きるのかがだいたいわかります。また、JSで操作するための余計なidやclassは一切なくなりました。

json.array! @todos do |todo|
  json.extract! todo, :title, :finish
end

これはJBuilderでTodoクラスの配列をJSON化してます。
データだけなのでシンプルですね。

class TodoViewModel
  constructor: ->
    @list = ko.observableArray()

  delete: (todo) ->
    @list.remove(todo)

class Todo
  constructor: (title, finish) ->
    @title = title
    @finish = ko.observable(finish)
    @isFinish = ko.computed ->
      if @finish()
        'finish'
      else
        'active'
    , this

  toggleFinish: ->
    @finish(!@finish())

$ ->
  todo_view_model = new TodoViewModel()
  ko.applyBindings(todo_view_model)

  $.getJSON '/foo.json', (data) ->
    data.forEach (datum)->
      todo_view_model.list.push(new Todo(datum.title, datum,finish))

クライアントサイドに色々実装するので、こちらは長くなります。
まずは、TodoViewModelクラスを定義しています。これはTodoリスト自体を管理するためのViewModelです。@listにはTodoクラスのインスタンスが入れられていきます。

Todoの1つ1つの動きはTodoクラスに定義されています。
spanタグにどういうcssを当てるのかも、Todoインスタンスの@finishの状態を見て勝手に書き換えてくれます(@isFinishが担当)。Dom探査が必要ありません。
削除ボタンを押された場合は、$parent.deleteを実行しています。この場合、$parentはTodoViewModelになります。なので、TodoViewModelのdeleteメソッドが呼ばれますが、順番を意識せずに@list.remove(todo)でちゃんと削除したい要素が消されます。削除されたらもちろん画面上からも削除されます。

※なお、ko.mapping.fromJSを使っていないのはわざとです。わかりやすくするため。

感想

jQueryのみでやっていた場合は、View側でもclassを意識して書いて、JSでもクラスを意識して書いて…という感じで、画面をガチャガチャ弄っていたために、片方で定義し忘れていてときどきレイアウト崩れが起きるとか、そういうこともあったと思うのですが、Knockout.jsを使うと、かなり見通しがよくなります。オブジェクトの状態が画面に反映されるので、オブジェクトに注力してコーディングできます。

RubyとJS間でのデータの受け渡しがもうちょっと簡単だったらなぁ…とは思いますが、Knockout.jsならば局所的に導入することもできるので、プロジェクト全体を書き直さないといけない!とかそういうこともないですし。DOM探査が辛くなってきたところに対して導入してみたら、綺麗に処理できるようになって嬉しいんじゃないかなと思います。


Spring Bootの練習日記:9日目

今回はBuilding a Hypermedia-Driven RESTful Web Serviceをやってみようと思う。

Hypermediaってどういうことやろうかと思っていたんだけれども、どうもJSONで色々返すことのようだ。

hypermedia-driven RESTは、Spring HATEOASというライブラリを使うらしい。これを使うとSpring MVCのコントローラーとリソースを簡単に設定できる(?)ようである。

まずはbuild.gradleを編集する

dependencies {
//    compile("org.springframework.boot:spring-boot-starter-web:1.2.2.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-actuator:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.2.2.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-security:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-rest:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:1.2.2.RELEASE")
    compile("com.h2database:h2:1.4.183")

    compile("org.webjars:jquery:2.1.3")
    testCompile("org.springframework.boot:spring-boot-starter-test:1.2.2.RELEASE")

    compile("com.fasterxml.jackson.core:jackson-databind")
    compile("org.springframework.hateoas:spring-hateoas")
    compile("org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE")
    compile("com.jayway.jsonpath:json-path:0.9.1")
}

gradle buildDependentsを実行しておく。

リソースにアクセスするためのクラスを作成

ここでは/greetingにアクセスしたらJSONを返すクラスを作るようなのだが、Greetingクラスを作ると前に作ったものとバッティングしそうなので、違うクラスを作成してみる。

ResourceSupportクラスを継承したら、Spring HATEOASに則ったモデルクラスが作れるらしい(適当に読んでいるので違う気もする)。あとはシンプルなPOJOを作るだけ。コンストラクタで代入とgetterを作ってねーと書いてある。

ちなみに私は未だにPOJOってのがなんなのかわかっていないのでついでに調べてみる。

Wikipedia: POJOとは

シンプルなJavaのオブジェクトのことを指すらしい…。なんだろう、通常のクラスが色々脚色されすぎたせいで、普通のクラスが普通という感じがしなくなったからプレーンなやつのことをわざわざPOJOと呼ぶのか。

で、サンプルコードを参考に作ったのが以下のコードとなる。

package sample;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.ResourceSupport;

public class Happy extends ResourceSupport {
    private final String content;

    @JsonCreator
    public Happy(@JsonProperty("content") String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

アノテーションが付いてるんだけれども、これもPOJOなんか?どこからがPOJOでどこからばPOJOでないのかがわからない。@JsonCreaterと@JsonPropertyがあると、Jacksonでインスタンスを生成するときに捗る、みたいに書いてある。こうしておくとJacksonがHappyクラスを自動でJSONにしてくれるらしい。

モデルはできたので次はコントローラーを作ってみよう。

HappyControllerを作ってみる

あとはHappyモデルを返すHappyControllerを作ってみる。
HappyControllerは@Controllerアノテーションを使うことと、@RequestMappingアノテーション、@ResponseBodyアノテーションを使う。

package sample;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

@Controller
public class HappyController {
    private static final String TEMPLATE = "Hello, %s";

    @RequestMapping("/happy")
    @ResponseBody
    public HttpEntity<Happy> happy(
        @RequestParam(value = "name", required = false, defaultValue = "Happy") String name
    ) {
        Happy happy = new Happy(String.format(TEMPLATE, name));
        happy.add(linkTo(methodOn(HappyController.class).happy(name)).withSelfRel());

        return new ResponseEntity<Happy>(happy, HttpStatus.OK);
    }
}

linkToメソッド、methodOnメソッド、withSelfRelメソッドなどが、ここの面白いところ(らしい)。HappyモデルはResourceSupportを継承しているので、addメソッドでlinkを追加できる。引数として、Linkクラスのインスタンスが必要なのだが、それをControllerLinkBuilderのメソッドで作っているらしい。

アプリケーションを起動する

ここまできたらあとは起動するだけ。いつものやつを打つ。

gradle bootRun

これで、http://localhost:8080/happy にアクセスしたら、JSONを返してくれる。

うーん、できたはできたのだが、Javaはアノテーションを使いまくる感じがするのだが、アノテーションってそんなにみんな覚えていられるものなのだろうか?いまいちお手軽さを感じられないのだが…。慣れのような気はするけれど、なんとなくモヤモヤする。


Spring Bootの練習日記:8日目

今回もデータベースとのやりとりをやってみる。

Accessing JPA Data with RESTをやってみる

まずはbuild.gradleを編集する。

dependencies {
//    compile("org.springframework.boot:spring-boot-starter-web:1.2.2.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-actuator:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-security:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-rest:1.2.2.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:1.2.2.RELEASE")
    compile("com.h2database:h2:1.4.183")

    compile("org.webjars:jquery:2.1.3")
    testCompile("org.springframework.boot:spring-boot-starter-test:1.2.2.RELEASE")
}

Spring Bootのバージョンが1.2.2になったらしいので、全体的に1.2.2にしておいた。

ドメインオブジェクトを作る

Personクラスを定義する。

package sample;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

そしてPerson Repositoryを作成。

package sample;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import java.util.List;

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {
    List<Person> findByLastName(@Param("name") String name);
}

@RepositoryRestResourceアノテーションは、RESTfulなエンドポイントを作成する。ここでは/peopleになるように設定されているみたいだ。

これでgradle bootRunを実行すると、http://localhost:8080/peopleにRESTfulなエンドポイントが出来上がっていた。

実験的に、例であるようにcurlを使ってアクセスしてみる。

# GET
curl http://localhost:8080/people
# POST
curl -i -X POST -H "Content-Type:application/json" -d '{  "firstName" : "Frodo",  "lastName" : "Baggins" }' http://localhost:8080/people
# GET
curl http://localhost:8080/people/1
# PATCH
curl -X PATCH -H "Content-Type:application/json" -d '{ "firstName": "Bilbo Jr." }' http://localhost:8080/people/1
# GET
curl http://localhost:8080/people/search
# GET
curl http://localhost:8080/people/search/findByLastName?name=Baggins

@RepositoryRestResourceアノテーションを設定するだけでここまでやってくれるのはすごいなー。

次はHypermedia Driven REST web serviceをやってみようと思う。

というかそろそろなんか作ってみたほうがいいのかもしれない…。


Spring Boot練習日記:7日目

今回はデータベースとのデータのやり取りについてやっていきたい。

Accessing Data with JPAをやってみる

Javaでデータベースを扱う方法でやったことがあるのはJDBCとOrmLiteくらいだったので、JPAは多分初めて使う。

まずはbuild.gradleを変更する。

dependencies {
//    compile("org.springframework.boot:spring-boot-starter-web:1.2.1.RELEASE")
//    compile("org.springframework.boot:spring-boot-starter-actuator:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-security:1.2.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-data-jpa:1.2.1.RELEASE")
    compile("com.h2database:h2:1.4.183")

    compile("org.webjars:jquery:2.1.3")
    testCompile("org.springframework.boot:spring-boot-starter-test:1.2.1.RELEASE")
}

H2はJavaでできたデータベースだ。Javaでデータベースを試すのであればこれがお手軽のようだ。

シンプルなEntityクラスを作る

今回はJPAのEntityアノテーションを使ったシンプルなEntityクラスを作るようだ。

package sample;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer(){}

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format(
                "Customer[id=%d, firstName='%s', lastName='%s']",
                id, firstName, lastName
        );
    }
}

protectedなコンストラクタは直接使うことはしない。そしてpublicなコンストラクタはデータを保存するときに使う。@Tableアノテーションがない場合はCustomerテーブルがあると仮定して動くようだ。
そして、プライマリキーに@Idをつけ、@GeneratedValueアノテーションのstrategyにGenerationType.AUTOを設定することでインクリメントされていくということだろう。

Spring Data JPA

Spring Data JPAはJPAを使ってデータベースにアクセスするためのものだ。interfaceを定義しておいたら、継承したCrudRepositoryが様々な処理を追加してくれるようである。独自に定義したい場合もルールに則ったメソッドを定義しておいたら、おそらくメタプログラミングしてくれるのだろう。

package sample;

import org.springframework.data.repository.CrudRepository;

import java.util.List;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

通常のJavaアプリケーションであれば、CustomerRepositoryクラスを定義するところであろうが、Spring Data JPAはパワフルなのでそんな必要はない、と書いてあった。さぁ!早速試してみようぜ!と書いてあるので試してみよう。

Applicationクラスにrunメソッドを

今回は画面上にではなくて、システムログに出すだけのようだ。
CommandLineRunnerインターフェースを入れたApplicationクラスにするようだ。

package sample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.Arrays;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    CustomerRepository repository;

    public static void main(String[] args) {
        // 略
    }


    @Override
    public void run(String... args) throws Exception {
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // fetch all customers
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // fetch customers by last name
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }
}

CustomerRepositoryインターフェースがSpring Application Contextによって生成されるようなので、newとか別にしなくてもいいらしい。saveメソッドにCustomerインスタンスを渡すと保存されるし、findAllメソッドで全部のデータを持ってこれるし、findOneメソッドでidを指定すれば1件だけ取得することもできる。自分でメソッド名のみ定義したfindByLastNameメソッドも作られていて利用できる。うーむ、すごい。

whereとかはJPAのドキュメントを読めってことだと思うんだけれど、とりあえずここまででサンプルは終わっていて、RESTで使ってみるのは?へのリンクがあったので、次回はそれをやってみようと思う。


3月の目標

先月のふりかえり

wasabiを使ってサンプル的にToDoアプリを作る(×)
できなかった。というかwasabiのバグを踏んでしまい、お手上げとなった。全体的にJVM、Gradle、Java周辺の知識が浅すぎるのにいきなり物をつくろうとしたことが問題だったと思うので、地に足をつけて学習していこうと思って今はSpring Bootの練習をしている。
家に関する雑務を済ませる(すまい給付金の申請、住宅ローン控除、公共料金引き落とし設定など)(◯)
やった。住まい給付金はアベノミクスの影響で投資での儲けが出ていたせいで、所得が増えててもらえる額が減ったのが痛かった…。しかも投資での損金が発生しているのでなおさら痛く感じた。でもまだもらえるだけいいのかなと。手間はかかったが…。住宅ローン控除や、太陽光発電設備の減価償却について、税務署の人に質問できたのがよかった。
確定申告を済ませる(◯)
無事に2月に終わらせることができたので、3月は悠々と過ごせる。締め切りに追われなくて済む。
知識創造企業を読み終わる(×)
よ、読めてません…。
自転車の整備を行う(BD-1)(×)
できてないです…。

トレーニングは継続しているので、体力は多少もどってきたんじゃないだろうか?

今月の目標

  • Spring Boot練習日記を続ける
  • 自転車トレーニングを続ける
  • Railsアプリのリリースする
  • 第二回パトランドお披露目会を開催するか予定を立てる
  • 自転車の整備を行う(BD-1)
  • 知識創造企業を読み終わる