Open h5y1m141 opened 9 years ago
QiitaのWebAPIと連携させるアプリでMoment.jsを使う ですが、サンプル通りにやるとエラーが出てしまいます。momentja.jsにエラーが出ているような…なんかそんなメッセージが出ます。
momentjaを読み込まず、momentのみで実装すれば表示できました。
取り急ぎご報告です。
@kozasaryosuke さん
momentja.jsにエラーが出ているような…なんかそんなメッセージが出ます。 momentjaを読み込まず、momentのみで実装すれば表示できました。
たしかにエラーになってますね・・Moment.jsのバージョンがあがって日本語化対応のほうが追いついてないのかなぁ・・とりあえずはMoment.js使うと便利なことが出来るっていうのを体感してもらいたかったので、momentのみで実装すれば表示できたのことで次に進むことにしましょう
Alloyでの開発について追記しましたのでそちらをチェックして先に進んでください
Titanium + Alloy + napp.alloy.adapter.restapiで作る簡単Qiitaビューワーアプリですが、alloy.jsのせいなのか(???)エラーが出てしまいます。 alloy.jsはデフォルトの状態のままで特に何もしていないのですが、何かコードを書く必要があるのでしょうか。もしかしたら私が何かしらの手順を間違えているのかもしれません。コード貼りますね。
/**
* Rest API Adapter for Titanium Alloy
* @author Mads Møller
* @version 1.1.6
* Copyright Napp ApS
* www.napp.dk
*/
function S4() {
return ((1 + Math.random()) * 65536 | 0).toString(16).substring(1);
}
function guid() {
return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4();
}
function InitAdapter(config) {
return {};
}
function apiCall(_options, _callback) {
if (Ti.Network.online) {
var xhr = Ti.Network.createHTTPClient({
timeout : _options.timeout || 7000
});
//Prepare the request
xhr.open(_options.type, _options.url);
xhr.onload = function() {
var responseJSON, success = (this.status <= 304) ? "ok" : "error", status = true, error;
// save the eTag for future reference
if (_options.eTagEnabled && success) {
setETag(_options.url, xhr.getResponseHeader('ETag'));
}
// we dont want to parse the JSON on a empty response
if (this.status != 304 && this.status != 204) {
// parse JSON
try {
responseJSON = JSON.parse(this.responseText);
} catch (e) {
Ti.API.error('[REST API] apiCall PARSE ERROR: ' + e.message);
Ti.API.error('[REST API] apiCall PARSE ERROR: ' + this.responseText);
status = false;
error = e.message;
}
}
_callback({
success : status,
status : success,
code : this.status,
data : error,
responseText : this.responseText || null,
responseJSON : responseJSON || null
});
cleanup();
};
//Handle error
xhr.onerror = function(e) {
var responseJSON, error;
try {
responseJSON = JSON.parse(this.responseText);
} catch (e) {
error = e.message;
}
_callback({
success : false,
status : "error",
code : this.status,
error : e.error,
data : error,
responseText : this.responseText,
responseJSON : responseJSON || null
});
Ti.API.error('[REST API] apiCall ERROR: ' + this.responseText);
Ti.API.error('[REST API] apiCall ERROR CODE: ' + this.status);
Ti.API.error('[REST API] apiCall ERROR MSG: ' + e.error);
Ti.API.error('[REST API] apiCall ERROR URL: ' + _options.url);
cleanup();
};
// headers
for (var header in _options.headers) {
// use value or function to return value
xhr.setRequestHeader(header, _.isFunction(_options.headers[header]) ? _options.headers[header]() : _options.headers[header]);
}
if (_options.beforeSend) {
_options.beforeSend(xhr);
}
if (_options.eTagEnabled) {
var etag = getETag(_options.url);
etag && xhr.setRequestHeader('IF-NONE-MATCH', etag);
}
if (_options.type != 'GET' && !_.isEmpty(_options.data)) {
xhr.send(_options.data);
} else {
xhr.send();
}
} else {
// we are offline
_callback({
success : false,
status : "offline",
offline : true,
responseText : null
});
}
/**
* Clean up the request
*/
function cleanup() {
xhr = null;
_options = null;
_callback = null;
error = null;
responseJSON = null;
}
}
function Sync(method, model, opts) {
model.idAttribute = model.config.adapter.idAttribute || "id";
// Debug mode
var DEBUG = model.config.debug;
// eTag enabled
var eTagEnabled = model.config.eTagEnabled;
// Used for custom parsing of the response data
var parentNode = model.config.parentNode;
// REST - CRUD
var methodMap = {
'create' : 'POST',
'read' : 'GET',
'update' : 'PUT',
'delete' : 'DELETE'
};
var type = methodMap[method];
var params = _.extend({}, opts);
params.type = type;
//set default headers
params.headers = params.headers || {};
// Send our own custom headers
if (model.config.hasOwnProperty("headers")) {
for (var header in model.config.headers) {
params.headers[header] = model.config.headers[header];
}
}
// We need to ensure that we have a base url.
if (!params.url) {
params.url = (model.config.URL || model.url());
if (!params.url) {
Ti.API.error("[REST API] ERROR: NO BASE URL");
return;
}
}
// Extend the provided url params with those from the model config
if (_.isObject(params.urlparams) || model.config.URLPARAMS) {
_.extend(params.urlparams, _.isFunction(model.config.URLPARAMS) ? model.config.URLPARAMS() : model.config.URLPARAMS);
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (Alloy.Backbone.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.processData = true;
params.data = params.data ? {
model : params.data
} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (Alloy.Backbone.emulateHTTP) {
if (type === 'PUT' || type === 'DELETE') {
if (Alloy.Backbone.emulateJSON)
params.data._method = type;
params.type = 'POST';
params.beforeSend = function(xhr) {
params.headers['X-HTTP-Method-Override'] = type;
};
}
}
//json data transfers
params.headers['Content-Type'] = 'application/json';
logger(DEBUG, "REST METHOD", method);
switch(method) {
case 'create' :
// convert to string for API call
params.data = JSON.stringify(model.toJSON());
logger(DEBUG, "create options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode);
//Rest API should return a new model id.
if (data[model.idAttribute] === undefined) {
//if not - create one
data[model.idAttribute] = guid();
}
params.success(data, JSON.stringify(data));
model.trigger("fetch");
// fire event
} else {
params.error(_response.responseJSON, _response.responseText);
Ti.API.error('[REST API] CREATE ERROR: ');
Ti.API.error(_response);
}
});
break;
case 'read':
if (model[model.idAttribute]) {
params.url = params.url + '/' + model[model.idAttribute];
}
if (params.search) {
// search mode
params.url = params.url + "/search/" + Ti.Network.encodeURIComponent(params.search);
}
if (params.urlparams) {
// build url with parameters
params.url = encodeData(params.urlparams, params.url);
}
if ( ! params.urlparams && params.data) {
// If we have set optional parameters on the request we should use it
// when params.urlparams fails/is empty.
params.url = encodeData(params.data, params.url);
}
if (eTagEnabled) {
params.eTagEnabled = true;
}
logger(DEBUG, "read options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode);
var values = [];
if (!_.isArray(data)) {
data = [data];
}
var length = 0;
for (var i in data) {
var item = {};
item = data[i];
if (item[model.idAttribute] === undefined) {
item[model.idAttribute] = guid();
}
values.push(item);
length++;
}
params.success((length === 1) ? values[0] : values, _response.responseText);
model.trigger("fetch");
} else {
params.error(model, _response.responseText);
Ti.API.error('[REST API] READ ERROR: ');
Ti.API.error(_response);
}
});
break;
case 'update' :
if (!model[model.idAttribute]) {
params.error(null, "MISSING MODEL ID");
Ti.API.error("[REST API] ERROR: MISSING MODEL ID");
return;
}
// setup the url & data
if (_.indexOf(params.url, "?") == -1) {
params.url = params.url + '/' + model[model.idAttribute];
} else {
var str = params.url.split("?");
params.url = str[0] + '/' + model[model.idAttribute] + "?" + str[1];
}
if (params.urlparams) {
params.url = encodeData(params.urlparams, params.url);
}
params.data = JSON.stringify(model.toJSON());
logger(DEBUG, "update options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode);
params.success(data, JSON.stringify(data));
model.trigger("fetch");
} else {
params.error(model, _response.responseText);
Ti.API.error('[REST API] UPDATE ERROR: ');
Ti.API.error(_response);
}
});
break;
case 'delete' :
if (!model[model.idAttribute]) {
params.error(null, "MISSING MODEL ID");
Ti.API.error("[REST API] ERROR: MISSING MODEL ID");
return;
}
//params.url = params.url + '/' + model[model.idAttribute];
if (_.indexOf(params.url, "?") == -1) {
params.url = params.url + '/' + model.id;
} else {
var str = params.url.split("?");
params.url = str[0] + '/' + model.id + "?" + str[1];
}
logger(DEBUG, "delete options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode);
params.success(null, _response.responseText);
model.trigger("fetch");
} else {
params.error(model, _response.responseText);
Ti.API.error('[REST API] DELETE ERROR: ');
Ti.API.error(_response);
}
});
break;
}
}
/////////////////////////////////////////////
// HELPERS
/////////////////////////////////////////////
function logger(DEBUG, message, data) {
if (DEBUG) {
Ti.API.debug("[REST API] " + message);
if (data) {
Ti.API.debug( typeof data === 'object' ? JSON.stringify(data, null, '\t') : data);
}
}
}
function parseJSON(DEBUG, _response, parentNode) {
var data = _response.responseJSON;
if (!_.isUndefined(parentNode)) {
data = _.isFunction(parentNode) ? parentNode(data) : traverseProperties(data, parentNode);
}
logger(DEBUG, "server response", _response);
return data;
}
function traverseProperties(object, string) {
var explodedString = string.split('.');
for ( i = 0, l = explodedString.length; i < l; i++) {
object = object[explodedString[i]];
}
return object;
}
function encodeData(obj, url) {
var str = [];
for (var p in obj) {
str.push(Ti.Network.encodeURIComponent(p) + "=" + Ti.Network.encodeURIComponent(obj[p]));
}
if (_.indexOf(url, "?") == -1) {
return url + "?" + str.join("&");
} else {
return url + "&" + str.join("&");
}
}
/**
* Get the ETag for the given url
* @param {Object} url
*/
function getETag(url) {
var obj = Ti.App.Properties.getObject("NAPP_REST_ADAPTER", {});
var data = obj[url];
return data || null;
}
/**
* Set the ETag for the given url
* @param {Object} url
* @param {Object} eTag
*/
function setETag(url, eTag) {
if (eTag && url) {
var obj = Ti.App.Properties.getObject("NAPP_REST_ADAPTER", {});
obj[url] = eTag;
Ti.App.Properties.setObject("NAPP_REST_ADAPTER", obj);
}
}
//we need underscore
var _ = require("alloy/underscore")._;
//until this issue is fixed: https://jira.appcelerator.org/browse/TIMOB-11752
var Alloy = require("alloy"), Backbone = Alloy.Backbone;
module.exports.sync = Sync;
module.exports.beforeModelCreate = function(config, name) {
config = config || {};
InitAdapter(config);
return config;
};
module.exports.afterModelCreate = function(Model, name) {
Model = Model || {};
Model.prototype.config.Model = Model;
Model.prototype.idAttribute = Model.prototype.config.adapter.idAttribute;
return Model;
};
var qiitaItems, refreshMainMenu;
$.index.open();
$.activityIndicator.show();
qiitaItems = Alloy.createCollection('qiita');
qiitaItems.fetch({
success: function() {
var items, _stringify;
$.activityIndicator.hide();
_stringify = JSON.stringify(qiitaItems);
items = JSON.parse(_stringify);
return refreshMainMenu(items);
},
error: function() {
$.activityIndicator.hide();
return Ti.API.info("error");
}
});
refreshMainMenu = function(items) {
var bodyLabel, item, row, rows, titleLabel, _i, _len;
rows = [];
for (_i = 0, _len = items.length; _i < _len; _i++) {
item = items[_i];
Ti.API.info(item.title);
row = $.UI.create("TableViewRow", {
classes: "itemRow",
data: item
});
titleLabel = $.UI.create("Label", {
text: item.title,
classes: "titleLabel"
});
bodyLabel = $.UI.create("Label", {
text: item.raw_body,
classes: "body"
});
row.add(titleLabel);
row.add(bodyLabel);
rows.push(row);
}
return $.mainMenu.setData(rows);
};
exports.definition = {
config: {
URL: "https://qiita.com/api/v1/items",
adapter: {
type: 'restapi',
collection_name: 'qiita'
},
extendModel: function(Model) {
return _.extend(Model.prototype, function() {
return Model;
});
},
extendCollection: function(Collection) {
return _.extend(Collection.prototype, function() {
return Collection;
});
}
}
};
"#mainWindow":{
statusBarStyle:0,
translucent:false,
navTintColor:"#0066ff",
backgroundColor:"#fcfcfc",
tabBarHidden:true
}
"#mainMenu":{
backgroundColor:"#fcfcfc",
separatorColor: '#cccccc',
width:Ti.UI.FULL,
height:Ti.UI.FULL,
left:0,
top:0,
zIndex:1
}
"#activityIndicator":{
top:"50%",
left:"20%",
textAlign:'center',
backgroundColor:"#222",
font:{
fontSize:18
},
color:'#fff',
zIndex:10
}
".itemRow":{
width:Ti.UI.FULL,
height:"15%",
hasChild:true,
backgroundColor:"#fcfcfc"
}
".titleLabel":{
width:"90%",
height:"20%",
top:"5%",
left:"5%",
textAlign:'left',
color:'#59BB0C',
font:{
fontWeight:'bold',
fontSize:16
}
}
".body":{
width:"90%",
height:"70%",
top:"25%",
left:"5%",
textAlign:'left',
color:'#222',
font:{
fontSize:12
}
}
<Alloy>
<TabGroup>
<Tab id="tabOne">
<Window id="mainWindow" class="container" title="Qiita">
<ActivityIndicator id="activityIndicator" message="Loading.." />
<TableView id="mainMenu" />
</Window>
</Tab>
</TabGroup>
</Alloy>
ちょっと待ってください…自力で解決できるかもしれません。。
すみません。やっぱりダメでした。
[ERROR] : Script Error {
[ERROR] : backtrace = "#0 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy.js:194\n#1 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy.js:170\n#2 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy/controllers/BaseController.js:173\n#3 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy/controllers/index.js:98\n#4 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy/controllers/index.js:83\n#5 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy/backbone.js:760\n#6 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy/sync/restapi.js:162\n#7 () at file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy/sync/restapi.js:43";
[ERROR] : line = 48;
[ERROR] : message = "'undefined' is not an object (evaluating 'copy.colors')";
[ERROR] : name = TypeError;
[ERROR] : sourceId = 302250656;
[ERROR] : sourceURL = "file:///Users/kozasaryousuke/Library/Developer/CoreSimulator/Devices/CA4D0955-551F-4D4B-AB8A-B86A752310BE/data/Containers/Bundle/Application/57B0ED18-F112-43BB-B63B-6F4AA6B14410/restapi.app/alloy.js";
[ERROR] : }
こんな感じのエラーコードが出ています。。
@kozasaryosuke さん、 うーん見る限りでは間違ってる感じしないですね。ひとまず気になったのはqiita.jsの配置場所がどのようになってますか?
Alloyは、決まった箇所に決まったネーミングルールでファイルを配置されてないと処理がうまく行われないようになってます。
なんとなく、qiita.jsは、以下のようにmodelというディレクトリの下に配置しておく必要あります
├── alloy.jmk
├── alloy.js
├── assets
│ ├── alloy
│ │ └── sync
│ │ ├── restapi.js
│ ├── iphone
│ └── mobileweb
├── config.json
├── controllers
│ └── index.js
├── models
│ └── qiita.js
├── styles
│ └── index.tss
└── views
└── index.xml
qiita.jsの位置は間違ってないようです。 alloy.jmkというファイルは(任意)と書いてあったのでいまはない状態ですが、関係ないですよね? (ファイルを作成しても動きませんでした。)
alloy.jmkというファイルは(任意)と書いてあったのでいまはない状態ですが、関係ないですよね?
はい、関係無いですね。alloy.jmkは、設定を色々カスタマイズしたい場合に記述するファイルなので基本的にはこれが無くってもOKです。
あとは、Titaniumというかスマートフォン向けのアプリでよくあるのですが、実際に書いたソースコードは、コンパイルという作業を経て、iPhoneシュミレーター or 実機で利用できるような仕組みになってます。
そのコンパイル作業はとても時間がかかるため、毎回毎回ゼロからコンパイルされるのではなく、以前コンパイルされたもの(= キャッシュ という言い方がされます。おそらく言葉を聞いたことがあるかと思います)はそのまま利用されます。
そのキャッシュファイルが原因でたまに謎のエラーが生じることがあるので、念のためキャッシュクリアーを試してもらえませんか?
キャッシュクリアーは、Titanium Studioのこのメニュー↓のように進むと実行できます
上記と別に、私の手元の環境で先ほど貼っていただいた内容でどのような結果になるのか検証してみますね
@kozasaryosuke さんが貼ってくれたソースコードをそのままコピペしたらこのように意図通り表示されましたね
可能性としては、私のMacの環境はXCode、Titaniumの開発環境とも少しバージョンを古めにしてます。
TitaniumSDK3.3.0というバージョンで、Alloyもバージョンが少し古いのですが、おそらく@kozasaryosuke さんは、3.4.0というバージョンでXCodeも6という最新のものを利用されてるのでそのあたりが要因かなぁ・・
キャッシュクリアーしたのですが、できません〜
このリンク先の人も、最新バージョンにより謎のエラーが出る、みたいなこと言ってますね。もしかしたら関係あるのかも。。
おっしゃる通り、僕の環境は最新バージョンで3.4.1です。
バージョン落としたものをインストールした方が良さそうですよね?
ちなみにAlloyのバージョンは1.5.1です。それが原因かもしれませんね。
@kozasaryosuke さんがリンク貼ってくれたError in Alloy after upgrading from 1.5.0-rc3 to 1.5.1.GAのページはさっき私も見てたのですが、なんかエラー内容とか使ってるSDKのバージョンとかからすると似た症状な気がしますね
バージョン落としたものをインストールした方が良さそうですよね?
たしかに、そうしたほうが良さそうなのですが、これが結構やっかいで
1.の手順でXCodeのバージョン5の名前は、XCode5としたほうが作業やりやすくなります 2.コマンドラインを実行するためのターミナルは以下の場所にあります
ターミナルをダブルクリックしたら、以下を入力します
sudo xcode-select -s /Applications/Xcode5.app/Contents/Developer/
あとは、TitaniumのSDKを3.4.1から3.3.0とかの1つ前のものをダウンロードしてやるのですが、このあたりの作業手順が慣れないとちょっと難しいと感じる所があるので、途中不明な点があれば、都度コメントいただくか、明日の午後8時とかに、ここまでの作業振り返りを兼ねつつ、画面共有などしながら、トラブルシューティングも出来ますので、そのあたりは遠慮なくお知らせください!
ここ二日、課題を進められていませんが、明日は時間が取れるので進められると思います! もう少し時間を割けると踏んでいたのですが。。思うように進まず…ですが、焦らず最速で進められればと思ってますので、宜しくお願いします!
ここ二日、課題を進められていませんが、明日は時間が取れるので進められると思います!
@kozasaryosuke さん、了解です。私が今日は日中、夜とも時間があまり取れ無さそうなのであまりフォロー出来ないかもしれませんが、何か詰まった所が出てきたら、コメントしておいてください。
今日の進み具合見て、来週の課題設定を考えてみますね
Alloyのバージョンを1.5.1から1.5.0に変えたらとりあえず表示はできました!
Titanium +Alloy+ACSの構成でユーザーログインでFacebookアカウントを利用するですが、最終的にどのようなものができるのかわからないため、合ってるのかよくわからないのですが、たぶんできてなさそうです。エラーが出てます。acs.jsにエラーが出てます。
23行目の
debugger;
が「Syntax Error: unexpected token "debugger"」と出てしまっています。
@kozasaryosuke さん
最終的にどのようなものができるのかわからないため、
失礼しました(^_^;)
今回作ってるアプリから、Facebookの個人情報にアクセスしてもいいかどうか尋ねられるこのような表示がでます。(画面上部の赤い警告の部分が出ないかもしれませんが、似たような画面に切り替わればOKです)
controllers/index.jsの以下が実行されます。TitaniumのFacebook機能では、最低限の情報しか取得できないため、別途requestWithGraphPath()という機能を通じて、そのアカウントの個人情報(メールとか誕生日)を取得しにいきます。
if (e.success) {
fb.requestWithGraphPath('me',{},"GET",function(e) {
if (e.success) {
var obj = JSON.parse(e.result),
moment = require('alloy/moment'),
token, user, email, birthday;
email = obj.email;
birthday = moment(obj.birthday, "MM/DD/YYYY");
token = fb.accessToken;
Ti.API.info("Success: " +
"email" + email +
"birthday" + birthday +
"token" + token
);
user = Alloy.createModel('social');
user.fbLogin(token,email, birthday);
}
}
);
記事を書いた時に、私の手元で作っていたサンプルアプリでは自作のFacebookアイコン画像を表示するようにしてしまっていてそのままのXMLファイルを記事に書いてしまってました。
そのため、以下のように変更してもらえればと思います
<Alloy>
<Window class="container">
<Label id="fbloginLabel">Facebookでログイン</Label>
</Window>
</Alloy>
できました〜
おー、出来ましたね!
@kozasaryosuke さん
1月第2週からTitaniumで最近主流のAlloyという仕組みを使った開発に着手してきます。
Webの世界のHTML/CSSのような感じで画面要素を分割する手法がAlloyの設計思想に組み込まれてるので、慣れてくると、どこにどんなファイルがあって、どこを修正、作成すればいいのかがわかるようになるので、かなり開発が楽に進められるようになってます。
手軽に使えて開発効率があがるJavaScriptライブラリを使う
Alloyでの開発に入る前に、Alloyで標準的に利用されてるJavaScriptのライブラリがあるのでそれの扱いを最低限おさえておきたいので以下をまずおねがいします
Alloyでの開発
適当なサンプルアプリ&情報がみつからないので、過去書いた記事をまとめてるので少しお待ちください