読者です 読者をやめる 読者になる 読者になる

yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

TypeScriptを使ったBackbone.jsアプリケーションの書き方

先日TypeScript使ってBackbone.jsのアプリケーションを書いて、いろいろハマったので備忘録的に書いておきます。

はじめに

既にTypeScript+Backbone.jsのサンプルはいくつか上がってたりします。

有名なのはTypeScript+Backbone.jsでTodoアプリケーションを作るやつ。
tastejs/todomvc

非常に簡単そうなのですが、型定義を独自に書いてあります。

個人的には、型定義はDefinitelyTypedでホストされているやつを使いたく、上述したサンプルコードではあまり参考にならなかったりします。

あとどうせならコンパイルオプションの--module amdを試したかったので、RequireJSを使ってモジュール管理をしようと思います。
(browserifyの使い方ちゃんとわかってない)

エディタについて

いつもはSublime2(or3) Textを使ってコード書いてるんですが、TypeScriptを書くときはWebStormを使うようになりました。
WebStorm - The smartest JavaScript IDE

理由はあまり設定しなくてもTypeScriptをちゃんと書ける、からです。
型定義を使った補完、自動コンパイル、ターミナル起動など、IDEとして申し分ない機能を発揮してます。

デフォルトのキーバインドが全然慣れなくて、最初は自分好みにキーバインドを設定するところから始めたほうがいいです。

あとWebStormは有料ソフトウェアなのですが、1ヶ月体験できるのでとりあえず触ってみるとよいです。
(WinであればVisual Stadio一択でしょう)

これからも継続してTypeScriptを書くようならPersonal License($49)を購入しようと思います。

TypeScript+Backbone.js

作ったサンプルはこちらにおいてあります。

DefinitelyTypedから型定義ファイルをインストールする

手動でインストールするのは面倒なので、tsdコマンドを使ってインストールしましょう。

tsdはnpmでインストールすることができます。

$ npm install -g tsd # グローバルに入れる

インストールできたらtsdと何もオプションを指定せずに実行してみてください。ヘルプっぽいのが表示されます。

tsdをインストールしたらまずはtsd.jsonを作っておきましょう。
このファイルを複数人で共有しておけば、プロジェクト内で型定義ファイルの共有が楽になるでしょう。

$ tsd init # 適当にエンター押すとtsd.jsonが生成される

では型定義ファイルをダウンロードしてきます。
Backbone.jsの型定義ファイルのインストールの仕方はこれ。

$ tsd query backbone  # まずは検索してみる

>> tsd 0.5.7

 - backbone/backbone.d.ts : <head> : 2014-07-06 08:11

$ tsd query backbone --action install --resolve --save # インストールする

>> tsd 0.5.7

 - backbone/backbone.d.ts : <head> : 2014-07-06 08:11

   >> jquery/jquery.d.ts : <head> : 2014-07-06 08:11
   >> underscore/underscore.d.ts : <head> : 2014-07-06 08:11

>> running install..

>> written 3 files:

    - backbone/backbone.d.ts
    - jquery/jquery.d.ts
    - underscore/underscore.d.ts

--resolve付けると依存している型定義ファイルも一緒に落ちてくるので、常に付けておくのがおすすめ。

インストールされたファイルはtypings/ディレクトリ以下に保存されます。また、typings/tsd.d.tsに全ての型定義ファイルへの参照が記述されているので、TypeScriptファイルを作るときはファイルの先頭でこのtsd.d.tsを参照しておくと良いです。

それにしても型定義ファイルをダウンロードするフォーマットが独特ですね。個人的にはnpmっぽく使えると良かったなーと思います。

TypeScript+RequireJS

まずRequireJSの使い方。 下記のサイトが大変参考になりました。
TypeScript AMD with RequireJS Tutorial

エントリーポイントとなるファイルをapp.tsとして用意しました。
(それと、事前にrequirejsの型定義ファイルをインストールしてください)

中身はこんな感じで、require.config()を実行しつつ、Backbone.Routerの初期化をしています。

/// <reference path="./typings/tsd.d.ts" />

require.config({
    paths: {
        jquery: 'bower_components/jquery/dist/jquery.min',
        underscore: 'bower_components/underscore/underscore',
        backbone: 'bower_components/backbone/backbone',
        handlebars: 'bower_components/handlebars/handlebars',

        // plugins
        hbs: 'bower_components/require-handlebars-plugin/hbs',

        // shortcuts
        conf: 'src/conf',
        core: 'src/core',
        modules: 'src/modules'
    },

    shim: {
        jquery: {
            exports: '$'
        },
        underscore: {
            exports: '_'
        }
    }
});

require([
    'jquery',
    'backbone',
    './src/core/router'
], ($, Backbone, Router) => {

    var router = new Router.Router();
    Backbone.history.start();
});

このファイルだけ特殊で、TypeScriptのimportとか、requireとか使っていません。
require.configの実行タイミングがおかしくなってしまったので、とりあえずapp.tsだけは普通のJSっぽい書き方になってしまってます。

Backbone.Model

それではTypeScriptでBackbone.jsアプリケーションを作る方法を見ていきます。

