What is scaleApp?
scaleApp is a tiny JavaScript framework for scalable and maintainable One-Page-Applications / Single-Page-Applications. The framework allows you to easily create complex web applications.
You can dynamically start and stop/destroy modules that acts as small parts of your whole application.
Architecture overview
scaleApp is based on a decoupled, event-driven architecture that is inspired by the talk of Nicholas C. Zakas - "Scalable JavaScript Application Architecture" (Slides). There also is a little Article that describes the basic ideas.
Module
A module is a completely independent part of your application. It has absolutely no reference to another piece of the app. The only thing the module knows is your sandbox. The sandbox is used to communicate with other parts of the application.
Sandbox
The main purpose of the sandbox is to use the facade pattern. In that way you can hide the features provided by the core and only show a well defined custom static long term API to your modules. This is actually one of the most important concept for creating mainainable apps. Change plugins, implementations etc. but keep your API stable for your modules. For each module a separate sandbox will be created.
Core
The core is responsible for starting and stopping your modules. It also handles the messages by using the Publish/Subscribe (Mediator) pattern
Plugin
Plugins can extend the core or the sandbox with additional features. For example you could extend the core with basic functionalities (like DOM manipulation) or just aliases the features of a base library (e.g. jQuery).
Features
- loose coupling of modules
- small (about 300 sloc / 8,7k min / 3.3k gz)
- no dependencies
- modules can be tested separately
- replacing any module without affecting other modules
- extendable with plugins
- browser and Node.js support
- flow control
- AMD & CommonJS support
- framework-agnostic
Extendable
scaleApp itself is very small but it can be extended with plugins. There already are some plugins available:
mvc
- simple MVCi18n
- multi language UIspermission
- take care of method accessstate
- Finite State Machinesubmodule
- cascade modulesdom
- DOM manipulationstrophe
- XMPP communicationmodulestate
- event emitter forinit
anddestroy
util
- helper methods likemixin
,uniqueId
etc.ls
- list modules, instances & plugins
You can easily define your own plugin (see plugin section).
Download
Latest stable 0.4.x version
or use the CDN:
<script src="//cdnjs.cloudflare.com/ajax/libs/scaleapp/0.4.3/scaleapp.min.js" ></script>
Old stable 0.3.x version
Note
There are some API changes in version 0.4.x (see Changelog). Docs for v0.3.9 can be found within the tar/zip file.
Unstable version
git clone git://github.com/flosse/scaleApp.git
Quick Start
Link scaleApp.min.js
in your HTML file:
<script src="scaleApp.min.js"></script>
or use the CDN:
<script src="//cdnjs.cloudflare.com/ajax/libs/scaleapp/0.4.3/scaleapp.min.js" ></script>
If you're going to use it with node:
npm install scaleapp --save
var sa = require("scaleapp");
or use bower:
bower install scaleapp
Create your own Sandbox
First of all create your own sandbox. By doing that you're able to guarantee a stable maintainable API for your modules.
var MySandbox = function(core, instanceId, options, moduleId) {
// define your API
this.myFooProperty = "bar";
// e.g. provide the Mediator methods 'on', 'emit', etc.
core._mediator.installTo(this);
// ... or define your custom communication methods
this.myEmit = function(channel, data){
core.emit(channel + '/' + instanceId, data);
};
// maybe you'd like to expose the instance ID
this.id = instanceId;
return this;
};
// ... and of course you can define shared methods etc.
MySandbox.prototype.foo = function() { /*...*/ };
Create a core
Now create a new core instance with your sandbox:
var core = new scaleApp.Core(MySandbox);
Register modules
core.register( "myModuleId", function( sandbox ){
return {
init: function(){ /*...*/ },
destroy: function(){ /*...*/ }
};
});
As you can see the module is a function that takes the sandbox as a parameter
and returns an object that has two functions init
and destroy
(the latter is
optional).
Of course your module can be any usual class with those two functions.
var MyGreatModule = function(sandbox){
return {
init: function(){ alert("Hello world!"); }
destroy: function(){ alert("Bye bye!"); }
};
};
core.register("myGreatModule", MyGreatModule);
The init
function is called by the framework when the module is supposed to
start. The destroy
function is called when the module has to shut down.
Asynchronous initialization
You can also init or destroy you module in a asynchronous way:
var MyAsyncModule = function(sandbox){
return {
init: function(options, done){
doSomethingAsync(function(err){
// ...
done(err);
});
},
destroy: function(done){
doSomethingElseAsync(done);
}
};
};
core.register("myGreatModule", MyGreatModule);
core.start("myGreatModule", { done:function(){
alert("now the initialization is done");
}});
Start modules
After your modules are registered, start your modules:
core
.start( "myModuleId" )
.start( "anOtherModule", function(err){
// 'anOtherModule' is running now
});
Start options
You may also want to start several instances of a module:
core.start( "myModuleId", {instanceId: "myInstanceId" } );
core.start( "myModuleId", {instanceId: "anOtherInstanceId" });
All you attach to options
is accessible within your module:
core.register( "mod", function(sandbox){
return {
init: function(opt){
(opt.myProperty === "myValue") // true
},
destroy: function(){ /*...*/ }
};
});
core.start("mod", {
instanceId: "test",
options: { myProperty: "myValue" }
});
If all your modules just needs to be instanciated once, you can simply starting them all:
core.start();
To start some special modules at once you can pass an array with the module names:
core.start(["moduleA","moduleB"]);
You can also pass a callback function:
core.start(function(){
// do something when all modules were initialized
});
Moreover you can use a separate sandbox for each instance:
var MySandbox = function(){/*...*/};
core.start("module", { sandbox: MySandbox });
Stopping
It's obvious:
core.stop("moduleB");
core.stop(); // stops all running instances
Publish/Subscribe
If the module needs to communicate with others, you can use the emit
and
on
methods.
emit
The emit
function takes three parameters whereas the last one is optional:
- topic
: the channel name you want to emit to
- data
: the data itself
- cb
: callback method
The emit function is accessible through the sandbox (as long as you exposed the Mediator methods of course):
sandbox.emit( "myEventTopic", myData );
on
A message handler could look like this:
var messageHandler = function( data, topic ){
switch( topic ){
case "somethingHappend":
sandbox.emit( "myEventTopic", processData(data) );
break;
case "aNiceTopic":
justProcess( data );
break;
}
};
... and it can listen to one or more channels:
sub1 = sandbox.on( "somthingHappend", messageHandler );
sub2 = sandbox.on( "aNiceTopic", messageHandler );
Or just do it at once:
sandbox.on({
topicA: cbA,
topicB: cbB,
topicC: cbC
});
You can also subscribe to several channels at once:
sandbox.on(["a", "b"], cb);
If you prefer a shorter method name you can use the alias on
.
attache and detache
A subscription can be detached and attached again:
sub.detach(); // don't listen any more
sub.attach(); // receive upcoming messages
Unsubscribe
You can unsubscribe a function from a channel
sandbox.off("a-channel", callback);
And you can remove a callback function from all channels
sandbox.off(callback);
Or remove all subscriptions from a channel:
sandbox.off("channelName");
Flow control
Series
var task1 = function(next){
setTimeout(function(){
console.log("task1");
next(null, "one");
},0);
};
var task2 = function(next){
console.log("task2");
next(null, "two");
};
scaleApp.util.runSeries([task1, task2], function(err, result){
// result is ["one", "two"]
});
// console output is:
// "task1"
// "task2"
Parallel
var task1 = function(next){
setTimeout(function(){
console.log("task1");
next(null, "a");
},0);
};
var task2 = function(next){
console.log("task2");
next(null, "b");
};
scaleApp.util.runParallel([task1, task2],function(err,result){
// result is ["a", "b"]
});
// console output is:
// "task2"
// "task1"
There is also a little helper tool to run the same async task again and again in parallel for different values:
var vals = ["a","b", "c"];
var worker = function(val, next){
console.log(val);
doSomeAsyncValueProcessing(val,function(err,result){
next(err, result);
});
};
scaleApp.util.doForAll(args, worker, function(err, res){
// fini
});
Waterfall
var task1 = function(next){
setTimeout(function(){
next(null, "one", "two");
},0);
};
var task2 = function(res1, res2, next){
// res1 is "one"
// res2 is "two"
next(null, "yeah!");
};
scaleApp.util.runWaterfall([task1, task2], function(err, result){
// result is "yeah!"
});
Plugins
There are some plugins available within the plugins
folder.
For more information look at the
plugin README.
Register plugins
A single plugin can be registered with it option object in that way:
core.use(plugin,options);
If you want to register multiple plugins at once:
core.use([
plugin1,
plugin2,
{ plugin: plugin3, options: options3 }
]);
Write your own plugin
It's easy:
core.use(function(core){
core.helloWorld = function(){ alert("helloWorld"); };
};
Here a more complex example:
core.use(function(core, options, done){
// extend the core
core.myCoreFunction = function(){ alert("Hello core plugin") };
core.myBoringProperty = "boring";
// extend the sandbox class
core.Sandbox.prototype.myMethod = function(){/*...*/};
// define a method that gets called when a module starts
var onModuleInit = function(instanceSandbox, options, done){
// e.g. define sandbox methods dynamically
if (options.mySwitch){
instanceSandbox.appendFoo = function(){
core.getContainer.append("foo");
};
}
// or load a something asynchronously
core.myAsyncMethod(function(data){
// do something...
// now tell scaleApp that you're done
done();
});
};
// define a method that gets called when a module stops
var onModuleDestroy = function(done){
myCleanUpMethod(function(){
done()
});
};
// don't forget to return your methods
return {
init: onModuleInit,
destroy: onModuleDestroy
};
});
Usage:
core.myCoreFunction() // alerts "Hello core plugin"
var MyModule = function(sandbox){
init: function(){ sandbox.appendFoo(); }, // appends "foo" to the container
};
Build browser bundles
If you want scaleApp bundled with special plugins type
grunt custom[:PLUGIN_NAME]
e.g. cake custom:dom:mvc
creates the file scaleApp.custom.js
that
contains scaleApp itself the dom plugin and the mvc plugin.
API
scaleApp
scaleApp.VERSION
- the current version of scaleAppscaleApp.Mediator
- the Mediator classscaleApp.Sandbox
- the Sandbox classscaleApp.Core
- the Core class
Core
// use default sandbox
var core = new scaleApp.Core();
// use your own sandbox
var core = new scaleApp.Core(yourSandboxClass);
core.register(moduleName, module, options)
- register a modulecore.use(plugin, options)
- register a plugincore.use(pluginArray)
- registers an array of pluginscore.boot(callback)
- initialize plugins (will be executed automatically on ´start´)core.start(moduleId, options, callback)
- start a modulecore.stop(instanceId, callback)
- stop a module
Mediator
// create a mediator
var mediator = new scaleApp.Mediator();
// create a mediator with a custom context object
var mediator = new scaleApp.Mediator(context);
// create a mediator with cascaded channels
var mediator = new scaleApp.Mediator(null, true);
mediator.emit(channel, data, callback)
mediator.on(channel, callback, context)
mediator.off(channel, callback)
mediator.installTo(context, force)
// subscribe
var subscription = mediator.on(channel, callback, context);
subscription.detach
- stop listeningsubscription.attach
- resume listening
var fn = function(){ /*...*/ };
var obj = { emit: fn };
// the installTo method prevents existing properties by default
mediator.installTo(obj);
obj.emit === fn // true
// set the second paramater to 'true'
// to force the mediator to override existing propeties
mediator.installTo(obj, true);
obj.emit === mediator.emit // true
Sandbox
This is the default sandbox of scaleApp. It's a better idea to use your own one.
var sandbox = new scaleApp.Sandbox(core, instanceId, options, moduleId)` - create a Sandbox
sandbox.emit
ismediator.emit
sandbox.on
ismediator.on
sandbox.off
ismediator.off
Changelog
v0.4.3 (02-2014)
- added option to
Mediator.installTo
to force overriding existing properties - added option
useGlobalMediator
to the submodule plugin - added option
mediator
to the submodule plugin - added submodule example
- fixed requireJS example
- fixed grunt task for custom builds
- strophe plugin
- expose the mediator
- fixed error emitting on failed connection
- compile with coffee-script 1.7.1
v0.4.2 (10-2013)
- fixed restarting modules
- speed up argument extraction
- little refactoring
v0.4.1 (09-2013)
- no more sandbox manipulation
- added start option to use a separate sandbox
- removed modules directory (building modules is your own business; above all they should depend on YOUR sandbox)
- available at cdnjs.com
- improved README
- bugfixes
v0.4.0 (07-2013)
- added a
Core
class that can be instantiated (var core = new scaleApp.Core();
) - new plugin API (
scaleApp.plugins.register
moved tocore.use
)- support asynchronous plugins
- added
boot
method to initialize asynchronous plugins
- changed API
startAll()
is nowstart()
stopAll()
is nowstop()
- the API is now chainable (e.g.
core.use(X).register("foo",bar).start("foo")
) - removed
setInstanceOptions
- removed
unregister
andunregisterAll
- dropped
subscribe
,unsubscribe
andpublish
from Mediator API (useon
,off
andemit
instead) - the methods
lsModules
,lsInstances
,lsPlugins
moved to thels
plugin - the
destroy
method of a module is now optional - the
callback
property of the start option object was removed. Use themodulestate
plugin instead
- plugins
- new
submodule
plugin - improved
permission
andi18n
- new
modulestate
plugin to emit events on module state changes
- new
- cleaner code
Mediator
: do not clone objects any more (do it manually instead)- test with mocha, chai, sinon, karma and PhantomJS instead of buster.js
v0.3.9 (12-2012)
- grunt as build systemt
- added waterfall flow control method
- improved permission plugin
- improved state plugin (thanks to Strathausen)
- added xmpp (stropje.js) plugin
- added a simple clock module
- added bower support
- added this changelog
v0.3.8 (08-2012)
- bug fixes
- added support for async. callback of the
publish
method - added amd support
v0.3.7 (07-2012)
- bug fixes
- added permission plugin
- ported specs to buster.js
- support for global i18n properties
v0.3.6 (03-2012)
- support for async. and sync. module initialization
v0.3.5 (03-2012)
- simpified Mediator code
v0.3.4 (03-2012)
- bugfixes
- added
lsModules
andlsInstances
- improved README
v0.3.3 (02-2012)
- run tests with jasmine-node instead of JSTestDriver
- added travis testing
- improved README
v0.3.2 (01-2012)
- bugfixes
- improved Mediator
v0.3.0 (11-2011)
- ported to Coffee-Script
- removed jQuery dependency
v0.2.0 (07-2011)
- bugfixes
- improvements
v0.1.0 (02-2011)
- first release
Testing
npm test
Demo
WARNING: the demo is totally out of date!
You can try out the sample application that is build on scaleApp. Also have a look at the source code.
Licence
scaleApp is licensed under the MIT license. For more information have a look at LICENCE.txt.
scaleApp - Plugins
i18n - Multi language UIs
Link scaleApp.i18n.min.js
in your HTML file:
<script src="scaleApp.min.js"></script>
<script src="scaleApp.i18n.min.js"></script>
Register the plugin:
core.use(scaleApp.plugins.i18n);
If your application has to support multiple languages, you can pass an objects containing the localized strings with the options object.
var myLocalization =
{
en: { welcome: "Welcome", ... },
de: { welcome: "Willkommen", ... },
...
}
...
core.register( "moduleId", myModule, { i18n: myLocalization } );
Now you can access these strings easily trough the sandbox using the _
method.
Depending on which language is set globally it returns the corresponding
localized string.
sandbox._("myStringId");
You can set the language globally by using the setLanguage
method:
core.i18n.setLanguage( "de" );
You can also set a global i18n object which can be used by all modules:
core.i18n.setGlobal( myGlobalObj );
Within your module you can define your local texts:
function(sandbox){
init: function(){
sandbox.i18n.addLocal({
en: {hello: "Hello" },
de: {hello: "Hallo" }
});
},
destroy: function(){}
}
Subscribe to change event:
sandbox.i18n.onChange(function(){
// update ui
});
mvc - very simple MVC
Here is a sample use case for using the MVC plugin (in coffeescript).
core = new scaleApp.Core
core.use scaleApp.plugins.mvc
core.boot()
class MyModel extends core.Model name: "Noname"
class MyView extends core.View
constructor: (@model, @sandbox, @template) -> super @model
# The render method gets automatically called when the model changes
# The 'getContainer' method is provided by the dom plugin
render: -> @sandbox.getContainer.innerHTML = @template @model
class MyController extends core.Controller
changeName: (name) -> @model.set "name", name
core.registerModule "myModule", (@sandbox) ->
init: (opt) ->
# You can use any template engine you like. Here it's
# just a simple function
template = (model) -> "<h1>Hello #{model.name}</h1>"
@m = new MyModel
@v = new MyView @m, @sandbox, @template
@c = new MyController @m, @v
# listen to the "changeName" event
@sandbox.on "changeName", @c.changeName, @c
destroy: ->
delete @c
delete @v
delete @m
@sandbox.off @
core.emit "changeName", "Peter"
state - Finite State Machine
The state plugin is an approach to implement a Finite State Machine that can be used to keep track of your applications state.
var s = new scaleApp.StateMachine({
start: "a",
states: {
a: { enter: function(ev){ console.log("entering state " + ev.to ); }},
b: { leave: function(ev){ console.log("leaving state " + ev.from ); }},
c: { enter: [cb1, cb2], leave: cb3 },
fatal: { enter: function(){ console.error("something went wrong"); }}
},
transitions:{
x: { from: "a" to: "b" },
y: { from: ["b","c"] to: "c" },
uups: { from: "*" to: "fatal" }
}
});
s.addState("d", { enter: function(){ /*..*/} }); // add an additional state
s.addState({ y: {}, z: { enter: cb } }); // or add multiple states
s.addTransition("t", { from: "b", to: "d" }); // add a transition
s.can("t"); // false because 'a' is current state
s.can("x"); // true
s.onLeave("a", function(transition, eventName, next){
// ...
next()
});
s.onEnter("b",function(transitioin, eventName, next){
doSomething(function(err){next(err);});
});
s.fire("x");
s.current // b
permission - controll all messages
If you include the permission
plugin, all Mediator
methods will be rejected
by default to enforce you to permit any message method explicitely.
core.permission.add("instanceA", "on", "a");
core.permission.add("instanceB", "emit", ["b", "c"]);
core.permission.add("instanceC", "emit", '*');
core.permission.add("instanceD", '*', 'd');
Now instanceA
is allowed to subscribe to channel a
but all others cannot
subscribe to it.
InstanceB
can emit data on channels a
and c
.
InstanceC
can emit to all channels.
InstanceD
can perform all actions (on
, off
, emit
)
but only on channel d
.
Of course you can remove a permission at any time:
core.permission.remove("moduleA", "emit", "x");
Or remove the subscribe permissions of all channels:
core.permission.remove("moduleB", "on");
strophe - XMPP plugin
This is an adapter plugin for Strophe.js with some helpful features (e.g. automatically reconnect on page refresh).
core.xmpp.login("myjid@server.tld", "myPassword");
core.xmpp.logout();
core.xmpp.jid // the current JID
submodule
core.register("parent", function(sandbox){
var childModule = function(sandbox){
return({
init: function(){
sandbox.emit("x", "yeah!");
},
destroy: function(){}
});
});
return({
init: function(){
sandbox.sub.register("child",childModule);
sandbox.permission.add("child", "emit", "x");
sandbox.sub.on("x",function(msg){
console.log("a child send this: " + msg);
});
sandbox.sub.start("child");
},
destroy: function(){}
});
});
// register the submodule plugin
core.use(scaleApp.plugins.submodule, {
inherit: true, // use all plugins from the parent's Core
use: [somePlugins], // use some additional plugins
useGlobalMediator: true, // emit and receive all events from the parent's Core
mediator: myCustomMediator // 'useGlobalMediator' has to be falsy
});
core.start("parent");
// the "parent" module starts a child within the init method
core.stop("parent");
// all children of "parent" were automatically stopped
Module State
core.use(scaleApp.plugins.modulestate).boot()
core.state.on("init", function(ev){
ev.instanceId // holds the instance ID
ev.moduleId // holds the module ID
});
core.state.on("init/myModule", function(ev){ /* */ });
core.state.on("destroy/myModule/anInstance", function(ev){ /* */ });
ls
Show registered modules
core.lsModules(); // returns an array of module names
Show running instances
core.lsInstances(); // returns an array of instance names
Show registered plugins
core.lsPlugins(); // returns an array of plugin names
util - some helper functions
Helper methods
core.mixin(receivingClass, givingClass, override=false)
core.countObjectKeys(object)
core.clone(object)
core.uniqueId(length=8)
Other plugins
- dom - basic DOM manipulations (currently only used for
getContainer
)