Skip to main content

dva

Free2017-10-22#JS#dvajs#dvajs原理#dvajs入门#dvajs教程#dvajs tutorial

Business framework based on industry best practices for react&redux

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:

  1. Configuration

    Implement solidification technically, limit flexibility, make business writing more unified, meet engineering needs

  2. Extend towards general scenarios

    Only open necessary openings, release minimum flexibility set that can meet most business scenario needs

  3. 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)

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment