Skip to content
This repository has been archived by the owner on Sep 23, 2020. It is now read-only.

Latest commit

 

History

History
673 lines (478 loc) · 15.2 KB

README.markdown

File metadata and controls

673 lines (478 loc) · 15.2 KB

What is scaleApp?

scaleApp is a tiny JavaScript framework for scalable One-Page-Applications / Single-Page-Applications. The framework allows you to easily create complex web applications.

Build Status

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.

scaleApp architecture

Unlike Zakas recommendations to abstract DOM manipulations and separating the framework from the base library, scaleApp does not implement any DOM methods.

Instead scaleApp can be extended by plugins. So you can just use one of your favorite libs (e.g. jQuery) as base library or you are going to implement all your needed DOM methods into the DOM plugin (scaleApp.dom.coffee) for a more clean and scaleable architecture.

Features

  • loose coupling of modules
  • small (about 340 sloc / 10k min / 3.4k 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

Extendable

scaleApp itself is very small but it can be extended with plugins. There already are some plugins available (e.g. mvc, i18n, permission, state, etc.) but you can easily define your own one.

Download latest version

Quick Start

Link scaleApp.min.js in your HTML file:

<script src="scaleApp.min.js"></script>

If you're going to use it with node:

npm install scaleapp
var sa = require("scaleapp");

or use bower:

bower install scaleapp

Register modules

scaleApp.register( "myModuleId", function( sb ){
  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. Of course your module can be any usual class with those two functions. Here an coffee-script example:

class MyGreatModule

  constructor: (@sb) ->
  init: -> alert "Hello world!"
  destroy: -> alert "Bye bye!"

scaleApp.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.

Show registered modules

scaleApp.lsModules() // returns an array of module names

Show running instances

scaleApp.lsInstances() // returns an array of instance names

Show registered plugins

scaleApp.lsPlugins() // returns an array of plugin names

Asynchronous initialization

You can also init or destroy you module in a asynchronous way:

class MyAsyncModule

  constructor: (@sb) ->

  init: (options, done) ->
    doSomethingAsync (err) ->
      done err
  destroy: (done) ->
    doSomethingAsync (err) ->
      done err

scaleApp.register "myGreatModule", MyGreatModule
end -> alert "now the initialization is done"
scaleApp.start "myGreatModule", callback: end

Unregister modules

It's simple:

scaleApp.unregister("myGreatModule");

Start modules

After your modules are registered, start your modules:

scaleApp.start( "myModuleId" );
scaleApp.start( "anOtherModule" );

Start options

You may also want to start several instances of a module:

scaleApp.start( "myModuleId", {instanceId: "myInstanceId" } );
scaleApp.start( "myModuleId", {instanceId: "anOtherInstanceId" });

If you pass a callback function it will be called after the module started:

scaleApp.start( "myModuleId", {callback: function(){ /*...*/ } );

All other options you pass are available through the sandbox:

scaleApp.register( "mod", function(sandbox){
  return {
    init: function(opt){
      (opt === sandbox.options)      // true
      (opt.myProperty === "myValue") // true
    },
    destroy: function(){ /*...*/ }
  };
});

scaleApp.start("mod", {
  instanceId: "test",
  options: { myProperty: "myValue" }
});

If all your modules just needs to be instanciated once, you can simply starting them all:

scaleApp.startAll();

To start some special modules at once you can pass an array with the module names:

scaleApp.startAll(["moduleA","moduleB"]);

You can also pass a callback function:

scaleApp.startAll(function(){
  // do something when all modules were initialized
});

Stopping

It's obvious:

scaleApp.stop("moduleB");
scaleApp.stopAll();

Listing modules and instances

lsModules()   // returns an array of all registered module IDs
lsInstances() // returns an array of all running instance IDs

Publish/Subscribe

If the module needs to communicate with others, you can use the publish and subscribe methods.

Publish

The publish function takes three parameters whereas the last one is optional:

  • topic : the channel name you want to publish to
  • data : the data itself
  • opt : options or callback
    • publishReference : If the data should be passed as a reference to the other modules this parameter has to be set to true. By default the data object gets copied so that other modules can't influence the original object.

The publish function is accessible through the sandbox:

sb.publish( "myEventTopic", myData );

You can also use the shorter method alias emit.

Subscribe

A message handler could look like this:

var messageHandler = function( data, topic ){
  switch( topic ){
    case "somethingHappend":
      sb.publish( "myEventTopic", processData(data) );
      break;
    case "aNiceTopic":
      justProcess( data );
      break;
  }
};

... and it can listen to one or more channels:

sub1 = sb.subscribe( "somthingHappend", messageHandler );
sub2 = sb.subscribe( "aNiceTopic", messageHandler );

Or just do it at once:

sb.subscribe({
  topicA: cbA
  topicB: cbB
  topicC: cbC
});

You can also subscribe to several channels at once:

sb.subscribe(["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

sb.unsubscribe("a-channel", callback);

And you can remove a callback function from all channels

sb.unsubscribe(callback);

Or remove all subscriptions from a channel:

sb.unsubscribe("channelName");

Flow control

Series

var task1 = function(next){
  setTimeout(function(){next(null, "one");},0);
};

var task2 = function(next){
  next(null, "two");
};

scaleApp.util.runSeries([task1, task2], function(err, results){
  // result is ["one", "two"]
});

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

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>

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", ... },
  ...
}
...
scaleApp.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.

sb._("myStringId");

You can set the language globally by using the setLanguage method:

scaleApp.i18n.setLanguage( "de" );

You can also set a global i18n object which can be used by all modules:

scaleApp.i18n.setGlobal( myGlobalObj );

mvc - very simple MVC

scaleApp mvc

Here is a sample use case for using the MVC plugin (in coffeescript).

class MyModel extends scaleApp.Model name: "Noname"
class MyView extends scaleApp.View

  constructor: (@model, @sb, @template) -> super @model

  # The render method gets automatically called when the model changes
  # The 'getContainer' method is provided by the dom plugin
  render: -> @sb.getContainer.innerHTML = @template @model
class MyController extends scaleApp.Controller

  changeName: (name) -> @model.set "name", name
registerModule "myModule", (@sb) ->

  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, @sb, @template
    @c = new MyController @m, @v

    # listen to the "changeName" event
    @sb.subscribe "changeName", @c.changeName, @c

  destroy: ->
    delete @c
    delete @v
    delete @m
    @sb.unsubscribe @
scaleApp.publish "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.

scaleApp fsm

s = new scaleApp.StateMachine
          start: "a"
          states:
            a:      { enter: (ev) -> console.log "entering state #{ev.to}"  }
            b:      { leave: (ev) -> console.log "leaving state #{ev.from}" }
            c:      { enter: [cb1, cb2], leave: cb3                         }
            fatal:  { enter: -> 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: -> }                 # 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", (transition, eventName, next) ->
  # ...
  next()

s.onEnter "b", (transitioin, eventName, next) ->
  doSomething (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.

scaleApp.permission.add "instanceA", "subscribe", "a"
scaleApp.permission.add "instanceB", "publish", ["b", "c"]

Now instanceA is allowed to subscribe to channel a but instanceB cannot subscribe to it. Therefore instanceB can publish data on channel a and instanceB can not.

Of course you can remove a permission at any time:

scaleApp.permission.remove "moduleA", "publish", "x"

Or remove the subscribe permissions of all channels:

scaleApp.permission.remove "moduleB", "subscribe"

strophe - XMPP plugin

This is an adapter plugin for Strophe.js with some helpful features (e.g. automatically reconnect on page refresh).

scaleApp.xmpp.login("myjid@server.tld", "myPassword");
scaleApp.xmpp.logout();
scaleApp.xmpp.jid       // the current JID

util - some helper functions

  • sb.mixin(receivingClass, givingClass, override=false)
  • sb.countObjectKeys(object)

Other plugins

  • dom - basic DOM manipulations (currently only used for getContainer)

Write your own plugin

scaleApp.registerPlugin

  # set the ID of your plugin
  id: "myPlgin"

  # define the core extensions
  core:
    myCoreFunction: -> alert "Hello core plugin"
    myBoringProperty: "boring"

  # define the sandbox extensions
  sandbox: (@sb) ->
    appendFoo: -> @sb.getContainer.append "foo"

Usage:

scaleApp.myCoreFunction()   # alerts "Hello core plugin"

class MyModule
  constructor: (@sb) ->
  init: -> @sb.appendFoo()  # appends "foo" to the container
  destroy: ->

Existing modules

You can find some example modules in src/modules/.

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.

Changelog

v0.3.9 (12-2012)

  • extended clock module
  • 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 and lsInstances
  • 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 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.