Kazitori.jsをTypeScriptで使ってみたら、ちょっとおかしなハマり方をしたのでメモしてみます。
そろそろ pushState/popState を始めてみようかなぁと思ってたのですが、以下の点でKazitori.jsが気に入ったので、試しに使ってみました。
- jQueryとかBackbone.jsなどのライブラリに依存しない。
- Ajax通信やViewの変更等とセットになってない。イベントが発生するだけ。
要は、pushState/popStateだけを単独で使いたいなと思った訳です。
処理は自分で勝手にやるよ、と。
なんですが、 Kazitori.jsはCoffeeScript製でTypeScriptには対応していません。
Haxeには対応しているのに!
せっかく良い感じそうなライブラリがあるのに、手持ちのTypeScript環境で使えないのは何か悔しいので、Kazitori.jsの概要をドキュメント見て眺めつつ、 d.tsファイルを作ってみました。
とりあえず、全文べた張り。
declare class Kazitori {
new (): Kazitori;
beforeAnytimeHandler: Array<any>;
befores: Object;
fragment: string;
isSuspend: boolean;
lastFragment: string;
notFound: string;
root: string;
rootFiles: Array<any>;
routes: Object;
silent: boolean;
started: boolean;
appendRouter(child: Object, childRoot: string):void;
beforeFailedHandler():void;
change(fragment: string, options: Object):void;
extractParams(rule: Rule, fragment: string, test: boolean): void;
getFragment(fragment:string): void;
getHash(): string;
loadURL(fragmentOverride:string, options:Object):void;
match(fragment: string): boolean;
omokazi(options: Object): void;
reject():void;
removeRouter(child: Object, childRoot: string);
replace(fragment: string, options: Object): void;
resume(): void;
start(options: Object): void;
stop(): void;
suspend(): void;
torikazi(options:Object):void;
}
declare class Rule {
new (rule: string, callback: Function, router: Kazitori): Rule;
callback: Function;
rule: string;
test(fragment): boolean;
update(path: string): void;
}
declare class EventDispatcher {
}
declare class KazitoriEvent {
new (type: string, next: string, prev: string): KazitoriEvent;
ADDED: string;
BEFORE_EXECUTED: string;
CHANGE: string;
EXECUTED: string;
FIRST_REQUEST: string;
INTERNAL_CHANGE: string;
NEXT: string;
NOT_FOUND: string;
PREV: string;
REJECT: string;
REMOVED: string;
RESUME: string;
START: string;
STOP: string;
SUSPEND: string;
}
ある程度、動作確認が出来たら、後ほどGitHubに上げようかなぁと思っています。
で、本題です。
d.tsを先に作った後、TypeScriptで検証を始めてみました。
とりあえず、公式サイトにある以下のCoffeeScriptのコードをそのまま移植。
class Router extends Kazitori
routes:
"/": "index"
"/<int:id>": "show"
index:()->
console.log "index!"
show:(id)->
console.log id
$(()->
app = new Router()
)
TypeScriptにするとこんな感じです。
/// <reference path="../dts/kazitori.d.ts" />
class Router extends Kazitori {
public routes: Object = {
"/": "index",
"/<int:id>": "show"
}
public index(): void {
console.log("index!");
}
public show(id: number): void {
console.log(id);
}
}
window.onload = () => {
var app: Router = new Router();
};
d.tsファイルを参照して型チェックを効かしてから、後はクラス構造をそのまま移植しています。
が、しかし、動かない。。。
色々調べてみると、 どうやら routes
インスタンス変数に設定したはずの値がnull になっていました。
原因を探るため、CoffeeScriptとTypeScriptでそれぞれ吐き出したJavaScriptを比較してみます。
※ routes
インスタンス変数に関わる所だけ抜粋しています。
まずは、TypeScriptからの変換。
var Router = (function (_super) {
__extends(Router, _super); // 上の方で__extendsが宣言されてる
function Router() {
_super.apply(this, arguments);
this.routes = {
"/": "index",
"/<int:id>": "show"
};
}
...
})(Kazitori);
続いて、CofeeScriptから変換。
Router.prototype.routes = {
"/": "index",
"/<int:id>": "show",
};
なるほど。
TypeScriptの場合は、 コンストラクタの中でインスタンス変数の初期値を設定 しています。
代わってCoffeeScriptの場合は、 変数に値が設定されるまでは prototype
に仕込んでおいた値を参照するようにして初期値を設定 している感じでしょうか。
こうしてみると、CofeeScriptの場合はインスタンス化が実行されるよりだいぶ前に初期値が設定される事になりますね。
Kazitori.jsの実装を細かく見てないので詳しくは分かりませんが、おそらくはKazitoriクラスを継承した子クラスで routes
インスタンス変数を設定しておいて、親クラス(Kazitori)のコンストラクタ内でその値を取得しているのだと思います。
しかし、そこの取得が上手くいってないっぽいですね。
CoffeeScriptとTypeScriptのクラス実装の違いが問題となっているのでしょうか。
普通に考えたら、 CoffeeScriptのクラスをTypeScriptのクラスで継承しようっていうのがおかしな話 なのですが(笑)。
で、ちょっと根が深そうだったので、解決策はこんな感じにしてみました。
/// <reference path="../dts/kazitori.d.ts" />
class Router extends Kazitori {
public index(): void {
console.log("index!");
}
public show(id: number): void {
console.log(id);
}
}
Router.prototype.routes = {
"/": "index",
"/<int:id>": "show",
};
window.onload = () => {
var app: Router = new Router();
};
TypeScriptの中に生のJavaScriptをそのまま書く。
これで、めでたく上手く動きました。
こうやって、応急処置でJavaScriptを書けちゃう辺りがTypeScriptの強み(?)でしょうか。
おしまい。