Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new api #9

Merged
merged 9 commits into from
Jul 13, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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