Skip to content

Commit

Permalink
Merge pull request #9 from sorrycc/implement-new-api
Browse files Browse the repository at this point in the history
Implement new api
  • Loading branch information
sorrycc authored Jul 13, 2016
2 parents 83da605 + 27cb1fb commit f1ed723
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 55 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ tmp
node_modules
lib
dist
coverage

9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: node_js

node_js:
- "4"
- "5"
- "6"

after_success:
- npm run coveralls
47 changes: 26 additions & 21 deletions examples/user-dashboard/src/entries/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,31 @@ app.model(require('../models/users'));
app.router(require('../routes'));

// 4. Start
const { render } = app.start(document.getElementById('root'));
app.start(document.getElementById('root'), {

// Support Routes HMR.
// This will be implemented in babel plugin later.
if (module.hot) {
const renderNormally = render;
const renderException = (error) => {
const RedBox = require('redbox-react');
ReactDOM.render(<RedBox error={error} />, document.getElementById('root'));
};
const newRender = (routes) => {
try {
renderNormally(routes);
} catch (error) {
console.error('error', error);
renderException(error);
// Support Routes HMR.
// This will be implemented in babel plugin later.
hmr: (render) => {
if (module.hot) {
const renderNormally = render;
const renderException = (error) => {
const RedBox = require('redbox-react');
ReactDOM.render(<RedBox error={error} />, document.getElementById('root'));
};
const newRender = (routes) => {
try {
renderNormally(routes);
} catch (error) {
console.error('error', error);
renderException(error);
}
};
module.hot.accept('../routes', () => {
const routes = require('../routes');
newRender(routes);
});
}
};
module.hot.accept('../routes', () => {
const routes = require('../routes');
newRender(routes);
});
}
},
});


13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@
"author": "chencheng <sorrycc@gmail.com>",
"license": "MIT",
"scripts": {
"test": "babel-node node_modules/.bin/babel-istanbul cover node_modules/.bin/_mocha --no-timeouts",
"debug": "./node_modules/.bin/mocha --require babel-core/register --no-timeouts",
"build": "rm -rf lib && babel src --out-dir lib --ignore __tests__",
"lint": "eslint --ext .js src"
"lint": "eslint --ext .js src",
"coveralls": "cat ./coverage/lcov.info | coveralls"
},
"dependencies": {
"global": "^4.3.0",
"is-plain-object": "^2.0.1",
"isomorphic-fetch": "^2.2.1",
"react-redux": "4.4.x",
"react-router": "^2.5.1",
Expand All @@ -40,6 +45,7 @@
"devDependencies": {
"babel-cli": "^6.10.1",
"babel-eslint": "^6.0.4",
"babel-istanbul": "^0.11.0",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-transform-runtime": "^6.9.0",
"babel-preset-es2015": "^6.9.0",
Expand All @@ -48,9 +54,12 @@
"babel-runtime": "^6.9.2",
"browserify": "^13.0.1",
"browserify-shim": "^3.8.12",
"coveralls": "^2.11.9",
"envify": "^3.4.1",
"eslint": "^2.7.0",
"eslint-config-airbnb": "^9.0.1",
"expect": "^1.20.2",
"mocha": "^2.5.3",
"uglifyjs": "^2.4.10"
},
"babel": {
Expand Down Expand Up @@ -80,4 +89,4 @@
"fetch.js",
"index.js"
]
}
}
138 changes: 107 additions & 31 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,34 @@ import { hashHistory, Router } from 'react-router';
import { syncHistoryWithStore, routerReducer as routing } from 'react-router-redux';
import { handleActions } from 'redux-actions';
import { fork } from 'redux-saga/effects';
import document from 'global/document';
import window from 'global/window';
import { is, check, warn } from './utils';