既存のライブラリをTypeScriptで使うには、まず型定義ファイルに目を通す必要があります。今までの書き方とは異なっている場合が多いので、型定義ファイルを落としたら一読しておくとよいです。

backbone.d.tsのBackbone.Modelのextendメソッドの項を抜粋します。

class Model extends ModelBase {
    /**
     * Do not use, prefer TypeScript's extend functionality.
     **/
    private static extend(properties: any, classProperties?: any): any;

コメントにあるように、Backbone.extendを使うことは推奨されていません。TypeScriptのextendキーワードを使って継承を行うことが推奨されています。

Backbone.ModelをTypeScriptで使うときはこんな感じになります。

src/core/mvcbase/model.tsとして保存しました。

/// <reference path="../../../typings/tsd.d.ts" />

import Backbone = require('backbone');

export class CoreModel extends Backbone.Model {
    constructor(options?) {
        super(options);
    }
}

これを--module amdコンパイルすると次のようなファイルが生成されます。

var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
define(["require", "exports", 'backbone'], function(require, exports, Backbone) {
    var CoreModel = (function (_super) {
        __extends(CoreModel, _super);
        function CoreModel(options) {
            _super.call(this, options);
        }
        return CoreModel;
    })(Backbone.Model);
    exports.CoreModel = CoreModel;
});

importしたモジュールがdefineの項に追加されています。
また、exportすると参照が外出しされるようになります。

Backbone.Collection

下記の内容をsrc/core/mvcbase/collection.tsとして保存しました。

/// <reference path="../../../typings/tsd.d.ts" />

import Backbone = require('backbone');
import CoreModel = require('./model');

export class CoreCollection<TModel extends CoreModel.CoreModel> extends Backbone.Collection<Backbone.Model> {
    constructor(options?) {
        super(options);
    }
}

classの定義部にジェネレータが使われています。

JSであればBackbone.Collection.extend({})でコレクションを作れましたが、TypeScriptだとやや記述が多くなってます。

型定義ファイルを見てみると、

class Collection<TModel extends Model> extends ModelBase {

という風に、Collectionクラスにジェネレータ記法で、Backbone.Modelを継承したModelを指定する必要があるようです。

したがってCollectionを定義するときは

class Collection extends Backbone.Collection<Backbone.Model>

といった記述の仕方をする必要があります。
型パラメータを省略するとコンパイルエラーになります。

Backbone.View

JSでBackbone.Viewを作成するときは
Backbone.View.extend({})で作成することができました。

Backbone.Viewの型定義ファイルを見てみると、

class View<TModel extends Model> extends Events {

とあるので、Collectionと同様に型パラメータを渡す必要があります。

型パラメータに渡す型は、Backbone.Modelを継承している必要があります。

src/core/mvcbase/view.tsというファイルを下記の内容で作成しました。

/// <reference path="../../../typings/tsd.d.ts" />

import Backbone = require('backbone');

export class CoreView extends Backbone.View<Backbone.Model> {
    constructor(options?) {
        super(options);
    }
}

Backbone.Router

JSであれば、Backbone.Routerのroutesプロパティはオブジェクト型またはオブジェクトを返却する関数を値として設定できましたが、下記の通り型定義ファイルにはメソッドの記述のみされています。

class Router extends Events {
    /**
    * Routes hash or a method returning the routes hash that maps URLs with parameters to methods on your Router.
    * For assigning routes as object hash, do it like this: this.routes = <any>{ "route": callback, ... };
    * That works only if you set it in the constructor or the initialize method.
    **/
    routes(): any;

そしてコメントで補足されていますが、もしroutesにオブジェクトをセットしたい場合は、constructorの中で次のような記述をすれば良いようです。

class Router extends Backbone.Router {
    'use strict';
    constructor(options?) {
        this.routes = <any>{
            'route' : callback
        };
        super(options);
    }
}

また、ハッシュとコールバックの組からなるオブジェクトを返却する関数を設定しても動くので、私はこちらの方法を使いました。

前に書いたBackbone.jsに入門してみる【Router編】の記述を参考にして作ったので、こちらを参考にしてみてください。

RequireJSプラグイン

たとえばRequireJSプラグインrequire-handlebars-pluginを使ってテンプレートを読み込む場合は次のような記述をします。

/// <amd-dependency path="hbs!src/modules/user/hbs/user" />

var hbsUserList = require('hbs!src/modules/user/hbs/user');

1行目のリファレンスコメントを記述することで、依存していることを明示的に表しています。
次の行ではテンプレートへの参照を取得しているのですが、パスの書き方などはJSで扱うときの書き方と同じです。

サンプルでいうと、ここらへんでrequire-handlebars-pluginを使っているので参考にしてみてください。

おわりに

TypeScriptを使った開発で大変だったことといえば、コンパイルエラーが発生した時にエラーメッセージから原因を特定する作業です。
(debugger仕込みたい!みたいな)

一度に複数個のコンパイルエラーが発生すると、一つを直してもまだエラーが消えず精神的にきます。
原因はインターフェースの使い方とか、型定義ファイルをちゃんと読んでないことが多かったです。

ちょっと慣れるとTypeScriptやっぱりいいなーと思うようになりました。
コンパイル時に整合性をチェックできることで、機能を追加する時に既存の仕組みを壊していないか確認できて好印象でした。