Synopsis
Titanium Mobile is moving toward the adoption of the CommonJS module specification as the way in which end users of the platform structure their JavaScript code. While CommonJS Modules are a "standard" specification, there are differences in implementation across multiple technology stacks.
Definitions
- Module - Any CommonJS-compliant module which will be consumed in a Titanium Mobile application. This can be a JavaScript file included with an application, or a native extension to Titanium which exposes a JavaScript API.
- Resources - The Resources directory of a Titanium application, where the user's
source code lives before any processing by our build system. Note: For Alloy, CommonJS modules are placed in
app/lib
exports
- a free variable within a module, to which multiple properties may be added to create a public interfacemodule.exports
- an object within a module, which may be REPLACED by an object representing the public interface to the module
CommonJS Module Specification implementation
Our specific implementation of the CommonJS Module Specification is based on (and the early implementation on Android taken directly from) that of node.js. While we should not consider our implementation a direct clone of node, we should favor node conventions where possible to foster reuse of modules across both environments.
Simple usage
In order to use a module within Titanium, you must use the require
function, which is built in to the global scope in every JavaScript context.
var myModule = require( 'MyModule' ); |
The string passed to require
must be resolvable to either a native/compiled module that Titanium Mobile
has access to, or a JavaScript module provided in the Resources directory
of the Titanium Mobile application. The require
function returns an JavaScript object, with properties, functions, and
other data assigned to it which form the public interface to the module.
If the module we loaded into the application above exposed a function sayHello
, which would print a name and a welcome message to the console, it would
be accessed in this way:
var myModule = require( 'MyModule' ); myModule.sayHello( 'Kevin' ); // Console output: "Hello Kevin!" |
Native/Compiled versus JavaScript modules
When a module is required, Titanium must first determine whether or not to load a native/compiled module or a JavaScript module shipped within the Resources directory of a Titanium Mobile application. Titanium will prefer to load a native module first. The deployment and processing of native modules is beyond the scope of this specification, but at the time of this writing, native modules can be deployed globally on a developer machine, or inside a modules directory in the top-level Titanium Mobile project directory.
Native/Compiled modules
Native/compiled modules are identified by a single string, specified within the global app configuration in tiapp.xml. Given the following configuration for a native/compiled module in tiapp.xml:
< modules > < module version = "1.0" >ti.paypal</ module > </ modules > |
and the following code within a Titanium Mobile application:
var paypal = require( 'ti.paypal' ); |
Titanium will load the ti.paypal
native module, and will NOT attempt to look for or load a module from
Resources. If a native module is not found for the string passed to require
, Titanium will look for a JavaScript module in the Resources.
JavaScript modules
Modules may also be loaded as JavaScript files from the Resources directory of the application. In Titanium Mobile, a JavaScript module is associated with a single JavaScript file. When the module is loaded, the JavaScript file will be evaluated and the public interface of the module will be populated.
JavaScript module path resolution
When dealing with JavaScript modules from Resources, the string passed
to require
is considered to be a path to the JavaScript file, minus the ".js"
extension. If the path string is not prefixed by a ./
, ../
, or similar, it is assumed that the module is being referenced relative
to the Resources directory. In a Titanium project with a CommonJS module
file located in Resources/app/lib/myModule.js
, that module could be loaded as such: var myModule = require('app/lib/myModule');
.
Similarly, if the path is prefixed with a /
, the module path is also resolved relative to the Resources directory.
For the module above, another valid loading syntax would be var myModule = require('/app/lib/myModule');
.
Relative paths may be specified as well. Assume we have modules located in the following locations:
Resources/app/ui/SomeCustomView.js
Resources/app/ui/widgets/SomeOtherCustomView.js
Resources/app/lib/myModule.js
Now assume we are writing code inside the SomeCustomView.js
module file. The following are valid require
statements:
var myModule = require( '../lib/myModule' ); var SomeOtherCustomView = require( './widgets/SomeOtherCustomView' ); |
JavaScript module composition
As in the CommonJS Module specification, inside the module JavaScript file, there will be a special variable called
exports
to which properties may be added for the public interface of the module.
1
2
3
4
5
6
|
exports.sayHello = function (name) { Ti.API.info( 'Hello ' +name+ '!' ); }; exports.version = 1.4; exports.author = 'Jon Doe' ; |
As many properties as desired can be added to the exports object.
Alternately, if the module author wishes to make the exported value from
the module an object of their own design and choosing, there is a non-standard
(but common, as with node.js) extension to the Module specification which
allows for this. The module.exports
object is available within the module file, and may be assigned any value
which the developer would like to return from the require
function for their module. This is most commonly used for functions which
act as object constructors. The following would be a typical use case for
this:
1
2
3
4
5
6
7
8
9
10
|
function Person(firstName,lastName) { this .firstName = firstName; this .lastName = lastName; } Person.prototype.fullName = function () { return this .firstName+ ' ' + this .lastName; }; module.exports = Person; |
Usage:
var Person = require( 'Person' ); var johnDoe = new Person( 'John' , 'Doe' ); var johnDoeName = johnDoe.fullName(); // "John Doe" |
Antipatterns and unsupported behavior
No direct assignments may be made to the exports object:
1
2
3
4
5
6
|
function Person(firstName, lastName) { this .firstName = firstName; this .lastName = lastName; } exports = Person; // THIS IS NOT OK AND PROBABLY WON'T WORK |
Similarly, you should not mix and match usage of module.exports
and exports.*
:
1
2
3
4
5
6
7
|
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 |
Also, it is recommended that you not mix and match assignments to module.exports
and exports
- use one or the other:
exports.foo = 'bar' ; module.exports.fooToo = 'something else' ; // Not good style - use one or the other. |
Caching
When a JavaScript module is loaded, the object returned by require
should be cached by Titanium and provided again to consumers without evaluating
the module's JavaScript code multiple times. If a developer thinks they
want their module code evaluated multiple times, they should really be
creating a module with a function that can be called multiple times. There's
no valid use case for re-evaluating JavaScript in a module over and over.
Security and sandboxing
As in the CommonJS Module specification, all modules have their own private scope. Variables declared within the module file are private - anything that needs to be made public should be added to the exports object. For more information on sandboxing, refer to the CommonJS module spec.
Stateful modules
All modules in Titanium are created once, and then passed by reference on subsequent occasions when the module is required. Because of this, modules themselves may have state variables, which are properties of the "singleton" object represented by the module.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
var stateful = require( 'statefulModule' ); var score = require( 'scoreModule' ); var window = Ti.UI.createWindow({ backgroundColor: 'white' , 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(); |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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; }; |
1
2
3
4
5
6
7
8
9
10
11
|
var _stepVal = 5; // default exports.setPointStep = function (value) { _stepVal = value; }; exports.getPointStep = function () { return _stepVal; }; exports.stepVal = _stepVal; |
NOTE: A module is created once per Titanium JavaScript context, so if additional contexts are created, new module objects will be created. For more on JavaScript contexts, see here.
Global variables
There shall not be ANY global variables in a Titanium application shared across all modules. Any data a module or any objects exposed by a module require should be passed in during construction or initialization.
JavaScript module examples
Here are some examples of modules we expect developers to implement:
Utility libraries
1
2
3
4
5
6
7
|
exports.info = function (str) { Titanium.API.info( new Date() + ': ' + str); }; exports.debug = function (str) { Titanium.API.debug( new Date() + ': ' + str); }; |
Usage:
var logger = require( 'logger' ); logger.info( 'some log statement I wanted with a timestamp' ); |
Packages of related functionality
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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); }; // Export the public interface exports.Point = Point; exports.Line = Line; |
Usage:
1
2
3
4
5
6
7
|
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(); |
Instantiable objects
1
2
3
4
5
6
7
8
9
10
|
function Person(firstName, lastName) { this .firstName = firstName; this .lastName = lastName; } Person.prototype.fullName = function () { return this .firstName + ' ' + this .lastName; }; module.exports = Person; |
Usage:
1
2
3
|
var Person = require( 'Person' ); var johnDoe = new Person( 'John' , 'Doe' ); var johnDoeName = johnDoe.fullName(); // "John Doe" |
Node.js support
Titanium supports node.js modules and structures the require()
handling based on the node.js require-specification. Read more about this
topic in the dedicated Node.js guide.
1 コメント
Ricardo
This topic is really nice, its like one of the best practices a developer may have well-organized its code.
Thank you very much for this article Rick Blalock.