dva

一.目標定位
想解決什麼問題?打算怎麼作?前端

簡言之:dva想提供一個基於業界react&redux最佳實踐的業務框架,以解決用裸redux全家桶做爲前端數據層帶來的種種問題react

編輯成本高,須要在reducer, saga, action之間來回切換web

不便於組織業務模型(或者叫domain model)。好比咱們寫了一個userlist以後,要寫一個productlist,須要複製不少文件。redux

saga書寫太複雜,每監聽一個action都須要走fork -> watcher -> worker的流程api

redux entry書寫麻煩,要完成store建立,中間件配置,路由初始化,Provider的store的綁定,saga的初始化websocket

例如:react-router

+ src
  + sagas
    - user.js
  + reducers
    - user.js
  + actions
    - user.js
  + service
    - user.js

二.核心實現
怎麼作了?app

依賴關係框架

dva
  react
  react-dom
  dva-core
    redux
    redux-saga
  history
  react-redux
  react-router-redux

實現思路
他最核心的是提供了app.model方法,用於把reducer, initialState, action, saga封裝到一塊兒less

const model = {
    // 用做頂層state key,以及action前綴
    namespace
    // module級初始state
    state
    // 訂閱其它數據源,如router change,window resize, key down/up...
    subscriptions
    // redux-saga裏的sagas
    effects
    // redux裏的reducer
    reducers
};

dva-core實際所做的主要工做是從model配置獲得reducers,worker sagas, states後,屏蔽接下來的一系列繁瑣工做:

接redux(組合state,組合reducer)

接redux-saga(完成redux-saga的fork -> watcher -> worker,並作好錯誤捕獲)

除了core裏最重要的兩部分外,dva還作了一些事情:

內置react-router-redux, history負責路由管理

粘上react-redux的connect,isomorphic-fetch等經常使用的東西

subscriptions錦上添花,給監聽場外因素的代碼提供一個容身之處

和react鏈接起來(用store鏈接react和redux,靠redux中間件機制把redux-saga拉進來一塊兒玩)

到這裏差很少封裝好了,那麼,下面開一些口子增長一點靈活性:

遞出一堆鉤子(effect/reducer/action/state級hook),讓內部狀態可讀

提供全局錯誤處理方式,解決異步錯誤不可控的痛點

加強model管理(容許動態增刪model)

猜想整個實現過程是這樣:

配置化

在技術上實現固化,把靈活性限制起來,讓業務寫法更統一,知足工程化的須要

面向通用場景擴展

只開必要的口子,放出能知足大多數業務場景須要的最小靈活性集合

面向特定須要加強

應對業務呼聲,考慮是否放出/提供更多一些的靈活性,在靈活性與工程化(可控程度)之間權衡取捨

三.設計理念
聽從什麼思想,想要怎麼樣?

借鑑自elm和choo,包括elm的subscription和choo的設計理念

elm的subscription
經過訂閱一些消息來從其它數據源取數據,好比websocket connection of server, keyboard input, geolocation change, history router change等等

例如:

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)
    }
  }
}

提供這種機制來接入其它數據源,並集中到model裏統一管理

choo的設計理念
choo的理念是儘可能精簡,儘可能下降選擇/切換成本:

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.

大意是說框架不該該發展成堡壘,應該隨時可用可不用(低成本切換),API及設計應該保持最小化,不要丟給用戶一坨「知識」,這樣你好他(同事)也好

P.S.固然,這段話拿到哪裏都是對的,至於dva甚至choo自身有沒有作到就很差說了(從choo的實現上沒看出來有什麼拆除堡壘的有效措施)

在API設計上,dva-core差很少保持最小化了:

一份model僅4個配置項

API屈指可數

hook差很少都是必須的(onHmr與extraReducers是後來面向特定須要的加強)

不過話說回來,dva-core實際作的只把redux和redux-saga經過model配置整合起來,並加強一些控制(錯誤處理等),引入的惟一外來概念是subscription,還掛在model上,即使用力設計API,也複雜不到哪去

四.優缺點
有什麼缺點,帶來的收益是什麼?

優勢:

框架限制有利於工程化,磚塊同樣的代碼最好了

簡化繁瑣的樣板代碼(boilerplate code),儀式同樣的action/reducer/saga/api…

解決多文件致使關注點分散的問題,邏輯分離是好事,但文件隔離就有點難受了

缺點:

限制了靈活性(好比combineReducers問題)

性能負擔(getSaga部分的實現,看着就不快,作了很多額外的事情來達到控制的目的)

五.實現技巧
外置參數檢查
invariant是源碼出現最多的基本套路:

function start(container) {
  // 容許 container 是字符串,而後用 querySelector 找元素
  if (isString(container)) {
    container = document.querySelector(container);
    invariant(
      container,
      `[app.start] container ${container} not found`,
    );
  }

  // 而且是 HTMLElement
  invariant(
    !container || isHTMLElement(container),
    `[app.start] container should be HTMLElement`,
  );

  // 路由必須提早註冊
  invariant(
    app._router,
    `[app.start] router must be registered before app.start()`,
  );

  oldAppStart.call(app);
  //...
}

invariant用來保證強條件(不知足條件直接throw,生產環境也throw),warning用來保證弱條件(開發環境log error並沒有干擾throw,生產環境不throw,換成空函數)

invariant無差異throw能夠用,但warning不建議使用,由於含warning的release代碼不如編譯替換乾淨(還會執行空函數)

另外一個技巧是包一層函數,在外面作參數檢查,好比示例中的:

function start(container) {
  //...參數檢查
  oldAppStart.call(app);
}

這樣作的好處是把參數檢查拿出去了,可讀性會更好一些,但有多一層函數調用的性能開銷,並且不如if-else控制度高(只能經過throw阻斷後續流程)

切面Hook
先看這部分源碼:

// 把每個effect都包一遍,爲了實現effect級的控制
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;
}

而後用法是這樣的(傳入的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 } });
  };
}

(摘自dva-loading

這不就是環繞加強(AOP裏的Around Advice)嗎?

圍繞一個鏈接點的加強,如方法調用。這是最強大的一種加強類型。環繞加強能夠在方法調用先後完成自定義的行爲。它也負責選擇是繼續執行鏈接點,仍是直接返回它們本身的返回值或者拋出異常來結束執行

(摘自AOP(Aspect-Oriented Programming))

這裏的實際做用是onEffect把saga包一層,把saga的執行權交出去,容許外部(經過onEfect hook)注入邏輯。把本身交給hook,不是什麼了不得的技巧,但用法上頗有意思,利用iterator可展開的特性,實現了裝飾者的效果(交出去一個saga,拿回來一個加強過的saga,類型沒變不影響流程)

相關文章
相關標籤/搜索