0.9.6 • Published 8 years ago

coeus v0.9.6

Weekly downloads
3
License
MIT
Repository
github
Last release
8 years ago

Coeus

Build Status npm version npm downloads

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 need
      • Object.assign
      • Promise
    • reducers: Reducers for coeus components
      • router: The reducer for Router
    • redux: Internal redux
      • createStore
      • combineReducers
      • combineSubscribers
    • utils: utils for webpack
      • routes-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

  1. Promise supported in reducers and subscribers. Feel free to return simple values and objects of Promise in reducers and subscribers.
  2. Completely support dynamic reducer. replaceReducer is deleted from the exposed API of store and mountReducer(a new API) is the only way to add new reducers to store.
  3. 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.
  4. combineSubscribers and combineReducers does not have to be called explicitly. mountReducer and subscribe will call them internally. What's more, not only a flatten object but also a tree object can be passed to combineSubscribers and combineReducers because they recursively call themselves to handle tree objects.
  5. createStore only accepts an optional parameter middlewares, no reducer, no initial state, no enhancer.
  6. INIT action is separated from createStore and has become a standalone API initState of store.
  7. 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 call dispatch.

API

  • [createStore(middlewares): store] (Object): The factory to create a store object. middlewares is an array of middleware. The returned store object exposes 5 API getState, mountReducer, subscribe, dispatch and initState.

  • [middleware(action, next): action] (Object / Promise): Middleware for dispatching pipeline. Support Promise. next is a closure has the same interface of dispatch(only has an action parameter and return action 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 with mountReducer 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 to combineSubscribers to return a single listener function finally held in listeners list. Additionally, thanks to the initial states for keys is undefined in combineSubscribers, 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): Dispatch INIT 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 before initState because React doesn't support Promise in componentWillMount and can not guarantee render to run before async initState finished. Initializing an empty but valid state before render 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:

  1. All : prefixes in components will be replaced to the closest componentsPath in itself, parent or ancestors;
  2. 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. All path 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 without path 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.

  • The reducer is here.
  • The component is here.

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

8 years ago

0.9.5

8 years ago

0.9.4

8 years ago

0.9.3

8 years ago

0.9.2

8 years ago

0.9.1

8 years ago

0.9.0

8 years ago