JP::HSJ::Junknews::HatenaSide このページをアンテナに追加 RSSフィード

新ブログに移転気味です

はてなブログに新しいブログ MUTEを作ってそちらのほうを中心に更新していますので、併せてご覧下さい。技術系のはこっちに残りそうな気配デス。

Information

TitaniumMobile関連ニュースを扱うブログTitanium Newsを開設しました
titanium-mobile-doc-ja(開店休業中です…ごめんなさい)
▼好評発売中:Titanium Mobileで開発するiPhone/Androidアプリ(翔泳社刊)
 ※Titanium Studio等、刊行後の新機能について補完するPDFを公開しています
  →(http://bit.ly/tsinstall)

Titanium Mobileリンク集

2011年12月30日

CommonJS Modules in Titanium

この記事はAppcelerator公式Wikiのドキュメント(Dec 20, 2011更新版)に基づき、和訳±αしたものです。

https://wiki.appcelerator.org/display/guides/CommonJS+Modules+in+Titanium

(補足:Titanium Mobile SDK 1.7.x以前とは状況が異なる部分もありますので、1.8.x前提ということでご覧ください)

概要

Titnianium Mobileは利用者がJavaScriptのコードを組み立てる方法としてCommonJSモジュール仕様を採用しようとしつつあります。

しかし、CommonJSモジュールは"標準的な"仕様である一方、複数のテクノロジ間で実装の違いがあります。

そのため、Titanium Mobile 1.8(やそれ以降の)の実装において何がサポートされ、何がサポートされていないのかを明示します。

定義
モジュール(Module)Titanium Mobileアプリケーションで使用されるあらゆるCommonJSに準拠した部品。JavaScriptのスクリプトファイルアプリケーションに取り込む形、もしくはObjective-CJavaにより拡張されるモジュールをJavaScript APIとして提供する形のいずれも指す。
リソース(Resources)ユーザソースコードが置いてあるTitaniumアプリケーションのResourcesディレクトリ
exportsモジュール内に存在する複数の変数に対して外部に公開されるインタフェイスを付与するすることができる
module.exportsモジュール内に存在する単一のオブジェクトに対して外部に公開されるインタフェイスを付与するすることができる

CommonJSモジュールの仕様実装

TiにおけるCommonJSモジュールの仕様の実装はnode.jsのものに基づいています。

我々はnode.jsの実装の直接のクローンとなることを考慮している訳ではありませんが、Ti・node.jsのいずれの環境でもモジュールの再利用ができるようnode.jsの規則に賛同するようにしています。

単純な使い方

Titanium内でモジュールを使うために、requireファンクションを用いなくてはなりません。

これによりJavaScriptの実行コンテクストごとのグローバルスコープに組み込まれるようになります。

var myModule = require('MyModule');

Titanium Mobileのネイティブモジュール、もしくはTitanium MobileアプリケーションのResourcesフォルダ内にあるJavaScriptで記述されたモジュールのいずれかが名前解決できる必要があります。この例でいえばMyModuleと言う名前のObj-C・Javaで書かれたモジュールがあるか、MyModule.jsというスクリプトファイルが存在する必要があるわけです。

requireファンクションは(モジュールへアクセスするインタフェースとしての)プロパティ関数を伴う一般的なJavaScriptオブジェクトを返されます。

もし、sayHelloという名前の関数により、コンソール上にWelcomeメッセージと名前を表示する機能をアプリケーションに組み込むには次の通り記述すればいいでしょう。

var myModule = require('MyModule');
myModule.sayHello('Kevin');
//console output is "Hello Kevin!"

ネイティブモジュールとJavaScriptなモジュール

requireファンクションを実行したとき、Titaniumはまずネイティブモジュールをロード可否を判断した後、Resourcesフォルダ内のJavaScriptモジュールで判定を行います。

ちなみにネイティブモジュールの配置と処理方法についてはこの記事の範囲を超えていますが(この記事が書かれた時点では)ネイティブモジュールは開発者のマシン上のグローバルエリアOSXでは/Library/Application Support/Titanium〜もしくは~/Library…)もしくは各Titanium Mobileプロジェクトディレクトリの直下にあるmodulesディレクトリ内部に展開されています。

ネイティブモジュール

