Catberry Framework Documentation
Table Of Contents
- Isomorphic Applications
- Flux
- Stores
- Cat-components
- Example of Application Structure
- Routing
- Catberry Services
- Service Locator
- Registration of Own Services
- Dependency Injection
- Userland Catberry Services
- Cookie
- Template Engines
- Browser Bundle
- Event Bus and Diagnostics
- CLI
- Get Started
- Plugin API
- Code Style Guide
Isomorphic Applications
Make a long story short, isomorphic applications are built to make it possible to write module once and use it for both pages rendering on server (for SEO and shared links) and in browser with no server side at all. It means on server your modules are executing the same code as in a browser. This [Single Page Application] (http://en.wikipedia.org/wiki/Single_Page_Application) can render all parts of the page using the same isomorphic modules and not reloading the page at all.
There is an awesome [post in Airbnb technical blog] (http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) about the idea of isomorphic JavaScript applications and what exactly it is. Also, you can find the video [Spike Brehm: Building Isomorphic Apps] (http://www.youtube.com/watch?v=CH6icJbLhlI) from JSConf 2014 talks.
Isomorphic applications can work with a set of independent RESTful services that implement some business logic (Facebook API, Twitter API, your API etc). In fact, each module in isomorphic application should receive all data from API server which could be written using any platform you want using REST approach.
There is a list of problems which are solved by isomorphic applications:
- Using Single Page Applications causes SEO problems. Your isomorphic modules will render exactly the same page on the server as it is in a browser
- Code duplication for rendering parts of the page at the server and in the browser, sometimes it is even written in different programming languages. Since isomorphic modules are written only once and in JavaScript you do not have this problem.
- Maintenance is complicated because you need to synchronize changes in server-side and browser modules. Obviously, you do not need it using isomorphic modules. It is always one module to change.
- Overhead connected with rendering all pages on the server. Since browsers receive a page from the server only once and then render all other pages themselves, your server's load will be reduced dramatically.
And maybe a lot of more problems, who knows.
Technologies like History API and node.js make this type of applications possible and we should use this possibility.
Flux
Catberry uses Flux architecture. It defines that you should use store as data source and some kind of view that gets data from the store. So, Catberry uses cat-components as these views.
Everything you need to know that there are stores, cat-components and store dispatcher that controls the whole workflow.
Store can handle some action messages
from cat-components and they can trigger changed
event.
The event changed
means that Catberry should re-render in the browser
every component that depends on changed store.
Store dispatcher works in such way that does not allow to call store
data loading while previous loading is not over, also it does not allow
some crazy cases when all your stores trigger changed
event at
the same time and re-rendering of component breaks everything.
This is the robust high-performance architecture that allows to create huge and
complicated applications.
One more thing about Catberry architecture: the main approach in Catberry
for controlling asynchronous operations is Promise.
Catberry uses the native Promise
in a browser or in Node.js (V8)
if it is possible. If global type Promise
is not found it will be defined using
"Bare bones Promises/A+ implementation".
It means you can use Promise
type globally and do not worry about its support.
Stores
The store is a module that loads data from a remote resource using routing
parameters. It also can handle action messages from anywhere and send requests
to the remote resource changing data. It can emit changed
event at any time
when it decided that data on the remote resource is changed.
By default, all stores should be placed into ./catberry_stores
directory
of your application. But you can change this directory by config
parameter. Every file should export constructor function for creation of store
instance.
When Catberry initializes it does recursively search in this directory and loads every file. The relative file path without extension becomes a store name.
So if you have such file hierarchy as listed below:
./catberry_stores/
group1/
store1.js
store2.js
group2/
store1.js
store2.js
store1.js
store2.js
then you will have such store list:
group1/store1
group1/store2
group2/store1
group2/store2
store1
store2
Please, keep in mind that all store names are case-sensitive.
Store Interface
As it is said every store should export a constructor function. Also you can define such methods and properties into constructor prototype, but all of them are optional.
- $lifetime – this field sets how long Catberry should cache data loaded from this store (milliseconds). By default, it is set to 60000ms.
load()
– loads and returns data (or Promise for it) from a remote resourcehandle<SomeActionNameHere>(args)
– does action and returns the result (or Promise for it). You can submit data to a remote resource here or just change some internal parameters in the store and then callthis.$context.changed()
. For example, the method can be namedhandleFormSubmit
and when action with nameform-submit
orform_submit
orFORM_SUBMIT
orformSubmit
orFormSubmit
will be sent by any component this method will be called
Please, keep in mind that stores are isomorphic and they are executing from
both server and client-side environments. Therefore you can not
use environment-specific global objects and functions like
window
, process
or DOM methods.
Store Context
Every store always has a context. Catberry sets the property $context
to every instance of each store. It has following properties and methods.
this.$context.isBrowser
– true if it executes in the browser environmentthis.$context.isServer
– true if it executes in the server environmentthis.$context.userAgent
– current user agent string of clientthis.$context.cookie
– current cookie wrapper objectthis.$context.location
– current URI object of current locationthis.$context.referrer
– current URI object of current referrerthis.$context.state
– the current set of parameters for current store parsed from routing definitionthis.$context.locator
– Service Locator of the applicationthis.$context.redirect('String')
- Redirects to specified location stringthis.$context.notFound()
- Passes request handling to the next middlewarethis.$context.changed()
– Triggerschanged
event for current store. You can use this method whenever you want, Catberry handles it correctly.this.$context.getStoreData('storeName')
– gets promise for another store's data, ifstoreName
is the same as current it will benull
.this.$context.sendAction('storeName', ‘name’, object)
– sends action to store by name and returns a promise of the action handling result. If the store does not have a handler for this action the result is alwaysnull
.this.$context.sendBroadcastAction(‘name’, object)
– the same as previous but the action will be sent to all stores that have handler for this action. Returns promise forArray
of results.this.$context.setDependency(‘storeName’)
– sets a dependency store for current store. Every time the dependency store changes, current store also will triggerchanged
event.this.$context.unsetDependency(‘storeName’)
– removes dependency store described in the previous method.
Every time router computes new application state it re-creates and re-assigns
context to each store, therefore, do not save references to this.$context
objects.
Please keep in mind that if you use getStoreData
method and data from
another store in load
method you should set that store as a dependency for
current store (this.$context.setDependency(‘storeName’)
), otherwise
cache of current store will not be updated if store-dependency is changed.
For example, you have two stores Country
and CityList
and you do
this.$context.getStoreData('Country')
in CityList.prototype.load
.
In this case, if Country
store is changed CityList
will not changed.
To avoid this just add this.$context.setDependency(‘Country’)
to
the CityList
constructor.
Also, there is one thing about setting a cookie, redirect
and notFound
methods.
If you use these methods while rendering document
or head
component it will
be done using HTTP headers and status codes on the server, otherwise it will be
rendered as inline <script>
tags.
notFound
method should be used only during rendering
of document
or head
components.
Code Example
This is an example how your store can look like:
'use strict';
module.exports = Some;
/**
* Creates a new instance of the "some" store.
* @param {UHR} $uhr Universal HTTP request.
* @constructor
*/
function Some($uhr) {
this._uhr = $uhr;
}
/**
* Current universal HTTP request to do it in isomorphic way.
* @type {UHR}
* @private
*/
Some.prototype._uhr = null;
/**
* Current lifetime of data (in milliseconds) that is returned by this store.
* @type {number} Lifetime in milliseconds.
*/
Some.prototype.$lifetime = 60000;
/**
* Loads data from a remote source.
* @returns {Promise<Object>|Object|null|undefined} Loaded data.
*/
Some.prototype.load = function () {
// Here you can do any HTTP requests using this._uhr.
// Please read details here https://github.com/catberry/catberry-uhr.
};
/**
* Handles action named "some-action" from any component.
* @returns {Promise<Object>|Object|null|undefined} Response to component.
*/
Some.prototype.handleSomeAction = function () {
// Here you can call this.$context.changed() if you know
// that remote data source has been changed.
// Also you can have many handle methods for other actions.
};
Cat-components
You may think cat-components are mustaches, paws or tail but they are not.
Cat-component is an isomorphic implementation of Google Web-Components. If dig deeper it is a subset of features that web-components specification declares.
The main point is that cat-component is a declaration of custom tag that can have own template (any template engine), own logic in JavaScript and own assets.
Cat-component is declared as a directory with cat-component.json
file by default. But you can change it in config.
When Catberry initializes it does recursively search for such directories
starting with your application root. It means you can publish and use
cat-components from npm.
cat-component.json
consists of following:
- name – the name of the component and postfix of custom tag (optional, by default it is the name of the directory).
- description – some additional information about cat-component (optional)
- template – relative path to component template (required)
- errorTemplate – relative path to component template for an error state (optional)
- logic – relative path to file that exports constructor for logic object (optional, index.js by default)
For example:
{
"name": "cool",
"description": "Some awesome and cool cat-component",
"template": "./template.hbs",
"errorTemplate": "./errorTemplate.hbs",
"logic": "./Cool.js"
}
In this example above you wil get a custom tag <cat-cool></cat-cool>
in your
application.
Please, keep in mind that all component names are NOT case-sensitive. If you declare component with the same name twice you will receive a warning message on startup.
After you define a cat-component you can use it like this:
<cat-cool id="unique-value" cat-store="group/store1" some-additional="value" ></cat-cool>
There are some important moments here:
- Every component tag should have an
id
attribute with a unique value, otherwise it is not rendered and throws an error - You can set
cat-store
attribute that means if store is changed this component will be re-rendered automatically - You can set any additional attributes you want without any problems
- You should always use open and close tags (not self-closing tags). The most of browsers do not support self-closing custom tags.
- You can use tags of other components in the template of any component
There are two reserved component names that are used in unusual way:
- document – the root template of the entire application (doctype, html, body etc.).
It can not depend on any store.
cat-store
attribute is just ignored. - head – the component for HEAD element on the page. It always is rendered in diff/merge mode otherwise all styles and scripts are re-processed every time. It can depend on a store and works as usual cat-component except rendering approach.
Cat-component Interface
As store component's logic file should export a constructor function for creating instances for every custom tag on the page. Also you can define such methods and properties into constructor prototype, but all of them are optional.
render()
– creates and returns data (or Promise for it) for component templatebind()
– creates and returns an object with event bindings (or Promise for it)unbind()
– this method is like a destructor and if you want to manually remove some listeners or to do something else you can implement this method
Some more details about bind()
method:
It is supported that bind()
method returns an object that describes all event
bindings inside the template of the current component. You can return binding object
(or Promise for it) like this.
Cool.prototype.bind = function () {
return {
click: {
'a.clickable': this._clickHandler,
'div#some': this._someDivHandler
},
hover: {
'a.clickable': this._clickableHoverHandler
}
};
};
As you may notice, every event handler is bound to the current instance of
the component, you do not need to use
.bind(this)
by yourself. Also, you can use bind method to make additional bindings outside
the component and then unbind it manually in unbind
method.
After a component is removed from the DOM all event listeners will be removed
correctly and then unbind
method will be called (if it exists).
Please, keep in mind that cat-component's constructor and render
methods
are isomorphic and they are executing from both server and client-side
environments. Therefore, you can not use environment-specific global objects
and functions like window
, process
or DOM methods inside these methods.
Cat-component Context
Every component always has a context. Catberry sets the property $context
to every instance of each store. It has following properties and methods.
this.$context.isBrowser
– true if it executes in the browser environmentthis.$context.isServer
– true if it executes in the server environmentthis.$context.userAgent
– current user agent string of clientthis.$context.cookie
– current cookie wrapper objectthis.$context.location
– current URI object of current location`this.$context.referrer
– current URI object of current referrer`this.$context.locator
– Service Locator of the applicationthis.$context.element
– current DOM element that is a root of the current componentthis.$context.attributes
– set of attributes are set when component was rendered the last timethis.$context.redirect('String')
- redirects to specified location stringthis.$context.notFound()
- Passes request handling to the next middlewarethis.$context.getComponentById(‘id’)
– gets other component by IDthis.$context.getComponentByElement(domElement)
– gets other component by DOM elementthis.$context.createComponent(‘tagName’, attributesObject)
– creates new component and returns promise for its root DOM elementthis.$context.collectGarbage()
– collects all components created withcreateComponent
method and that still are not attached to the DOM.this.$context.getStoreData()
– gets promise for store data if component depends on any storethis.$context.sendAction(‘name’, object)
– sends action to store if component depends on any store and returns a promise of the action handling result. If the store does not have a handler for this action the result is alwaysnull
.this.$context.sendBroadcastAction(‘name’, object)
– the same as previous but the action will be sent to all stores that have handler for this action. Returns promise forArray
of results.
Every time router computes new application state, it re-creates and re-assigns
context to each component, therefore, do not save references to this.$context
objects.
Also, there is one thing about setting a cookie, redirect
and notFound
methods.
If you use these methods while rendering document
or head
component it will
be done using HTTP headers and status codes on the server, otherwise it will be
rendered as inline <script>
tags.
notFound
method should be used only during rendering
of document
or head
components.
Code Example
This is an example how your cat-component can look like:
'use strict';
module.exports = Some;
/**
* Creates a new instance of the "some" component.
* @constructor
*/
function Some() {
}
/**
* Gets data context for the template engine.
* This method is optional.
* @returns {Promise<Object>|Object|null|undefined} Data context
* for the template engine.
*/
Some.prototype.render = function () {
};
/**
* Returns event binding settings for the component.
* This method is optional.
* @returns {Promise<Object>|Object|null|undefined} Binding settings.
*/
Some.prototype.bind = function () {
};
/**
* Does cleaning for everything that have NOT been set by .bind() method.
* This method is optional.
* @returns {Promise|undefined} Promise or nothing.
*/
Some.prototype.unbind = function () {
};
Example of Application Structure
Typically directory structure of your application should look like this:
./catberry_stores/
group1/
store1.js
store2.js
group2/
store1.js
store2.js
store1.js
store2.js
./catberry_components/
document/
index.js
template.hbs
cat-component.json
component1/
index.js
template.hbs
errorTemplate.hbs
cat-component.json
# directory for your own external not catberry modules/services
./lib/
# this directory is the default destination for browser bundle building
./public/
bundle.js
# entry script for the browser code
./browser.js
# route definitions
./routes.js
# entry script for the server code
./server.js
If you want to see finished application as an example then please proceed to the example directory.
Routing
Catberry's routing system triggers the "changed" event in every store that depends on changed arguments in routed URI. Those arguments are set by route definitions in file './routes.js'.
When you change the URI in a browser or send a request to the server Catberry computes the application state using those definitions and pass it to Store Dispatcher that controls everything related to stores.
After any store emits "changed" event every cat-component depended on this store will be also re-rendered in a browser.
Route definition is a rule that describes which URIs are handled by Catberry, what parameters Catberry can parse from these URIs and what stores will receive parsed parameters.
Colon-marked Parameters in a String
Default definition syntax is following:
/some/:id[store1,store2]/actions?someParameter=:parameter[store1]
All parameters should be marked with the colon at the beginning and optionally followed by the list of store names that will receive the value of this parameter. These stores are called stores-dependants. This list can also be empty.
In previous example id
value will be set to states of stores
store1
, store2
; and parameter
value will be set only to the state
of store store1
.
Please keep in mind that parameter name in route definition should satisfy
regular expression [^\[\],]+
and parameter value should satisfy
regular expression [^\\\/&?=]*
.
map
Function
Colon-marked Parameters with Additional Also, you can define mapper object, that allows you to modify application state object before it will be processed by Catberry.
If you want to use a map
function just define route like this:
{
expression: '/user/news/:category[news]',
map: function(state) {
state.news.pageType = 'userNews';
return state;
}
}
Map function receives the state prepared by route definition string. State is an object, where keys are store names and values are state objects for every store. You can change entire state object if you want and return it from a map function.
In this example, store news
will receive additional state parameter pageType
with value userNews
.
Regular Expression
For some rare cases, you may need to parse parameters by regular expressions. In these cases you can define mapper object as listed below:
{
expression: /^\/orders\/\d+/i,
map: function(uri) {
var matches = uri.path.match(/^\/orders\/(\d+)/i);
return {
order:{
orderId: Number(#matches[1])
}
};
}
}
In this example the store order
will receive parameter orderId
with value
matched with a number in URL.
URL with any Query Parameters
If the route definition includes any query parameters they are always optional. For example if you have such route definition:
/some/:id[store1,store2]/actions?a=:p1[store1]&b=:p2[store1]&c=:p3[store1]
Now if you try to route such URL:
/some/1/actions?b=123
you will receive the state:
{
store1: {
id: "1",
p2: "123"
},
store2: {
id: "1"
}
}
The parameters p1
and p3
will be skipped.
You can even route the URL without any query parameters at all.
/some/1/actions
and receive such state
{
store1: {
id: "1"
},
store2: {
id: "1"
}
}
File Example
Here is an example of ./routes.js
file with all 3 cases of the route definition:
module.exports = [
'/user/:id[user,menu,notifications]',
{
expression: '/user/news/:category[news]',
map: function(state) {
state.news.pageType = 'userNews';
return state;
}
},
{
expression: /^\/orders\/\d+/i,
map: function(urlPath) {
var matches = urlPath.match(/^\/orders\/(\d+)/i);
return {
orderId: Number(matches[1])
};
}
}
];
Catberry Services
Let's talk about Catberry Framework for isomorphic applications. In Catberry, every framework component such as Logger or Universal HTTP(S) Request module are called "Services".
Entire Catberry architecture is built using [Service Locator] (http://en.wikipedia.org/wiki/Service_locator_pattern) pattern and Dependency Injection. Service Locator is a Catberry core component that stores information about every Catberry's component (service). It is similar with IoC Container in other platforms. So, if any components depend on others it just says it to Service Locator and it creates instances of every required dependency. For example, every component and even userland Catberry module can ask for a "logger" service to log messages to the console.
When Catberry initializes itself it fills Service Locator with own set of services, but framework users can also register own plugins (services) and even overwrite implementation of some Catberry services. For example, you can replace Logger service in Service Locator with your favorite logger, you just need to write an adapter that matches the interface of Catberry "logger" service.
To register your own services, you should keep in mind that you probably need different implementations of your service for the server and the browser environment. But in some cases it does not matter.
Learn more how to use Service Locator in next section.
Service Locator
Entire architecture of Catberry framework is based on Service Locator pattern and Dependency Injection.
Registration of Own Services
There is only one service locator (singleton) in a Catberry application
and all Catberry services are resolved from this locator.
It happens when you use getMiddleware
method on the server or startWhenReady
method in the browser code.
Before that, feel free to register your own services.
Your Catberry application can have ./server.js
with code like this:
var catberry = require('catberry'),
RestApiClient = require('./lib/RestApiClient'),
connect = require('connect'),
config = require('./server-config'),
cat = catberry.create(config),
app = connect();
// when you have created an instance of Catberry application
// you can register everything you want in Service Locator.
// last "true" value means that the instance of your service is a singleton
cat.locator.register('restApiClient', RestApiClient, config, true);
// you can register services only before this method below
app.use(cat.getMiddleware());
// now Catberry already has initialized the whole infrastructure of services
app.use(connect.errorHandler());
http
.createServer(app)
.listen(config.server.port || 3000);
Also for the browser you application can have ./browser.js
with code
like this:
var catberry = require('catberry'),
RestApiClient = require('./lib/RestApiClient'),
config = require('./browser-config'),
cat = catberry.create(config);
// when you have created an instance of Catberry application
// you can register everything you want in Service Locator.
// last "true" value means that instance of your server is a singleton
cat.locator.register('restApiClient', RestApiClient, config, true);
// you can register services only before this method below
cat.startWhenReady(); // returns promise
// now Catberry already has initialized the whole infrastructure of services
Also, you can override some existing service registrations, for example, Logger:
cat.locator.register('logger', MyCoolLogger, config, true);
It registers one more implementation of logger and Catberry always uses the last registered implementation of every service.
You can also get an access to all registered implementations of any service
using resolveAll
method.
If you want to know how to use registered services, please read Dependency Injection section.
Dependency Injection
If you need to use some registered service you just inject it into a constructor of Store or Cat-component.
For example, you have the store called "AwesomeStore". In Catberry, every store is a constructor with prototype, to inject "logger" service, your own "restApiClient" service and someConfigKey from the config object you just can do that:
function AwesomeStore($logger, $restApiClient, someConfigKey) {
// here the $logger and the $restApiClient are instances
// of registered services.
// someConfigKey is a field from startup-config object.
// Every injection without '$' at the beginning is a config field
}
When you start an application in release mode this code will be optimized (minified) for the browser, but all these injections will stay as is and will not be broken.
Also, you can inject just $serviceLocator
and resolve everything you want
directly from locator.
It is really important not to make loops in the graph of dependencies. It causes infinite recursion and just kills your application.
Please keep in mind that config fields can be injected only side by side with service injections. If you want to inject only config section alone it will not work, use $config injection instead.
Read also:
Userland Services
Logger
Catberry has a universal logger service registered as "logger" in Service Locator and it is accessible via dependency injection.
Just inject $logger
into your store or component.
Also, it can be resolved from Service Locator directly.
This logger implementation has standard for almost every logger methods {trace, warn, info, error, fatal}. Last two support Error object to be passed as the only argument.
Actually when you use this service at the server it uses log4js module inside. It means you can configure it as described [here] (https://github.com/nomiddlename/log4js-node) in its README file.
In a browser, it is implemented as a very simple logger that can only write to the browser's console.
Configuration
To configure the browser logger you can set a config field logger
in the
Catberry config object.
Like this for browser logger:
{
"logger": {
"levels": "warn,error"
}
}
To configure the server logger you have to set the configuration like this:
{
"logger": {
"appenders": [
{
"type": "console",
"category": "catberry"
},
{
"type": "gelf",
"host": "logserver.example",
"hostname":"my.app",
"port": "12201",
"facility": "MyApp",
"category": "catberry"
}
],
"levels": {
"catberry": "TRACE"
}
},
}
More details here.
Config
Catberry has a configuration object registered as "config" service in Service Locator and it is accessible via dependency injection.
Just inject $config
into your module or resolve it from
Service Locator directly.
This service is just a full config object which was passed to catberry.create()
method.
Catberry uses following parameters from it:
- componentsGlob – glob expression for searching cat-components,
can be an string array or just a string
(
['catberry_components/**/cat-component.json','node_modules/*/cat-component.json']
by default) - storesDirectory – relative path to the directory with stores ("./catberry_stores" by default)
- publicDirectoryPath – path to public directory ("./public" by default)
- bundleFilename – name of the browser bundle file ("bundle.js" by default)
UHR (Universal HTTP(S) Request)
Catberry has Universal HTTP(S) Request service registered as "uhr" in Service Locator and it is accessible via dependency injection.
This is isomorphic implementation of HTTP request. All details you can find in UHR readme file [here] (https://github.com/catberry/catberry-uhr/blob/master/README.md).
Cookie
As you may notice, store and cat-component context have a property cookie
that
allows you to control cookie in isomorphic way.
Actually, it is a universal wrapper that can get
and set
cookie
in environment-independent way.
It has following interface:
/**
* Gets cookie value by name.
* @param {string} name Cookie name.
* @returns {Object}
*/
CookiesWrapper.prototype.get = function (name) { }
/**
* Sets cookie to this wrapper.
* @param {Object} cookieSetup Cookie setup object.
* @param {string} cookieSetup.key Cookie key.
* @param {string} cookieSetup.value Cookie value.
* @param {number?} cookieSetup.maxAge Max cookie age in seconds.
* @param {Date?} cookieSetup.expires Expire date.
* @param {string?} cookieSetup.path URL path for cookie.
* @param {string?} cookieSetup.domain Cookie domain.
* @param {boolean?} cookieSetup.secure Is cookie secured.
* @param {boolean?} cookieSetup.httpOnly Is cookie HTTP only.
* @returns {string} Cookie setup string.
*/
CookiesWrapper.prototype.set = function (cookieSetup) { }
/**
* Gets current cookie string.
* @returns {string} Cookie string.
*/
CookieWrapper.prototype.getCookieString = function () { }
/**
* Gets map of cookie values by name.
* @returns {Object} Cookies map by names.
*/
CookieWrapperBase.prototype.getAll = function () { }
Template Engines
Catberry supports any template engine that have the "precompiling to string" feature. Currently Dust, Handlebars, and Jade are officially supported but you can create own adapter for any template engine, just take a look how it is done for Handlebars.
To set template engine you just need to register template provider like this:
var handlebars = require('catberry-handlebars'),
cat = catberry.create(config);
handlebars.register(cat.locator);
Actually, Catberry CLI does it for you, see its [readme](Catberry CLI).
Browser Bundle
The Catberry application object has a method build
that can be used like this:
var catberry = require('catberry'),
cat = catberry.create();
cat.build(); // returns a promise
This method can be called in ./server.js
script or separately in
different script and process.
It is highly recommended to use build
method in separated process
(not in server process) because JavaScript minification requires a lot of memory
and it looks like your ./server.js
script uses 1GB of RAM, which is not so of
course.
For example you can use ./build.js
script like this:
node ./build.js release
To build browser bundle, Catberry uses browserify which is awesome and can convert your server-side JavaScript to browser code.
Including Packages into the Browser Bundle
There are some rules according browserify limitations:
- If you want to include some module into browser bundle it should be required
directly via
require('some/path/to/module')
. If module path is a variable browserify just skips it or throws an error. - If you want to exclude some server-side package from browser bundle or
replace it with browser version just use browserify
browser
field inpackage.json
as it has been described here.
Code Watching and Reloading
By default, Catberry works in debug mode and it means that all changes in code
of your stores or components will automatically reload everything.
You can switch application to release mode passing isRelease: true
parameter
in config object application like this:
var catberry = require('catberry'),
cat = catberry.create({isRelease: true}),
So, the difference between modes is:
- Debug mode - everything is watched by builder that rebuilds everything if something is changed
- Release mode - there is no watch on files and all code in the browser bundle is minified using uglify-js
Event Bus and Diagnostics
Catberry has a set of events that can be used for diagnostics or in components and stores. Catberry uses the same events for logging all trace, info and error messages.
There are two ways of listening to Catberry event:
- Subscribe on it using Catberry application instance directly like this
var catberry = require('catberry'),
cat = catberry.create();
cat.events.on('error', function (error) {
// some action
});
- Subscribe on it using the context of store or
cat-component using the same
on
,once
methods.
In a browser, you can access Catberry application instance via window
object
// catberry object is global because it is a property of the window
catberry.events.on('error', function (error) {
// some action
});
Actually cat.events
has interface similar with EventEmitter.
Event Names and Arguments
Here is a list of common Catberry events:
Event | When happens | Arguments |
---|---|---|
ready | Catberry has finished initialization | no |
error | error happened | Error object |
storeLoaded | each store is loaded | {name: String, path: String, constructor: Function} |
componentLoaded | each component is loaded | {name: String, properties: Object, constructor: Function, template: Object, errorTemplate: Object} |
allStoresLoaded | all stores are loaded | Map of loaded stores by names |
allComponentsLoaded | all components are loaded | Map of loaded components by names |
componentRender | Catberry starts rendering component | {name: String, context: Object} |
componentRendered | Catberry finishes rendering component | {name: String, context: Object, hrTime: [number, number], time: Number} |
storeDataLoad | Catberry starts loading data from store | {name: String} |
storeDataLoaded | Catberry finishes loading data from store | {name: String, data: Object, lifetime: Number} |
actionSend | Catberry sends action to store | {storeName: String, actionName: String, args: Object} |
actionSent | Action is sent to store | {storeName: String, actionName: String, args: Object} |
documentRendered | Catberry finishes rendering of all components | Routing context with location, referrer, userAgent etc. |
storeChanged | Catberry application's store is changed | Name of store |
stateChanged | Catberry application changed state | {oldState: Object, newState: Object} |
List of server-only events:
Event | When happens | Arguments |
---|---|---|
storeFound | each store is found | {name: String, path: String} |
componentFound | each component is found | {name: String, path: String, properties: Object} |
bundleBuilt | browser bundle is built | {hrTime: [number, number], time: Number, path: String} |
List of browser-only events:
Event | When happens | Arguments |
---|---|---|
documentUpdated | stores are changed and components are re-rendered | ['store1', 'store2'] |
componentBound | each component is bound | {element: Element, id: String} |
componentUnbound | each component is unbound | {element: Element, id: String} |
These events can be used for browser extensions, extended logging or component/store logic, feel free to use them everywhere you want but remember if any event has too many subscribers it can cause performance degradation.
CLI
Catberry has a Command Line Interface that helps to start a new project and add new stores and components to it.
To start using of Catberry CLI just install it globally from npm
npm -g install catberry-cli
And then follow usage instructions you can find here or just use the help of catberry utility:
catberry --help
Get Started
First of all you need to install CLI:
npm install -g catberry-cli
After that, you can create a project. So, create a directory for your new project and change to the new directory.
mkdir ~/new-project
cd ~/new-project
Now you can initialize one of the Catberry project templates.
Please choose one:
example
- finished project that works with GitHub API and demonstrates how to implement such isomorphic application using Catberry Frameworkempty-handlebars
- empty project using Handlebars template engine.empty-dust
- empty project using Dust template engine.empty-jade
- empty project using Jade template engine.
After you have chosen a template, please do the following:
catberry init <template>
Where <template>
is a chosen name.
For example,
catberry init empty-handlebars
Now you have created the project structure. Also you can see some instructions in the console that say how to install and start the application.
You need to install dependencies:
npm install --production
Then you can start your application, but you can start it using two modes:
- Debug mode – no code minification, watching files for changing and rebuilding
- Release mode – code minification, no watching files, production-ready
To start in release mode:
npm start
To start in debug mode:
npm run debug
Or
npm run debug-win
if you use Windows.
The application will say to you which port it is listening on. The address will be http://localhost:3000 by default.
Now you have your first Catberry application, create your own Stores and Cat-components.
The CLI can help you here as well.
For adding stores:
catberry addstore <store-name>
where <store-name>
is a name like some-group/Store
.
For adding cat-components:
catberry addcomp <component-name>
where <component-name>
is a name like hello-world
.
Hope now you are an expert in Catberry Framework. Enjoy it!
Plugin API
The entire Catberry's plugin API is based on the Service Locator and every plugin is just a service registered in the locator.
So, there are several ways how to register plugins.
Store Transformation API
You can register a plugin that does some transformations on loaded
stores. A Promise is supported as a returned value. A plugin can be registered
as an instance (locator.registerInstance
)
or as a constructor (locator.register
) like this:
locator.registerInstance(‘storeTransform’, {
transform: function (store) {
// store is loaded
// you can replace values, wrap constructor, or even build a new object
return newStore;
}
);
The store
parameter will be an object like this:
{
name: 'some store name',
constructor: function StoreConstructor() { }
}
Component Transformation API
You can register a plugin that does some transformations on loaded
components. A Promise is supported as a returned value.
A plugin can be registered as an instance (locator.registerInstance
)
or as a constructor (locator.register
) like this:
function ComponentTransform($config, $serviceLocator) {
// …
}
ComponentTransform.prototype.transform = function (component) {
// component is loaded
// you can replace values, wrap constructor, or even build a new object
return Promise.resolve(newComponent);
};
locator.registerInstance(‘componentTransform’, ComponentTransform);
The component
parameter will be an object like this:
{
name: 'nameOfTheComponent',
constructor: function ComponentConstructor() {},
// the object from cat-component.json
// you can store in these files whatever you want and use it in
// transformations
properties: {
name: 'nameOfTheComponent',
template: './template.hbs',
logic: 'index.js'
},
templateSource: 'template compiled sources here',
errorTemplateSource: 'error template compiled sources here or null'
}
Post-build Action API
You can register any plugin that does any actions after the browser bundle is
built. It can be assets building or some post-processing of files.
A Promise is supported as a returned value. A plugin can be registered
as an instance (locator.registerInstance
) or
as a constructor (locator.register
) like this:
locator.registerInstance(‘postBuildAction’, {
action: function (storeFinder, componentFinder) {
// you can get a list of found stores or a list of found components
// using storeFinder.find() and componentFinder.find() respectively
// every component object has "properties" field
// that contains cat-component.json values
return Promise.resolve();
}
);
find()
method returns a promise for a map object of
stores or promises by their names.
Every store
, in this case, will be an object like this:
{
name: 'some store name',
path: 'relative path to a store module'
}
Every component
, in this case, will be an object like this:
{
name: 'nameOfTheComponent',
path: 'path to a cat-component.json file',
// the object from cat-component.json
// you can store in these files whatever you want and use it in
// transformations
properties: {
name: 'nameOfTheComponent',
template: './template.hbs',
logic: 'index.js'
}
}
This type of objects above are called descriptors.
Every finder is an EventEmitter and has following events:
Store Finder
- add – a new store has been added to the application
- change – the store source has been changed
- unlink – the store has been removed from the application
- error – watch error occurs
Every event handler receives a store descriptor as the first parameter.
Component Finder
- add – a new component has been added to the application
- change – the component folder has been changed (any inner files)
- changeLogic – the component's logic file has been changed
- changeTemplates – the component's template or error template has been changed
- unlink – the component has been removed from the application
- error – watch error occurs
Every event handler except the change
event receives a component descriptor
as the first parameter, but change
event handler receives
an object with additional data like this:
{
filename: 'filename of changed file',
// component descriptor
component: {
name: 'nameOfTheComponent',
path: 'path to a cat-component.json file',
properties: {
name: 'nameOfTheComponent',
template: './template.hbs',
logic: 'index.js'
}
}
}
Browserify Transformation API
You can register a
browserify transformation.
A plugin can be registered as an instance (locator.registerInstance
) or
as a constructor (locator.register
):
locator.registerInstance(‘browserifyTransformation’, {
transform: function (fileStream) {
return transformStream;
},
options: { /* transform options will be passed to the browserify */ }
);
List of Officially Supported Plugins
- catberry-assets – The plugin that builds assets for every component using Gulp
- catberry-l10n – The localization plugin
- catberry-oauth2-client – The OAuth 2.0 client plugin that allows your stores to work with RESTful API using OAuth 2.0