coeus
v0.9.6
Published
A tiny front-end framework to build SPA with React and webpack
Downloads
3
Readme
Coeus
A tiny front-end framework to build SPA with React and webpack. It contains:
- Internal redux.
- A routes-loader for webpack.
- A Router component.
Quick Start
A project template named coeus-project is provided. In the project, a welcome page can show you how coeus build powerful but neat application. And two default configuration for development and production can help you start building without heavy setup work.
Install
npm install --save coeus
Usage
After installing, all things you can import to use is in the namespace coeus/lib
which has the same folder shape as coeus/src
. In fact, coeus/lib
is built with babel src --out-dir lib
.
coeus/lib
polyfill
: Two polyfill used by coeus for old browsers, use them when you needObject.assign
Promise
reducers
: Reducers for coeus componentsrouter
: The reducer for Router
redux
: Internal reduxcreateStore
combineReducers
combineSubscribers
utils
: utils for webpackroutes-loader
Router
: Router
If you simply import Coeus from 'coeus'
, that is actually equal to import Router from 'coeus/lib/Router'
.
Internal redux
Coeus refactor redux to add some more powerful features and make it internal.
diff with redux
- Promise supported in reducers and subscribers. Feel free to return simple values and objects of Promise in reducers and subscribers.
- Completely support dynamic reducer.
replaceReducer
is deleted from the exposed API ofstore
andmountReducer
(a new API) is the only way to add new reducers to store. - New function
combineSubscribers
in utils. It accepts an object of subscribers and returns a function to maintains the previous states for keys so that the returned function only notify subscribers when states for the keys has been changed. combineSubscribers
andcombineReducers
does not have to be called explicitly.mountReducer
andsubscribe
will call them internally. What's more, not only a flatten object but also a tree object can be passed tocombineSubscribers
andcombineReducers
because they recursively call themselves to handle tree objects.createStore
only accepts an optional parametermiddlewares
, no reducer, no initial state, no enhancer.INIT
action is separated fromcreateStore
and has become a standalone APIinitState
of store.dispatch
always returns a Promise object and doesn't warn any more when you dispatch an action during the previous action is still dispatching. Store holds the previous promise object so it can chain dispatching in the sequence that you calldispatch
.
API
[createStore(middlewares): store] (Object)
: The factory to create a store object.middlewares
is an array ofmiddleware
. The returned store object exposes 5 APIgetState
,mountReducer
,subscribe
,dispatch
andinitState
.[middleware(action, next): action] (Object / Promise)
: Middleware for dispatching pipeline. Support Promise.next
is a closure has the same interface ofdispatch
(only has anaction
parameter and returnaction
wrapped in Promise)[next(action): action] (Promise)
.[getState(): state] (Object)
: Get the state of the store.[mountReducer(reducer): unmountReducer] (function)
:reducer
can be a function or a tree object of reducers. In fact, there is no magic to support dynamic reducer withmountReducer
but achieving a merge & separate algorithm for tree. Once we handle the reducers as tree,combineReducers
can transfer the reducer tree to a single reducer function that describes the state shape:)
[subscribe(listener): unsubscribe] (function)
:listener
can be a function or a tree object of listeners. It should be mentioned that listeners will never affect each other, which means different from the organization of reducers, all listeners will never be merged to a big tree but simply passed tocombineSubscribers
to return a single listener function finally held in listeners list. Additionally, thanks to the initial states for keys isundefined
incombineSubscribers
, you can listen to non-existent keys of the state tree, and once these keys are created by reducers, the listeners can catch them regarded as they has been changed.[dispatch(action): action] (Promise)
: Always return a Promise object. Internally chain dispatching.
// internal chaining
store.dispatch({ type: 'action1' });
store.dispatch({ type: 'action2' });
// is equal to
store.dispatch({ type: 'action1' }).then(() => {
store.dispatch({ type: 'action1' });
});
[initState(): action] (Promise)
: DispatchINIT
action.
use with React / Component Data Flow Convention
The powerful core of Coeus is here! With the internal redux, all components built with coeus should have a consistent data flow convention. The convention help users of coeus neaten the data flow of applications and guide the building of components.
Here it is:
We should
setState
beforeinitState
because React doesn't support Promise incomponentWillMount
and can not guaranteerender
to run before asyncinitState
finished. Initializing an empty but valid state beforerender
is a little hack to make things work fine in the first rendering.
When you build a component, there are two parts you should think of. One is the component class extends React.Component
, the other is reducer. The former describes the lifecycle of the component. The latter separates the complex data flow from the component.
As shown above, reducer factory
is not necessary if there are not props
participating in the data flow. render
only accepts the state from listener
which means props
should not be seen in render
any more.
Nothing more to tell. The best guide for coding with Coeus is the code of Router component and its reducer(reducer factory exactly).
Routes Loader
load components on demand
define routes shape in yaml
# save as ./routes.yml
path: '/'
components: 'foo'
children:
- components: [ './foo', './bar' ]
load routes with webpack
// es6
import routes from 'coeus/lib/utils/routes-loader!yaml!./routes.yml';
// commonjs
var routes = require('coeus/lib/utils/routes-loader!yaml!./routes.yml');
// webpack.config.js
module: {
loaders: [
{ test: /routes\.yml$/, loaders: [ 'coeus/lib/utils/routes-loader', 'yaml' ], exclude: /node_modules/ }
]
}
import routes from './routes.yml';
var routes = require('./routes.yml');
console.log(routes) will show you something
"path": "/",
"components": ["foo"],
"_components": function() {
return new Promise(function(resolve) {
require.ensure([], function(require) {
resolve([require("foo").default || require("foo")]);
});
});
},
"children": [{
"components": ["./foo", "./bar"],
"_components": function() {
return new Promise(function(resolve) {
require.ensure([], function(require) {
resolve([require("./foo").default || require("./foo"), require("./bar").default || require("./bar")]);
});
});
}
}]
If you don't know what it exactly means above, it is recommended to read webpack manual first.
regex path & named arguments
define path
path: /foo<?bar:\\d{1,2}>/bar.foo<foo:\\w{17}[abc]>_tail_
accordingly get two internal properties from routes module
"_path": "/foo(\\d{1,2})?/bar\\.foo(\\w{17}[abc])_tail_",
"_params": ["bar", "foo"]
Things is clear. We can easily match path and capture named arguments with the internal properties. The supported format for regex path & named arguments is
relative components path
define routes
path: /
components: ':foo'
children:
- components: ':foo'
- path: mocha/mocha
componentsPath: './components'
components: ':foo'
children:
- components: ':foo'
accordingly get
"path": "/",
"components": [":foo"],
"_components": function() {
return new Promise(function(resolve) {
require.ensure([], function(require) {
resolve([require("foo").default || require("foo")]);
});
});
}
"children": [{
"components": [":foo"],
"_components": function() {
return new Promise(function(resolve) {
require.ensure([], function(require) {
resolve([require("foo").default || require("foo")]);
});
});
}
}, {
"path": "mocha/mocha",
"componentsPath": "./components",
"components": [":foo"],
"_components": function() {
return new Promise(function(resolve) {
require.ensure([], function(require) {
resolve([require("./components/foo").default || require("./components/foo")]);
});
});
},
"children": [{
"components": [":foo"],
"_components": function() {
return new Promise(function(resolve) {
require.ensure([], function(require) {
resolve([require("./components/foo").default || require("./components/foo")]);
});
});
}
}]
}]
As you seen, routes loader support relative components with componentsPath
property and :
prefix. Two rules should be mentioned:
- All
:
prefixes in components will be replaced to the closestcomponentsPath
in itself, parent or ancestors; - If there is no
componentsPath
found,:
will be replaced to an empty string.
named routes
give the leaf node of routes a name
path: /foo<?bar:\\d{1,2}>/bar.foo
children:
- path: '_tail_'
name: 'foo'
- path: '<foo:\\w{17}[abc]>'
name: 'bar'
an extra internal property _names
will be found in the top level of routes
"_names": {
"foo": {
"pathTemplate": "/foo<bar>/bar.foo_tail_",
"paramsRegex": {
"bar": "\\d{1,2}"
},
"paramsOptional": {
"bar": true
}
},
"bar": {
"pathTemplate": "/foo<bar>/bar.foo<foo>",
"paramsRegex": {
"bar": "\\d{1,2}",
"foo": "\\w{17}[abc]"
},
"paramsOptional": {
"bar": true,
"foo": false
}
}
}
With the _names
property, it's easy to generate a path matching the named route when given the name and arguments.
match & link
Hoped the internal sight for routes module does not upset you. Talking about internal properties only help you understand what's going on in routes loading but should never bother you when using routes to match a path or generate a path given route's name and arguments. Two functions match
and link
exposed in routes module is designed to take the work.
[match(path): result] (Promise)
: Given a path,match
will take a depth-first matching on the routes tree. Allpath
properties on every route path of the tree will be concated to match the given path. Once a route path is matched, the algorithm continously look for the tail node of the route path whether there is a node withoutpath
property in its children, in short, look for an index child. After that, matching is finished and the result generated during the traverse will be returned(wrapped in a Promise). If not match, false will be returned.
[link(name, args): path] (String)
: Simple function to generate a path with named route. If you use regex path & named arguments,link
will check the existence of required args and validate args with regex.
Router Component
Coeus's entry. But if you have understood the principles of internal redux and routes loader, it's really easy to write your own Router component. The Router provided here is considered a quick start for users to build applications with Coeus. A pretty demo with it is the living welcome page in coeus-project.
props
routes(Object)
: The routes module loaded from routes loader.middlewares(Array)
: The middlewares array registered to redux store.
context
Router will create redux store and pass routes module to you. Use it in your components as below:
import { storeShape, routesShape } from 'coeus/lib/types';
// ...
class Foo extends React.Component {
// use store to mountReducer, subscribe or dispatch:
//
// let store = this.context.store;
// store.mountReducer(reducer);
// store.subscribe(listener);
// store.dispatch(action);
// use routes to generate the named routes' path:
//
// let routes = this.context.routes;
// routes.link(name, args);
}
// ...
Foo.contextTypes = {
store: storeShape.isRequired,
routes: routesShape.isRequired
};
export default Foo;
state structure
router
components(Array)
: The components array returned from routes module.args(Object)
: The arguments captured after matching path.search(Object)
: The search object parsed from location.search.notFound(String)(only exists when routing to a 404 path)
: The 404 path which you have route to just now.
actions
{ type: 'ROUTE', path: (String), search: (Object) }
: Route to a path.{ type: 'ROUTE_FORWARD' }
: Forward in history.{ type: 'ROUTE_BACK' }
: Back in history.