Obj-CやJavaで記述されたバイナリであるネイティブモジュールはユニークな文字列で判別され、使用するTitanium mobileプロジェクトのtiapp.xml内に使用宣言の記述を行います。

<modules>
   <module version="1.0">ti.paypal</module>
</modules>

これに対応するTitanium Mobileアプリケーションのコードは次のようになります。

var paypal = require('ti.paypal');

Titaniumは"ti.paypal"という名前のネイティブモジュールをロードしようとしますが、その際にResourcesフォルダ内を探したり、そこからロードしようとすることはありません。前述のとおり、ネイティブモジュールの在処(mobileSDKの所在の並びもしくはプロジェクト直下のmodulesフォルダ)に"ti.paypal"がない場合、Resourcesフォルダ内にあるJavaScriptモジュールを探しに行きます。

JavaScriptモジュール

ResourcesフォルダからロードされるJavaScriptのファイルもモジュールとなります。

Titanium MobileのJavaScriptモジュールは単一のJavaScriptファイルで構成されます。

モジュールがロードされたときにJavaScriptのファイルは評価され、モジュールの公開インタフェイスによってスコープに影響が与えられます。

JavaScriptモジュールのパス解決問題

JavaScriptモジュールをResourcesフォルダ内で走査するとき、JavaScriptモジュールから拡張子".js"を取り除いたパスを記述する必要があります。

指定した文字列の先頭が相対パスを表す ./ や ../ といったモノの場合、Resourcesフォルダ内を相対的に参照しようとします。例えば、プロジェクトフォルダ/Resources/app/lib/myModule.jsファイルを設置した場合、次のように記述する事でロードされます。

var moduleValueName = require('app/lib/myModule');
||< 
同じように、/で始まるパスを指定した場合、Resourcesディレクトリからの相対的な参照を行い((実質的な絶対パス指定とも言えます))、次のように記述してロードします。
>|javascript|
var moduleValueName = require('/app/lib/myModule');
||< 
例えば以下のようなファイルがあるとき、
-Resources/app/ui/SomeCustomView.js
-Resources/app/ui/widgets/SomeOtherCustomView.js
-Resources/app/lib/myModule.js
SomeCustomView.jsから相対パスを指定するとなると次のようになります。
>|javascript|
//SomeCustomView.js
var myModule = require('../lib/myModule');
var SomeOtherCustomView = require('./widgets/SomeOtherCustomView');

JavaScriptモジュール構成

CommonJSモジュール仕様にあるように、JavaScriptモジュールファイル内に"exports"という特別な変数があり、これにより、モジュール外部とやり取りを行えるインターフェイスを持つ事ができます。

exports.sayHello = function(name) {
    Ti.API.info('Hello '+name+'!');
};
 
exports.version = 1.4;
exports.author = 'Don Thorp';

一方、モジュール制作者が設計し選んだオブジェクトのみをモジュール外部に提供したい場合には、非標準(といってもnode.jsで使用されており、共通的に使われている)の拡張方法があります。

モジュールをrequireした時に返したい値をモジュール内で"module.exports"オブジェクトに割り当てる方法です。一般的にオブジェクトコンストラクタとして機能する機能に使用されます。典型的な例をあげると次のようになります。

// /Resources/Person.js
function Person(firstName,lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}
 
Person.prototype.fullName = function() {
    return this.firstName+' '+this.lastName;
};
 
module.exports = Person;
// /Resources/app.js

var Person = require('Person');
var don = new Person('Don','Thorp');
var donsName = don.fullName(); // "Don Thorp"
アンチパターンとサポートされていない挙動

次のように直接割り当てると正しく挙動しません。

function Person(firstName,lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}
 
exports = Person; //THIS IS NOT OK AND PROBABLY WON'T WORK

同様に、module.exportsとexportsを混ぜて使ってはいけません。


function Person(firstName,lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}
 
module.exports = Person; //This is okay, but...
exports.foo = 'bar'; //This is discouraged - use one or the other

また、次のようなmodule.exportsにプロパティを割り当てての定義も推奨されません。

exports.foo = 'bar';
module.exports.fooToo = 'something else'; // Not good style - use one or the other.

キャッシュ機構

JavaScriptモジュールがロードされた時、requireによって返されるオブジェクトTitaniumによってキャッシュされ、複数回ファイルの中身を評価する事なく、利用者に提供されます。もしモジュールのコードが何度も評価されたいのであれば、そのように挙動するモジュールを作成する必要があります。しかし、それは妥当な方法とは言いがたいです。