function dva() {
function dva(opts = {}) {
const onError = opts.onError || function(err) {
throw new Error(err);
};
const onErrorWrapper = (err) => {
if (err) {
if (is.string(err)) err = new Error(err);
onError(err);
}
};

let _routes = null;
const _models = [];
const app = {
model,
router,
start,
store: null,
};
return app;

function model(model) {
check(model.namespace, is.notUndef, 'Namespace must be defined with model');
check(model.namespace, is.notUndef, 'Namespace must be defined with model.');
check(model.namespace, namespace => namespace !== 'routing', 'Namespace should not be routing.');
_models.push(model);
}

Expand All @@ -29,58 +43,125 @@ function dva() {
_routes = routes;
}

// Usage:
// app.start();
// app.start(container);
// app.start(container, opts);
// app.start(opts);
function start(container, opts = {}) {
check(container, is.element, 'Container must be DOMElement');
check(_routes, is.notUndef, 'Routes is not defined');
let sagas = {};
const rootReducer = {};
// If no container supplied, return jsx element.
if (arguments.length === 0
|| (arguments.length === 1 && is.object(container))) {
opts = container || {};
container = null;
} else {
check(container, is.element, 'Container must be DOMElement.');
}
check(_routes, is.notUndef, 'Routes is not defined.');

// Get sagas and reducers from model.
let sagas = {};
let reducers = {
routing,
};
_models.forEach(model => {
rootReducer[model.namespace] = handleActions(model.reducers || {}, model.state);
if (is.array(model.reducers)) {
const [_reducers, enhancer] = model.reducers;
reducers[model.namespace] = enhancer(handleActions(_reducers || {}, model.state));
} else {
reducers[model.namespace] = handleActions(model.reducers || {}, model.state);
}
sagas = { ...sagas, ...model.effects };
});

// Support external reducers.
if (is.notUndef(opts.reducers)) {
check(opts.reducers, is.object, 'Reducers must be object.');
check(opts.reducers, optReducers => {
for (var k in optReducers) {
if (k in reducers) return false;
}
return true;
}, 'Reducers should not be conflict with namespace in model.');
reducers = { ...reducers, ...opts.reducers };
}

// Create store.
if (is.notUndef(opts.middlewares)) {
check(opts.middlewares, is.array, 'Middlewares must be array.')
}
const sagaMiddleware = createSagaMiddleware();
const enhancer = compose(
applyMiddleware(sagaMiddleware),
applyMiddleware.apply(null, [sagaMiddleware, ...(opts.middlewares || [])]),
window.devToolsExtension ? window.devToolsExtension() : f => f
);
const store = createStore(
combineReducers({ ...rootReducer, routing }), {}, enhancer
const initialState = opts.initialState || {};
const store = app.store = createStore(
combineReducers(reducers), initialState, enhancer
);
const history = syncHistoryWithStore(opts.history || hashHistory, store);

// Sync history.
// Use try catch because it don't work in test.
let history;
try {
history = syncHistoryWithStore(opts.history || hashHistory, store);
} catch (e) {}

// Start saga.
sagaMiddleware.run(rootSaga);

document.addEventListener('DOMContentLoaded', () => {
_models.forEach(({ subscriptions }) => {
if (subscriptions) {
check(subscriptions, is.array, 'Subscriptions must be an array');
subscriptions.forEach(sub => {
check(sub, is.func, 'Subscription must be an function');
sub(store.dispatch);
});
}
});
// Handle subscriptions.
_models.forEach(({ subscriptions }) => {
if (subscriptions) {
check(subscriptions, is.array, 'Subscriptions must be an array');
subscriptions.forEach(sub => {
check(sub, is.func, 'Subscription must be an function');
sub(store.dispatch, onErrorWrapper);
});
}
});

// Render and hmr.
if (container) {
render();
if (opts.hmr) {
opts.hmr(render);
}
} else {
const Routes = _routes;
return <Provider store={store}>
<Routes history={history} />
</Provider>;
}

function getWatcher(k, saga) {
let _saga = saga;
let _type = 'takeEvery';
if (Array.isArray(saga)) {
[_saga, opts] = saga;
opts = opts || {};
check(opts.type, is.sagaType, 'Type must be takeEvery or takeLatest');
warn(opts.type, v => v === 'takeLatest', 'takeEvery is the default type, no need to set it by opts');
check(opts.type, is.sagaType, 'Type must be takeEvery, takeLatest or watcher');
warn(opts.type, v => v !== 'takeEvery', 'takeEvery is the default type, no need to set it by opts');
_type = opts.type;
}

if (_type === 'takeEvery') {
function* sagaWithErrorCatch() {
try {
yield _saga();
} catch (e) {
onError(e);
}
}

if (_type === 'watcher') {
return sagaWithErrorCatch;
} else if (_type === 'takeEvery') {
return function*() {
yield takeEvery(k, _saga);
yield takeEvery(k, sagaWithErrorCatch);
};
} else {
return function*() {
yield takeLatest(k, _saga);
yield takeLatest(k, sagaWithErrorCatch);
};
}
}
Expand All @@ -102,11 +183,6 @@ function dva() {
</Provider>
), container);
}

render();
return {
render,
};
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isPlainObject from 'is-plain-object';

export function check(value, predicate, error) {
if(!predicate(value)) {
Expand All @@ -20,8 +21,9 @@ export const is = {
number : n => typeof n === 'number',
element : n => typeof n === 'object' && n.nodeType && n.nodeName,
array : Array.isArray,
object : isPlainObject,
jsx : v => v && v.$$typeof && v.$$typeof.toString() === 'Symbol(react.element)',
sagaType : v => v === 'takeEvery' || v === 'takeLatest',
sagaType : v => v === 'takeEvery' || v === 'takeLatest' || v === 'watcher',
};

/**
Expand Down
Loading

0 comments on commit f1ed723

Please sign in to comment.