Skip to content

Commit eaf25db

Browse files
committed
refactor: support mobile
1 parent 7e1a296 commit eaf25db

File tree

7 files changed

+318
-280
lines changed

7 files changed

+318
-280
lines changed

src/createDva.js

+266
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import { Provider } from 'react-redux';
4+
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
5+
import createSagaMiddleware, { takeEvery, takeLatest } from 'redux-saga';
6+
import { handleActions } from 'redux-actions';
7+
import { fork } from 'redux-saga/effects';
8+
import isPlainObject from 'is-plain-object';
9+
import assert from 'assert';
10+
import Plugin from './plugin';
11+
12+
export default function createDva(createOpts) {
13+
const {
14+
mobile,
15+
initialReducer,
16+
defaultHistory,
17+
routerMiddleware,
18+
setupHistory,
19+
} = createOpts;
20+
21+
return function dva(hooks = {}) {
22+
const plugin = new Plugin();
23+
plugin.use(hooks);
24+
25+
const app = {
26+
// properties
27+
_models: [],
28+
_router: null,
29+
_store: null,
30+
_history: null,
31+
_plugin: plugin,
32+
// methods
33+
use: plugin.use.bind(plugin),
34+
model,
35+
router,
36+
start,
37+
};
38+
return app;
39+
40+
////////////////////////////////////
41+
// Methods
42+
43+
function model(m) {
44+
checkModel(m, mobile);
45+
this._models.push(m);
46+
}
47+
48+
// inject model dynamically
49+
function injectModel(createReducer, onError, m) {
50+
checkModel(m, mobile);
51+
const store = this._store;
52+
53+
// reducers
54+
store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state);
55+
store.replaceReducer(createReducer(store.asyncReducers));
56+
// effects
57+
if (m.effects) {
58+
store.runSaga(getSaga(m.effects));
59+
}
60+
// subscriptions
61+
if (m.subscriptions) {
62+
runSubscriptions(m.subscriptions, this, onError);
63+
}
64+
}
65+
66+
function router(router) {
67+
assert.equal(typeof router, 'function', 'app.router: router should be function');
68+
this._router = router;
69+
}
70+
71+
function start(container, opts = {}) {
72+
// support: app.start(opts);
73+
if (isPlainObject(container)) {
74+
opts = container;
75+
container = null;
76+
}
77+
78+
// support selector
79+
if (typeof container === 'string') {
80+
container = document.querySelector(container);
81+
assert.ok(container, 'app.start: could not query selector: ' + container);
82+
}
83+
84+
assert.ok(!container || isHTMLElement(container), 'app.start: container should be HTMLElement');
85+
assert.ok(this._router, 'app.start: router should be defined');
86+
87+
// set history
88+
const history = opts.history || defaultHistory;
89+
90+
// error wrapper
91+
const onError = plugin.apply('onError', function(err) {
92+
throw new Error(err.stack || err);
93+
});
94+
const onErrorWrapper = (err) => {
95+
if (err) {
96+
if (typeof err === 'string') err = new Error(err);
97+
onError(err);
98+
}
99+
};
100+
101+
// get reducers and sagas from model
102+
let sagas = [];
103+
let reducers = { ...initialReducer };
104+
for (const m of this._models) {
105+
reducers[m.namespace] = getReducer(m.reducers, m.state);
106+
if (m.effects) sagas.push(getSaga(m.effects, onErrorWrapper));
107+
}
108+
109+
// extra reducers
110+
const extraReducers = plugin.get('extraReducers');
111+
assert.ok(Object.keys(extraReducers).every(key => !(key in reducers)), 'app.start: extraReducers is conflict with other reducers');
112+
113+
// create store
114+
const extraMiddlewares = plugin.get('onAction');
115+
const reducerEnhancer = plugin.get('onReducer');
116+
const sagaMiddleware = createSagaMiddleware();
117+
let middlewares = [
118+
sagaMiddleware,
119+
...extraMiddlewares,
120+
];
121+
if (routerMiddleware) {
122+
middlewares = [routerMiddleware(history), ...middlewares];
123+
}
124+
const devtools = window.devToolsExtension || (() => noop => noop);
125+
const enhancers = [
126+
applyMiddleware(...middlewares),
127+
devtools(),
128+
];
129+
const store = this._store = createStore(
130+
createReducer(),
131+
opts.initialState || {},
132+
compose(...enhancers)
133+
);
134+
135+
function createReducer(asyncReducers) {
136+
return reducerEnhancer(combineReducers({
137+
...reducers,
138+
...extraReducers,
139+
...asyncReducers,
140+
}));
141+
}
142+
143+
// extend store
144+
store.runSaga = sagaMiddleware.run;
145+
store.asyncReducers = {};
146+
147+
// store change
148+
const listeners = plugin.get('onStateChange');
149+
for (const listener of listeners) {
150+
store.subscribe(listener);
151+
}
152+
153+
// start saga
154+
sagas.forEach(sagaMiddleware.run);
155+
156+
// setup history
157+
if (setupHistory) setupHistory.bind(this, history);
158+
159+
// run subscriptions
160+
const subs = this._models.reduce((ret, { subscriptions }) => {
161+
return [ ...ret, ...(subscriptions || [])];
162+
}, []);
163+
runSubscriptions(subs, this, onError);
164+
165+
// inject model after start
166+
this.model = injectModel.bind(this, createReducer, onError);
167+
168+
// If has container, render; else, return react component
169+
if (container) {
170+
render(container, store, router, this);
171+
plugin.apply('onHmr')(render);
172+
} else {
173+
return getProvider(store, router, this);
174+
}
175+
}
176+
177+
////////////////////////////////////
178+
// Helpers
179+
180+
function getProvider(store, router, app) {
181+
return () => (
182+
<Provider store={store}>
183+
<router app={app} history={app._history} />
184+
</Provider>
185+
);
186+
}
187+
188+
function render(container, store, router, app) {
189+
ReactDOM.render(React.createElement(getProvider(store, router, app)), container);
190+
}
191+
192+
function checkModel(model, mobile) {
193+
assert.ok(model.namespace, 'app.model: namespace should be defined');
194+
assert.ok(mobile || model.namespace !== 'routing', 'app.model: namespace should not be routing, it\'s used by react-redux-router');
195+
assert.ok(!model.subscriptions || Array.isArray(model.subscriptions), 'app.model: subscriptions should be Array');
196+
assert.ok(!model.reducers || typeof model.reducers === 'object' || Array.isArray(model.reducers), 'app.model: reducers should be Object or array');
197+
assert.ok(!Array.isArray(model.reducers) || (typeof model.reducers[0] === 'object' && typeof model.reducers[1] === 'function'), 'app.model: reducers with array should be app.model({ reducers: [object, function] })')
198+
assert.ok(!model.effects || typeof model.effects === 'object', 'app.model: effects should be Object');
199+
}
200+
201+
function isHTMLElement(node) {
202+
return typeof node === 'object' && node !== null && node.nodeType && node.nodeName;
203+
}
204+
205+
function getReducer(reducers, state) {
206+
if (Array.isArray(reducers)) {
207+
return reducers[1](handleActions(reducers[0], state));
208+
} else {
209+
return handleActions(reducers || {}, state);
210+
}
211+
}
212+
213+
function getSaga(effects, onError) {
214+
return function *() {
215+
for (const key in effects) {
216+
const watcher = getWatcher(key, effects[key], onError);
217+
yield fork(watcher);
218+
}
219+
}
220+
}
221+
222+
function getWatcher(key, _effect, onError) {
223+
let effect = _effect;
224+
let type = 'takeEvery';
225+
if (Array.isArray(_effect)) {
226+
effect = _effect[0];
227+
const opts = _effect[1];
228+
if (opts && opts.type) {
229+
type = opts.type;
230+
}
231+
assert.ok(['watcher', 'takeEvery', 'takeLatest'].indexOf(type) > -1, 'app.start: effect type should be takeEvery, takeLatest or watcher')
232+
}
233+
234+
function *sagaWithCatch(...args) {
235+
try {
236+
yield effect(...args);
237+
} catch(e) {
238+
onError(e);
239+
}
240+
}
241+
242+
switch (type) {
243+
case 'watcher':
244+
return sagaWithCatch;
245+
case 'takeEvery':
246+
return function*() {
247+
yield takeEvery(key, sagaWithCatch);
248+
};
249+
case 'takeLatest':
250+
return function*() {
251+
yield takeLatest(key, sagaWithCatch);
252+
};
253+
default:
254+
throw new Error(`app.start: unsupport effect type ${type}`);
255+
}
256+
}
257+
258+
function runSubscriptions(subs, app, onError) {
259+
for (const sub of subs) {
260+
assert.ok(typeof sub === 'function', 'app.start: subscription should be function');
261+
sub({ dispatch: app._store.dispatch, history:app._history }, onError);
262+
}
263+
}
264+
265+
};
266+
}

0 commit comments

Comments
 (0)