前置き
Titanium Mobile + Alloy では、ModelをBackbone.jsを使って書きます。
などと言われているようだが、実際にはBackbone.jsそのままというわけではないようだ。
そんなわけで、ちょっと調べてみた。
ちなみにここでは、Todoリストアプリっぽいものを書いていると思ってください。
モデルの書き方
Titanium Mobile + Alloy の新しいプロジェクトを作成すると、appフォルダ以下に、controllers, models, views など、いかにもMVCなんです的なフォルダ構成になっている。
この中の、modelsフォルダに「なんちゃら.js」というファイル名でモデルを書いてやると、
Titanium Mobileが自動的にBackbone.jsを使ってくれる。
強制である。
使わないという選択肢は(おそらく)存在しない。
ためしに、ネットでサンプルを読みつつ、Todoリストを扱うモデルを書いてみた。
exports.definition = {
config: {
"columns": {
"description": "TEXT",
"limitTime": "TEXT",
"done": "INTEGER"
},
"defaults": {
"description": "-",
"limitTime": "",
"done": "false"
},
"adapter": {
"type": "sql",
"collection_name": "todos",
"idAttribute": "id"
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {
// Extend Backbone.Model
});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
// extended functions and properties go here
});
return Collection;
}
}
とりあえず、Backbone.jsとUnderscore.jsは、とくにrequire()とかしなくても自動的に読み込んでくれる。
モデルはどのように変換されているのか?
ここで疑問。
exports.definition って何?
Backbone.jsにはないよね?
気になったので、どんな風に変換されるのか追ってみた。
iPhoneシミュレータ用に"Debug"あるいは"Run"すると、
(ホームディレクトリ)/Library/Application Support/iPhone Simulator/(バージョン)/Applications/
ディレクトリ以下に、数字とアルファベットが羅列された名前のディレクトリが出来る。
その中のどこかのフォルダに(この場合は)Todo.appというファイルが見つかる。
で、Todo.appを右クリック->「パッケージの内容を表示」で、中身を見てみる。
パッケージの中で重要なのは、alloyフォルダっぽい。
alloyフォルダ以下には、controllers, models フォルダがある。
どうやら、alloy/modelsフォルダにあるTodo.jsというのが、上で書いたtodo.jsが変換されたもののようだ。
その中身は以下。
exports.definition = {
config: {
columns: {
description: "TEXT",
limitTime: "TEXT",
done: "INTEGER"
},
defaults: {
description: "-",
limitTime: "-",
done: "false"
},
adapter: {
type: "sql",
collection_name: "todos",
idAttribute: "id"
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {});
return Collection;
}
};
var Alloy = require("alloy"), _ = require("alloy/underscore")._, model, collection;
model = Alloy.M("todo", exports.definition, []);
collection = Alloy.C("todo", exports.definition, model);
exports.Model = model;
exports.Collection = collection;
最後の5行以外は書いたままなので、最後だけ見ていく。
var Alloy = require("alloy"), _ = require("alloy/underscore")._, model, collection;
model = Alloy.M("todo", exports.definition, []);
collection = Alloy.C("todo", exports.definition, model);
exports.Model = model;
exports.Collection = collection;
1行目は、alloyやunderscoreを読み込んでいるだけなので無視。
4〜5行目は、exportsしてるだけなので無視。
2〜3行目で、exports.definitionで定義したものを、Alloy.M()やらAlloy.C()という名前の関数に渡しているのが怪しい。
Alloyなんちゃらは、パッケージを開いたルートディレクトリにあるalloy.jsで定義されている。
M()とC()の定義だけ抜き出してみる。
どうでもいいが、MはModel、CはCollectionの頭文字と思われる。
exports.M = function(name, modelDesc, migrations) {
var config = modelDesc.config;
var type = (config.adapter ? config.adapter.type : null) || "localDefault";
"localDefault" === type && (type = "sql");
var adapter = require("alloy/sync/" + type);
var extendObj = {
defaults: config.defaults,
sync: function(method, model, opts) {
var config = model.config || {};
var type = (config.adapter ? config.adapter.type : null) || "localDefault";
"localDefault" === type && (type = "sql");
require("alloy/sync/" + type).sync(method, model, opts);
}
};
var extendClass = {};
migrations && (extendClass.migrations = migrations);
_.isFunction(adapter.beforeModelCreate) && (config = adapter.beforeModelCreate(config, name) || config);
var Model = Backbone.Model.extend(extendObj, extendClass);
Model.prototype.config = config;
_.isFunction(modelDesc.extendModel) && (Model = modelDesc.extendModel(Model) || Model);
_.isFunction(adapter.afterModelCreate) && adapter.afterModelCreate(Model, name);
return Model;
};
exports.C = function(name, modelDesc, model) {
var extendObj = {
model: model,
sync: function(method, model, opts) {
var config = model.config || {};
var type = (config.adapter ? config.adapter.type : null) || "localDefault";
"localDefault" === type && (type = "sql");
require("alloy/sync/" + type).sync(method, model, opts);
}
};
var Collection = Backbone.Collection.extend(extendObj);
var config = Collection.prototype.config = model.prototype.config;
var type = (config.adapter ? config.adapter.type : null) || "localDefault";
var adapter = require("alloy/sync/" + type);
_.isFunction(adapter.afterCollectionCreate) && adapter.afterCollectionCreate(Collection);
_.isFunction(modelDesc.extendCollection) && (Collection = modelDesc.extendCollection(Collection) || Collection);
return Collection;
};
さきに、M()のほうを見ていく。
2番目の引数modelDescに、exports.definitionが入っているのを頭に入れておく。
そうすると、2〜3行目の
var config = modelDesc.config;
var type = (config.adapter ? config.adapter.type : null) || "localDefault";
は、変数configにexports.definition.configを代入し、
変数typeにexports.definition.adapterを元に求めた値を代入していることになる。
次。
alloy.jsの7行目あたり。
var extendObj = {
defaults: config.defaults,
として、extendObj.defaultsにconfig.defaults(=exports.definition.config.defaults)を代入している。
結構バラバラにされているようです < exports.definition
その後も紆余曲折を経ながら最終的に、18〜19行目
var Model = Backbone.Model.extend(extendObj, extendClass);
Model.prototype.config = config;
で、Backbone.Modelに結びつけられている。
結局、extendObj.defautlsに結びつけられたexports.definition.defaultsが
Backbone.Model.extend()に渡され、また、
exports.definition.configが、Model.prototype.configに代入されている。
さらに、extendObj.sync()の中では、config.adapterから求めたtypeが使用されている(typeはシリアライズのとき"sql"を使うのか、"localStorage"を使うのか、みたいなことを表している)。
ということになる。
でこれから何がわかるかというと…Backbone.jsを勉強するべき、ということになる(?)
C()のほうは以下同様で略。
おまけ
それだけでは何なので、ちょっとだけ。
config.defaults
exports.definitionで定義されたconfig.defaultsが、Backbone.Model.Extend()に渡されるので、
exports.definition = {
config: {
"defaults": {
"description": "-",
"limitTime": "",
"done": "false"
}
}
}
と書いておけば、
var Model = Backbone.Model.extend({
"defaults": {
"description": "-",
"limitTime": "",
"done": "false"
}
});
と書いたのと同じことになる。
extendModel
extendModelプロパティに、以下のような記述をすることでモデルを拡張出来る。
exports.definition = {
extendModel: function(Model) {
_.extend(Model.prototype, {
// Implement the validate method
validate: function (attrs) {
for (var key in attrs) {
var value = attrs[key];
if (key === "description") {
if (value.length <= 0) {
return "Error: No description!";
}
}
}
}
});
return Model;
}
}
extendCollection
Backbone.jsを使う場合、Backbone.Modelを拡張したオブジェクトを、Backbone.Collectionを拡張したオブジェクトにいくつもいれておくことがある(ようだ)が、
exports.definitionに、extendModelとextendCollectionを同時に記述することで、
ModelとCollectionを一度に記述できる(ような気がする…)。
すみません。Backbone.jsは勉強不足なので…。
exports.definition = {
extendModel: function(Model) {
_.extend(Model.prototype, {
// こっちはBackbone.Model関係
});
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
// こっちはBackbone.Collection関係
});
return Collection;
}
}