セキュリティ(とSandbox機能)

CommonJSモジュール仕様にあるように、すべてのモジュールはプライベートの変数スコープを持ちます。モジュールファイル内で宣言された変数はプライベートで、外部に公開する必要があるものは、exportsオブジェクトに追加する必要があります。

Sandboxの詳細については、CommonJSのモジュールの仕様を参照してください。

ステートフルなモジュール

Titaniumのすべてのモジュールは一度作成されると、その後、requireされるたびに参照渡しされます。このため、モジュール自体がsingletonオブジェクトの状態変数である可能性があります。

ちなみに追加の実行コンテキストが作成されるとき、コンテキストごとにモジュールは作成*1されるので、新たなモジュールオブジェクトが評価され、作成されます。

app.js


var stateful = require('statefulModule');
var score = require('scoreModule');
 
var window = Ti.UI.createWindow({
  backgroundColor:'white',
  fullscreen:false,
  title:'Click window to score'
});
 
window.addEventListener('click', function() {
  try {
    Ti.API.info("The latest score is " + score.latestScore());
    Ti.API.info("Adding " + stateful.getPointStep() + " points to score...");
    score.pointsWon();
    Ti.API.info("The latest score is " + score.latestScore());
    Ti.API.info("Setting points per win to 10");
    stateful.setPointStep(10);
    Ti.API.info("Adding " + stateful.getPointStep() + " points to score...");
    score.pointsWon();
    Ti.API.info("The latest score is " + score.latestScore());
    Ti.API.info("---------- Info ----------");
    Ti.API.info("stateful.getPointStep() returns: " + stateful.getPointStep());
    Ti.API.info("stateful.stepVal value is: " + stateful.stepVal); // will always return default of 5
    Ti.API.info("**************************");
  } catch(e) {
    alert("An error has occurred: " + e);
  }
});
 
window.open();

scoreModule.js


var appStateful = require('statefulModule'); // a reference to the "stateful" variable in app.js that contains the stateful module
var _score = 0; // default
 
exports.pointsWon = function() {
  _score += appStateful.getPointStep();
};
 
exports.pointsLost = function() {
  _score -= appStateful.getPointStep();
};
 
exports.latestScore = function() {
  return _score;
};

statefulModule.js

var _stepVal = 5; // default
 
exports.setPointStep = function(value) {
  _stepVal = value;
};
 
exports.getPointStep = function() {
  return _stepVal;
};
 
exports.stepVal = _stepVal;
グローバル変数

アプリケーション内のすべてのモジュールにわたって共有されるグローバル変数は存在してはいけません。

モジュール、またはモジュールによって公開されるオブジェクトであっても、作成時・初期化時に引き渡される必要があります。


JavaScriptモジュールの実装&使用例

ユーティリティライブラリ
// logger.js

exports.info = function(str) {
    Titanium.API.info(new Date()+': '+str);
};
 
exports.debug = function(str) {
    Titanium.API.debug(new Date()+': '+str);
};
// app.js
var logger = require('logger');
logger.info('some log statement I wanted with a timestamp');
関連する機能のパッケージ
// geo.js
function Point(x,y) {
    this.x = x;
    this.y = y;
}
  
function Line(start,end) {
    this.start = start;
    this.end = end;
}
 
Line.prototype.slope = function() {
    return (this.end.y - this.start.y) / (this.end.x - this.start.x);
};
 
Line.prototype.yIntercept = function() {
    return this.start.y - (this.slope()*this.start.x);
};
  
//create public interface
exports.Point = Point;
exports.Line = Line;
// app.js

var geo = require('lib/geo');
  
var startPoint = new geo.Point(1,-5);
var endPoint = new geo.Point(10,2);
  
var line = new geo.Line(startPoint,endPoint);
var slopeValue = line.slope();
インスタンス化可能なオブジェクト
// Person.js

function Person(firstName,lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}
 
Person.prototype.fullName = function() {
    return this.firstName+' '+this.lastName;
};
 
module.exports = Person;
// app.js

var Person = require('Person');
var don = new Person('Don','Thorp');
var donsName = don.fullName(); // "Don Thorp"

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。