I. Target Positioning
What problems does it want to solve? How does it plan to do it?
In short: dva wants to provide a business framework based on industry best practices for react&redux, to solve various problems brought by using bare redux family bucket as frontend data layer
- High editing cost, need to switch back and forth between reducer, saga, action
- Not convenient for organizing business models (or called domain model). For example, after we write a userlist, to write a productlist, need to copy many files.
- Saga writing is too complex, every time listening to an action needs to go through fork -> watcher -> worker process
- Redux entry writing is troublesome, need to complete store creation, middleware configuration, route initialization, Provider's store binding, saga initialization
For example:
+ src
+ sagas
- user.js
+ reducers
- user.js
+ actions
- user.js
+ service
- user.js
II. Core Implementation
How was it done?
Dependency Relationships
dva
react
react-dom
dva-core
redux
redux-saga
history
react-redux
react-router-redux
Implementation Ideas
Its core is providing app.model method, used to encapsulate reducer, initialState, action, saga together
const model = {
// Used as top-level state key, and action prefix
namespace
// Module-level initial state
state
// Subscribe to other data sources, such as router change, window resize, key down/up...
subscriptions
// Sagas in redux-saga
effects
// Reducer in redux
reducers
};
The main work actually done by dva-core is after getting reducers, worker sagas, states from model configuration, shield the following series of tedious work:
-
Connect redux (combine state, combine reducer)
-
Connect redux-saga (complete redux-saga's fork -> watcher -> worker, and do error capture well)
Besides the two most important parts in core, dva also does some things:
-
Built-in react-router-redux, history responsible for route management
-
Attach react-redux's connect, isomorphic-fetch and other commonly used things
-
Subscriptions as icing on the cake, provide a place for code that listens to off-field factors
-
Connect with react (use store to connect react and redux, rely on redux middleware mechanism to bring redux-saga in to play together)
At this point it's almost encapsulated well, so, below open some openings add a bit of flexibility:
-
Hand over a bunch of hooks (effect/reducer/action/state level hook), make internal state readable
-
Provide global error handling method, solve the pain point of uncontrollable async errors
-
Enhance model management (allow dynamically adding/removing models)
Guess the entire implementation process is like this:
-
Configuration
Implement solidification technically, limit flexibility, make business writing more unified, meet engineering needs
-
Extend towards general scenarios
Only open necessary openings, release minimum flexibility set that can meet most business scenario needs
-
Enhance towards specific needs
Respond to business calls, consider whether to release/provide more flexibility, weigh between flexibility and engineering (controllability)
III. Design Philosophy
What ideology to follow, what does it want?
Learned from elm and choo, including elm's subscription and choo's design philosophy
elm's subscription
Subscribe to some messages to get data from other data sources, such as websocket connection of server, keyboard input, geolocation change, history router change, etc.
For example:
subscriptions: {
setupHistory ({ dispatch, history }) {
history.listen((location) => {
dispatch({
type: 'updateState',
payload: {
locationPathname: location.pathname,
locationQuery: queryString.parse(location.search),
},
})
})
},
setup ({ dispatch }) {
dispatch({ type: 'query' })
let tid
window.onresize = () => {
clearTimeout(tid)
tid = setTimeout(() => {
dispatch({ type: 'changeNavbar' })
}, 300)
}
}
}
Provide this mechanism to access other data sources, and concentrate into model for unified management
choo's Design Philosophy
choo's philosophy is to be as minimal as possible, minimize selection/switching cost:
We believe frameworks should be disposable, and components recyclable. We don't want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. choo is modest in its design; we don't believe it will be top of the class forever, so we've made it as easy to toss out as it is to pick up.
We don't believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.
Roughly says frameworks shouldn't develop into fortresses, should be usable or not usable at any time (low cost switching), APIs and design should remain minimal, don't throw a lump of "knowledge" at users, this way you're good and he (colleague) is good too
P.S. Of course, this passage is right wherever taken, as for whether dva or even choo itself has achieved it is hard to say (didn't see any effective measures to remove fortresses from choo's implementation)
In API design, dva-core is almost kept minimal:
-
One model has only 4 configuration items
-
APIs are countable
-
Hooks are almost all necessary (onHmr and extraReducers are later enhancements towards specific needs)
But having said that, dva-core actually only integrates redux and redux-saga through model configuration, and enhances some control (error handling, etc.), the only foreign concept introduced is subscription, still hanging on model, even if designing APIs hard, can't get too complex
IV. Pros and Cons
What are the disadvantages, what benefits does it bring?
Advantages:
-
Framework restrictions benefit engineering, brick-like code is best
-
Simplify tedious boilerplate code, ritual-like action/reducer/saga/api...
-
Solve the problem of scattered focus caused by multiple files, logic separation is good, but file isolation is a bit uncomfortable
Disadvantages:
-
Limits flexibility (such as combineReducers problem)
-
Performance burden (getSaga part implementation, doesn't look fast, does quite a bit of extra work to achieve control purposes)
V. Implementation Techniques
External Parameter Checking
invariant is the most common basic pattern in source code:
function start(container) {
// Allow container to be string, then use querySelector to find element
if (isString(container)) {
container = document.querySelector(container);
invariant(
container,
`[app.start] container ${container} not found`,
);
}
// And is HTMLElement
invariant(
!container || isHTMLElement(container),
`[app.start] container should be HTMLElement`,
);
// Route must be registered in advance
invariant(
app._router,
`[app.start] router must be registered before app.start()`,
);
oldAppStart.call(app);
//...
}
invariant is used to guarantee strong conditions (if conditions not met directly throw, production environment also throws), warning is used to guarantee weak conditions (development environment log error and [non-interfering throw](/articles/redux 源码解读/#articleHeader6), production environment doesn't throw, replace with empty function)
invariant's indiscriminate throw can be used, but warning is not recommended, because release code containing warning is not as clean as compile replacement (will still execute empty function)
Another technique is to wrap a layer of function, do parameter checking outside, such as in the example:
function start(container) {
//...parameter checking
oldAppStart.call(app);
}
The benefit of doing this is taking parameter checking out, readability will be better, but has performance overhead of one more layer of function call, and not as controllable as if-else (can only block subsequent flow through throw)
Aspect Hook
First look at this part of source code:
// Wrap every effect once, to achieve effect-level control
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
function applyOnEffect(fns, effect, model, key) {
for (const fn of fns) {
effect = fn(effect, sagaEffects, model, key);
}
return effect;
}
Then usage is like this (passed in onEffect Hook):
function onEffect(effect, { put }, model, actionType) {
const { namespace } = model;
return function*(...args) {
yield put({ type: SHOW, payload: { namespace, actionType } });
yield effect(...args);
yield put({ type: HIDE, payload: { namespace, actionType } });
};
}
(Extracted from dva-loading
Isn't this around enhancement (Around Advice in AOP)?
Enhancement around a join point, such as method invocation. This is the most powerful type of enhancement. Around enhancement can complete custom behavior before and after method invocation. It's also responsible for choosing whether to continue executing the join point, or directly return their own return values or throw exceptions to end execution
(Extracted from AOP(Aspect-Oriented Programming))
The actual effect here is onEffect wraps saga one layer, hands over saga's execution right, allows external (through onEfect hook) to inject logic. Handing oneself over to hook is not some amazing technique, but usage is very interesting, utilizing iterator's expandable characteristic, achieved decorator effect (hand over a saga, get back an enhanced saga, type unchanged doesn't affect flow)
No comments yet. Be the first to share your thoughts.