Genji
Writing reusable, modular and flexible node.js applications made easy.
Introduction
Genji is not a full stack web framework, instead it focuses on making your code reusable and flexible enough to integrate with other components and frameworks. In development of modern web application, your service usually consumed by various kinds of clients with different manners. You may have web site for browser user, private apis for mobile clients, public apis for third-party developers and internal apis for queues or job workers. Genji helps you write reusable code by providing extensible plugin/middleware system, routing/transport agnostic application class, models fields validation and custom getter/setter method, views layout manager and namespaced template overriding, url routing with hook support. While it doesn't mean you have to stick with a particular development style or technology, genji is highly modular designed and customizable, so you can decide which part of the framework to use and how.
- For documentations see: http://lsm.github.com/genji
- Check out the test coverage and plato report for source analysis
- Ideas, bug report or general discussion are always welcome, feel free to open issue at: https://github.com/lsm/genji/issues
About the name "Genji"
The word Gen
Ji
in Chinese means root
base
and it's also the name of the
Japanese era name which was created to mark the beginning of a new 60-year
cycle of the Chinese zodiac. So, basically it means something fundamental that you can grow with.
The philosophy of this project follows this meaning.
License
(The MIT License)
Copyright (c) 2010-2013 Senmiao Liu senmiao.liu@gmail.com
The way to Genji
The simplest way to use Genji is to use it as a url based http request router
var genji = require('genji');
var http = require('http');
// create a router instance
var simpleRouter = genji.route();
// routing request to function by matching url
simpleRouter.get('^/$', function(context) {
context.send('Hello world!');
});
// create a http server object
var server = http.createServer();
// listen to request event
simpleRouter.listen(server);
// start handling request
server.listen(8888, '127.0.0.1');
This is regular request routing functionnality which you can find in a lots of frameworks of any language. Of course, this is not what all Genji can do. If you feel the above example already meets most of your needs, then go to Router to see detailed usages. And also go Context check out what it is and see how you can use and extend it. If it's not, and you feel this is too primitive. You have another option called Site which is more suitable for large/complex project. So you can change the way of using Genji during evolvement of your project and Genji helps you grow smoothly during that process.
Router
Router routes http request to designated handling function based on the requesting url. It supports calling hook function before/after dispatch to handling function. Router can be used standalone which means no site/middleware/app involved.
Add route definition
var genji = require('genji');
var router = genji.route({urlRoot: '/home'});
// handle GET request for url '/home/hello?title=Mr' (urlRoot + url)
router.get('/hello/(.*)', function(context, name) {
context.sendHTML('Hello, ' + context.query.title + ' ' + name);
});
// 'urlRoot` will not be prefixed before your url if it starts with '^'
router.post('^/post', function(context) {
// post parameters will be parsed if context listen to the 'params' event.
context.on('params', function(params) {
// do something
context.send('ok');
});
});
Hooks
You can use Hooks
to do some tasks before and after dispatch. Hook functions together with handle function will be chained
into an array by router. And all functions in chain will be called in order during dispatch. During the dispatching process,
you must explicitly call next
or return true
in hook function to call the next function in chain.
We use null
to mark the position of the dispatch function. During dispatch, the null
placeholder will be replaced by
handle function. Some special cases when:
- null
is at beginning of the array, all functions are post hook (e.g. [null, fn1, fn2])
- null
is not presented or at the end of array, all functions are pre hook (e.g. [fn1, fn2] === [fn1, fn2, null])
- the hook is a function, it means the function is a pre hook (e.g. fn1 === [fn1])
function preHook(context, next) {
// you can call `next` asynchronously, the next function in chain won't be called
// until you call `next`
setTimeout(next, 1000);
}
function postHook(context, next) {
// if you return true, the next function in chain (if any) will be called immediately
return true;
}
router.get('/hooked', function() {}, [preHook, null, postHook]);
Listen to server event
You can use router directly with HttpServer instance.
// listen to 'request' event of HttpServer instance
router.listen(server);
If you have more complex project, please use Site with App.
API
Router
exposed by require('genji').Router
. Instance can be created by
var router = require('genji').route(options);
genji.route(options:Object)
Takes optional options and create a new genji.Router
instance.
Supported options:
urlRoot
is the base url of the router, default value is '^/'.contextClass
the default context class for each request. See Context for more info.
Methods
{get|post|put|delete|head}(url:{String|RegExp}, handler:Function, /*optional*/ options:Object)
The get/post/put/delete/head
route defining methods of Router
instance have the same signature:
url The RegExp instance or string representation of it, string will be converted to regular expression. If it's string, router will check if the given string starts with
^
. If not theurlRoot
will be prepended to the string.handler Function to handle the request, the handling function has following signature:
function handler(context, /*optional*/ matched...) {}
context
Instance object ofContext
matched
Optional values matched from your url regular expression (e.g. /path/to/item/([0-9]*) )
hooks a function or array of functions which will be called during dispatch
Routing rules are grouped by http method. Previous rule could be overriden by subsequently calling routing method with same http method and url. That means you can have different handling functions for the same url with different http method.
mount(urls:{Array})
Add batch of routing rules at once. Each element in the urls
array should be an array with members of following order:
url
same as described above inget/post/put/delete/head
methodshandler
same as described abovehttpMethod
Optional http method that accepted, default to 'GET'options
Optional same as described above
notFound(url:{String|RegExp}, handler:Function)
The miss matched matcher. You can use it to handle miss matched requests based on different url.
router.notFound('^/blog/*', function(context) {
// for miss matched url start with `/blog/`
});
router.notFound('^/*', function(context) {
// for any other cases
});
Router#route(type:String, url:String, context:Object, /*optional*/ notFound:Function):Boolean
The routing function, it takes input and try to match with existent rules. Return true
on matched otherwise flase
.
type
is type of requestGET|POST|PUT|DELETE|HEAD|NOTFOUND
url
is the request urlcontext
is thethis
object for dispatchingnotFound
is an optional function which will be called when no rule matched the given input
Router#{hook}(fn:{Function|Array})
Add pre/post hook(s) to all existent url routing rules in the current router instance. Takes one argument which could be
function (pre hook) or array of null positioned
hooking functions. This method adds pre hook to the left
side (begin)
of existent pre hooks and adds post hook to right
side (end) of existent post hooks.
Router#listen(server:{HttpServer}, /*optional*/ notFound:Function)
Listen to request
event of the vanilla node.js HttpServer
instance.
server
is an instance of HttpServernotFound
is an optional function which will be called when all routes miss matched in the router. A default function that responds 404 to client and output error tostderr
will be called if you omit the function
App
An app is a class
where you define and implement your core business logic. App it self is an event emitter.
Properties can be overridden by subclassing existent app.
Define app
You can define an App
like this:
var App = require('genji').App;
var BlogApp = App(/* instance properties */ properties);
Let's go through a simple example to see how we can define an app.
var App = require('genji').App;
var BlogApp = App({// instance properties object
/**
* Name of your app
*/
name: 'Blog',
/**
* App constructor function
*/
init: function(db) {
this.db = db;
},
/**
* Create a blog post
*/
createPost: function(title, content, callback) {
// Make the post object
var post = {title: title, content: content, created: new Date()};
// save to the db
this.db.save(post).then(function(result){
// error first callback style
callback(null, result);
});
}
});
The BlogApp
you just defined can be used as a standalone application class. It means you don't need the http stack to run your app code. Once you get the BlogApp
class, you initialize and use it just like any other classes. The init
function will be called on initialization and it's optional.
Handle result
There are two different ways to handle result of the instance function. One is to add callback as the last argument and handle result inline. Another is to listen to the event emitted from object of app instance.
Inline callback style
var myBlog = new BlogApp(db);
// create a blog post in db
myBlog.createPost('My awesome post', 'Some contents', function(err, result) {
// handle error and result here
// this == myBlog, so you can emit event manually
this.emit('createPost', err, result);
});
Event style
Default callback
Sometime you don't care about if the post is saved or not. And you may wish to handle the result in other part of your code. So when you call instance function and the last argument is not a callback, genji will generate a default callback for you. You call the callback as usual and an event with the name of the instance method will be triggered.
// the event callback's argument is same as how you call the callback inside the "createPost" function
myBlog.on('createPost', function(err, result) {
// handle the event
});
//...
// create and forget
myBlog.createPost('I do not care about the result', 'Yes, there is no callback after me.');
There is one exception. When your instance function is synchronized and returns non-undefined value on calling. The event/callback will not be emitted/invoked in async operation.
getDB: function(cb) {
cb(); // will call `theCallback`, if no callback gived event will be emitted
process.nextTick(function(){
cb(); // `theCallback` will not be called end emit event
});
return this.db;
}
function theCallback() {
// this function will never be called in async operation
}
var db = myBlog.getDB(theCallback);
Delegation
You can delegate all the events to other event emitter object by setting the delegate
property to that emitter.
var otherEmitter = new EventEmitter();
myBlog.delegate = otherEmitter;
// event name will be prefixed by app name on delegation by default
otherEmitter.on('Blog:createPost', function(err, result) {
// handle the event
});
myBlog.on('createPost', function(err, result) {
// this will never be called as you allready delegate events to `otherEmitter`
});
myBlog.createPost('title', 'content');
Extending
It's easy to extend an existent app class, use the app class you want to extend just like you use App
:
var AwesomeBlogApp = BlogApp({
name: 'AwesomeBlog',
createPost: function(title, content, callback) {
// ...
},
getPost: function(id, callback) {
// ...
}
});
Conventions
App introduced a very thin mechanism to organize business logic code. To make app works simply and efficiently with other part of system, here are the conventions you may need to know and understand.
Session
Although we call defined app class
, but actually we use individial instance functions in a functional programming
style in conjuction with Site and Router. This allow genji to reuse a single app instance
for multiple requests instead of constructing for each one of them. You may noticed that, there's no session info passed
to createPost
in the above example, this is not possible in a real world application. So Genji put the session object
which may contains credential or other user specific info as the first argument of app's instance function when working
with Site. So your instance function will have some sort of session first callback last
style of function signature.
createPost: function(session, title, content, callback) {
if (session.group === 'author' && session.user) {
// Make the post object with user info
var post = {
user: session.user,
title: title, content: content, created: new Date()
};
this.db.save(post).then(function(result){
callback(null, result);
});
} else {
// do something
}
}
To make the app class more reusable, don't put any http stack object as the function argument
(e.g. request/response object of node.js etc.). That will make you loose the flexiblity and coupled with http stack.
Leave that job to genji.Site
to make you life easier.
The this
object
You can always use this
refer to the instance of app inside of instance function, callback function and event listening function.
Naming and url mapping
Site use name
property of app and it's functions' name for mapping url automatically. For example:
blog/ceate/post
to matchBlog
app'screatePost
function.awesomeblog/create/post
map toAwesomeBlog
'screatePost
.blog/camel/cased/function/name
to matchBlog
'scamelCasedFunctionName
function.- if the function name start with low dash
_
(e.g._privateFunc
), no url will map to this function.
Error first callback style
We follow the native node.js api's callback style, which put the error object at the first argument of a callback function. It's true for the event listening function as well.
API
App
is exposed by require('genji').App
. Inherits from EventEmitter.
Properties
name
is the name of your app in string, name should be upper camel case.emitInlineCallback
is an enum value ('after', 'before', false) which tells genji automatically emit eventafter
/before
callback is called when you handle result inline. Default is booleanfalse
which means not to emit.prefixDelegatedEvent
is an enum value indicates whether or not to prefix the event name when usingdelegate
as emitter:true
event name will be prefixed, the prefix is'name of app' + ':'
(e.g.createPost
->Blog:createPost
). This is the default valuefalse
not to prefix- Any non-empty string value as customized prefix
publicMethods
is an object of functions created upon initialization which considered as public methods,controller
uses this property to map url to function.reservedMethodNames
is an array of reserved names which cannot be used as public method, the default value is:["setMaxListeners","emit","addListener","on","once","removeListener","removeAllListeners","listeners", "init", "isPublicMethodName"]
site
is thesite
instance when works withgenji.site
is optional but reserved
Methods
init
is the constructor function, it will be called once and only once at the time of initialization, you should not call it manually.isPublicMethodName
is a function use to check if a string can be used as public method name or not. The default rule is:The name must be a non-empty string and must not equal to one of the
reservedMethodNames
and not start with lower dash_
.
Site
Site is the organizer for your applications. And using Site is the recommended way to use Genji when you have a large/complex project. It has the following features:
- It inherits
EventEmitter
- It has setter/getter methods which can be used to save and retrieve
settings
- It can load and expose your
app
to external world and manage maps between url and app function - It works with Core - the
plugin
system - It works with
view
and renders result by convention - Settings, plugins and apps are all namespaced by
env
A site
object can be created by calling genji.site()
:
var mySite = genji.site();
Getter/Setter
You can use mySite
to set and get settings.
mySite.set('title', 'My Great Website');
// 'My Great Website'
var title = mySite.get('title');
// set a batch of settings
mySite.set({
host: '127.0.0.1',
port: 8888
});
// get a batch of settings
var settings = mySite.get(['title', 'host', 'port']);
// {title: 'My Great Website', host: '127.0.0.1', port: 8888}
Use middleware
You don't have to initialize middleware manager by yourself when you use site. You only need to call use
method of the
site instance.
// use built-in middlewares
mySite.use('conditional-get');
mySite.use('logger', conf);
// use custom middleware
mySite.use(function(core, conf) {
// do something
}, conf);
Load app
Using app with site is as simple as middleware. But instead of use
you need to call load
with app instance.
var blog = new BlogApp();
mySite.load(blog);
// site instance will be setted to app instance's `delegate` property
// blog.delegate === mySite
If you have a lots of apps which have similar initializing options. Then you can set the default app options and let site initialize them for you.
var options = {x: 1, y: 2};
mySite.set('appOptions', options);
mySite.load(BlogApp);
mySite.load(MyOtherApp);
The default options could be overridden and inherited.
var someOptions = {y: 3, z: 2};
// the actual intializing option is {x: 1, y: 3, z: 2}
mySite.load(SomeApp, someOptions);
// load three apps at once
mySite.load([SomeApp1, SomeApp2, SomeApp3], someOptions);
Routing
All methods in the app.publicMethods
property will be mapped to url by default follows the convention.
For example, the blog.createPost
will be mapped to url that matches ^/blog/create/post
by default.
Of course, if the default convention/mapping does not meet your needs, you can use the map
function to map individual
url to app method and override the default options.
mySite.map({
// handle `POST` requests for url '/blog/create/post'
blogCreatePost: {method: 'post'}
// handle both `GET` and `POST` requests for url '/blog/read/post/:id'
blogReadPost: {url: '^/blog/read/post/[0-9]{16}'},
// add hooks
blogUpdatePost: {method: 'put', hooks: [preFn1, null, postFn1]}
});
If you have predefined routing definition object, you can load it at the same time when loading app.
var routes = {
// it's an app default settings if the property key of the route is one of the app's name (in lower case)
blog: {hooks: [fn1, null, fn2], method: 'get', urlRoot: '^/blog/'},
blogReadPost: {url: '^/blog/read/post/[0-9]{16}'},
// a comprehensive route definition
blogCreatePost: {
url: '/create/post',
method: 'post',
hooks: [fn3, fn4, null, fn5],
view: 'json' // see [Output result](#output-result)
}
};
mySite.load(AnotherApp, someOptions, routes);
The routes.blog
object holds default settings for all publicMethods
of blog
instance. It means if some setting is
not specified in route, it will use the default one provided by routes.blog
. And hooks will be combined. So the final
routes of above example normalized by genji would be:
var routes = {
blogReadPost: {
url: '^/blog/read/post/[0-9]{16}',
method: 'get',
hooks: [fn1, null, fn2],
view: 'html' // site default value, see [Output result](#output-result)
},
// a comprehensive route definition
blogCreatePost: {
url: '^/blog/create/post',
method: 'post',
hooks: [fn1, fn3, fn4, null, fn5, fn2],
view: 'json' // see [Output result](#output-result)
}
};
Output result
We already knew how to map a url to an app's method. Let's see how we can output the result.
- Output the result as html string (default):
blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: 'html'}
- Output the result as json string:
blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: 'json'}
- Handle result by customized function:
blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: function(err, result) {
if (err) {
this.error(err);
return;
}
this.sendJSON(result);
}}
When you use genji's view
system, site can render view template automatically for you.
var hogan = require('hogan.js');
mySite.use('view', {engine: hogan, rootViewPath: __dirname});
mySite.map({
// render template file at "rootViewPath + /blog/views/read_post.html"
blogReadPost: {url: '^/blog/read/post/[0-9]{16}', view: '/blog/views/read_post.html'},
// the default behavior is try to find a template file at "rootViewPath + /blog/update_post.html" if you use the view system
blogUpdatePost: {method: 'put'}
});
Of course, it's possible to do some tweaks and render manually.
blogUpdatePost: {method: 'put', view: function(err, result) {
result.someValue = 1;
// auto template file discovery, render and send
this.render(result);
// or indicates template view name manually
this.render('blog/update_post.html', result);
var self = this;
// or render and send manually
this.render('/path/to/blog/views/update_post.html', result, function (err, html) {
self.sendHTML(html);
});
}}
Environment
By using env
you can have different settings, middlewares and apps for different environments. The default environment
named default
.
// switch to environment "dev"
mySite.env('dev');
// the following title and middleware will be used if your 'process.env.NODE_ENV' === 'dev'
mySite.set('title', '[DEV] My Great Website');
mySite.use('dev-verbose', 'all');
// the "DevApp" is also only avaliable for "dev" environment
mySite.load(DevApp);
// switch back to the 'default' environment
mySite.env(); // or mySite.env('default')
The dev
environment inherits default
. It means all settings, middlewares and apps setted before you switched to dev
are also setted/used/loaded. So when you switched to new environment, set/use/load
overrides the differences.
Start your site
Start the server is easy
mySite.start();
Base
The base module exports two classes
Klass is a lightweight javascript OO implementation. It only gives you a way to inherit class and nothig more.
Base is a feature rich javascript OO implementation. It supports mixin for instance/static property.
Klass
Klass has follwoing features:
- instance property will be overridden during subclassing
- no reference to the parent overridden method (aka.
super
) in instance method instanceof
operator works, butKlass
itself is not parent class of any subclass defined- the root super class's constructor cannot be overridden and will always be called during initialization
Klass(SuperClass:Function, module:Object, /*optional*/ inherit:Function):Function
SuperClass
is constructor function of your parent classmodule
is an object of instance properties which you want to override or add to the subclassinit
is a reserved property in module object. If it's a function it will be called during initialization after SuperClass constructor function has been called. Theinit
defined in parent class will be overridden by subclass just like normal property.
inherit
is an optional function for changing the default inheriting behaviourreturns
Subclass
which you can continue to subclass by callingSubclass(module:Object, /\*optional\*/ inherit:Function)
the initial SuperClass constructor function (root super class) will always be called and cannot be overridden during subclassing
Example
var Klass = require('genji').Klass;
var Person = Klass(function(name){
this.name = name;
}, {
getName: function(){
return this.name;
}
});
var Worker = Person({
init: function(name) {
this.name += ' Worker';
},
work: function(task) {
return this.getName() + ' start working on ' + task;
}
});
var steve = new Worker('Steve');
var result = steve.work('writing report');
// 'Steve Worker start working on writing report' true true
console.log(result, worker instanceof Worker, worker instanceof Person);
Base
(Coming soon)
CHANGELOG
0.7 (2013)
0.7.0 (2013/03/31)
NOTE: This version is not compatible with version 0.5.x
- Documentation
- Introduce
Klass
- lightweight Javascript OO implementation - Introduce
Core
- the plugin/middleware system - Introduce
Site
- to avoid global shared object in
lib/genji.js
- setting management namespaced by environment
- coporate with
Core
and map routes to apps
- to avoid global shared object in
- Replace
Handler
withContext
, convert some handlers to plugins - Refactor
router
- allow routing without middleware
- overwrite routes which has same url pattern and http method
- Rewrite
App
- use
Klass
- separate routing from App logic
- remove support of static properties
- use
- Rewrite
Model
- use
Klass
- remove support of static properties
- use
- Remove
Role
- Remove
Base
- Remove file plugin and lib/mime.js
- Refactor
view
0.5 (2012)
0.5.10 (2012/12/15)
App
clone routes object before useHandler
keep parsed and raw http request data
0.5.9 (2012/12/15)
View#minify
json
,html
,text
shortcuts forApp#routeResults
0.5.8 (2012/11/24)
handler#sendAsFile
- allow customize response headers
- detect Buffer when calculate data length
0.5.7 (2012/11/06)
util#expose
export sub modules- remove
client.js
Model
toData
andtoDoc
accept array as argument to indicate the fields you need to get.App#routePreHook
support bulk set prehook for array of routes
0.5.6 (2012/10/31)
crypto#decipher
handle exception 'TypeError: DecipherFinal fail' when decipher string with different key ciphered
0.5.5 (2012/10/26)
control
defer().defer(otherDeferrable)
defer().callback(cb)
0.5.4 (2012/09/14)
- set default context for layout
- batch attache prehook to routes
- allow set cookie during redirection
- default options for
Role
0.5.3 (2012/08/17)
- node 0.8.x compatibility
0.5.2-2 (2012/07/21)
- Bug fix for
Model
0.5.2-1 (2012/07/18)
- Bug fix for
App
0.5.2 (2012/07/05)
Model
- new field type and type validator:
array
,regexp
,date
,bool
- dynamic fields validation status
- instance function accepts callback function as last argument,
call
this.emit()
as usual will call the callback and event won't be emitted. - use
toDoc()
instead oftoData('alias')
- bug fix for type validation
- new field type and type validator:
App
- bug fix for instance function not return result
0.5.1 (2012/06/16)
View
- add script loader support (head.js)
- change
addViewPath
tosetViewPath
- add support for default context (e.g. var view = new View(engine, {context: {title: 'Title'}});)
- merge
BaseView
withViewWithCompiler
- basic
layout
manager
Model
model.attr([key1, key2])
get group of attributes as hash object- Bi-direction aliased field name
App
support application level and route levelroutePreHook
- Introduce
Role
0.5.0 (2012/06/05)
- external app loader
- New
App
module genji.app
renamed togenji.route
0.3.3 (2012/05/19)
- expose submodules by default,
genji.short
andgenji.require
are deprecated - rewrite
lib/model.js
, added test - support multi-root path for view template (with namespace)
- add
Model#changed
, return object which contains changed fields/values after initialized. - add
util#byteLength
0.3.2 (2012/05/08)
- improve
view
andmodel
0.3.1 (2012/03/12)
- improve
view
partial - introduce
model
0.3.0 (2012/01/05)
- introduce
view
for working with template engine like hogan.js- render files
- simple caching support
- preregister partial files in the
rootViewPath
0.2.4 (2012/01/02)
- add
send
,sendJSON
andsendHTML
to thebase#BaseHandler
- introduce
event
incontrol#defer
0.2.3 (2011/12/15)
- Upgraded
expresso
for node 0.6.x support
0.2.2 (2011/08/29)
- Cleaned code follow default jshint rules
- Rewrited
client.js#Client
and added some tests
0.2.1 (2011/08/11)
- Add timeout support for
control#parallel
- bug fix for handler#Simple#sendJson
- new event for handler with method
POST
/PUT
:data
for raw dataparams
for http query string, parsed as plain javascript objectjson
for json string, parsed as plain javascript object
0.2.0 (2011/07/10)
- Changed the way how we define sub url for
App#[get, post, put, del, head]
control#defer
- put flow control object as the first argument of
and
callback - callbacks of
and
,then
now will be called in registered orderdefer(fs.readFile, fs).and(fn1, fn2, fn3).then(fn4, fn5).and(fn6, fn7)
functions will be called in the following order: fn1->fn2->fn3->(fn4&fn5)->fn6->fn7
- put flow control object as the first argument of
util#extend
takes unlimited argumentsextend(obj, props1, props2, ..., propsN)
- Add
crypto#hmac
handler#Simple
parses url parameters by default- Add context for
control#parallel
0.1.0 (2011/06/29)
- Remove a lots of functionalities, keep small and focus.
- Introduce
App
control#parallel
set done/fail callbacks byparallel.done(fn)
andparallel.fail(fn)
- add
genji.short
: extends thegenji
namespace withutil
base
control
submodules - Simplified
handler
,- the default handler is
genji.require('handler').Handler
which can handle normal http request, parse cookies and send files - you can use
genji.require('handler').BaseHandler
to include features you only need
- the default handler is
0.0.3 (2011/06/13)
- etag for sending file-like contents
- util
- crypto
- new shorthand functions for cipher/decipher
- enable to select the digest encoding for
md5
,sha1
,hmac_sha1
- crypto
- web
- middleware
- now the middleware does not care about application settings
- new style middleware config format
secure-cookie
new middleware to encrypt/decrypt cookie
- router, new url routing system, supports declarative and programmatic style of defining url rules.
- move
createServer
/startServer
into submoduleweb.server
,web/index.js
only use to export submodules
- middleware
- pattern
- control
- promise: call the original async function in next tick
- math, new pattern group, add
random
- control