React — 端的編程範式

dvajs 是 Alibaba 針對於 react/redux 技術棧基於 elm 概念編寫的一套腳手架。javascript

兩年前由於 antd 開始接觸了這套腳手架。個人確很須要這套腳手架,對於新手來講,整合 react / redux / react-redux / react-router / react-router-redux 的確仍是蠻費勁的 —— 若是像我這麼偷懶,可能都沒辦法瞭解它們是什麼。css

固然,不少高階的工具至少通過了前人深思熟慮後才造出來的,當我在 macOS / iOS 開發碰到困難的時候,我就再一次想起了他們:html

  1. react 最開始解決了單頁面組件化的問題,組件與組件之間的狀態管理卻沒有解決。
  2. redux 解決了單頁面狀態管理的問題,提供了通用的方案。
  3. react / redux 天然而然的結合在了一塊兒,或者說他們一開始就是眉來眼去的。

One Page Application

一切的一切,都開始於前端的一個特殊的概念 OPA (One Page Application),即單頁面應用,白話就是在一個頁面裏面實現一個完整的應用,好處顯而易見:不再用等待白屏加載你的頁面了,業務和業務之間的切換也流暢天然,這在現代前端領域裏面已經達成了深入的共識。可是前端原先存在諸多工程問題沒有解決:龐大的組織裏面如何分工協做?代碼如何管理?組件如何複用?固然這些問題在客戶端開發看來徹底不是問題,一個 Activity / ViewController 便可解決全部問題,不行就再來一個,再不行咱們就開始嵌套着來 —— 咱們又沒有白屏問題。前端

React Component 的出現至關於爲前端提供了一個 View 級別的 namespace,它的粒度就是一個視覺組件,包含了這個視覺樣式,同時也提供了事件響應模型等等。至此,前端開發和全部 Native 客戶端開發(包含 Desktop 的廣義客戶端開發)站在了同一個起跑線上 —— 終於能夠爲一個組件作一個命名了,依賴 Virtual DOM 或者 Web Component 的形式。DOM 沒有問題了,樣式的獨立能夠採用命名或者 scoped css 的方式解決,這個是小問題。java

以上,是前端領域解決的第一個大問題,若是視圖組件能夠抽象成一個類,那麼組件就能夠共享,頁面的開發從簡單的 html 標籤改成業務複用 View 組件,整個開發流程從平行開始變得立體。react

React

State

View 必定是存在狀態的,什麼叫狀態?若是咱們不給一個 View 傳入一個外部的值,隨着事件的產生,View 本身的某些屬性也會發生改變,這些屬性咱們稱之爲狀態。這些事件是由於交互產生,某個組件集合內部,由於某個交互(好比進入這個頁面)去訪問了網絡,網絡下載來的數據填充了這個 View,那麼這些數據就容易是狀態的一部分。git

狀態不是一個數據,它是一組數據。這是一個很重要的概念。一組數據意味着兩次狀態之間的某些數據是不能互相組合的。好比 { a: 1, b: 2 } 是合法的一組, { a: 3, b: 4 } 是合法的一組。那麼 { a: 1, b : 4 } 若是不是咱們業務中存在的組合的話,在代碼的任意時刻,咱們的 View 狀態都不該該存在這種可能。基於以上法則,咱們引入了 immutable 這個概念。github

Immutable

Immutable 和不少範式結合在一塊兒使用,最經典好比函數式編程(Functional Programming),咱們知道 Pure Function 是不存在併發問題的,由於輸入和輸出對於外部環境不會產生任何反作用。那麼產生反作用的可能有兩種:一是訪問了外部資源,二是對已經存在的對象產生了修改。編程

若是咱們必定要對一個已經存在的 Immutable Object 進行修改怎麼辦呢?很是簡單,咱們使用 CopyOnWrite 的策略返回出去就行。這樣依然保證了輸入和輸出是恆定的,同時對外部環境不會產生影響,函數的「純潔性」獲得了保證。Immutable 相關的庫有不少,js 有 Immutable.js,java 有 Google AutoValue,guava 裏面也有相關的實現。redux

Pure Function 的概念爲咱們代碼的可測試性和可維護性提供了很好的方向,若是可能的話,咱們但願咱們全部的函數都是 Pure Function,就像 TDD 同樣,是咱們亙古不變追求的目標。

那麼在 React 中,setState 這個 API,就是咱們說的 Immutable 的一個展示。在 Immutable 設計模式中,若是你把歷史狀態本身保存一份的話,這個歷史狀態即可以隨時回溯 —— 咱們的編輯器裏面就有這麼一個狀態機,咱們作 Undo 和 Redo 的時候,這個狀態機很是重要。

Immutable

Redux

在使用狀態的過程當中,咱們碰到了一個問題,咱們的組件要和別的組件進行一些聯動,通常來講,咱們採起的方案是和別的組件進行必定程度的引用 —— 經過 callback,那麼 macOS / iOS 裏面比較常見的就是 delegate。React 一開始也能夠經過 callback 的方式把數據反哺出去。可是若是有多個組件須要這個數據的話,咱們可能甚至要作到把 callback 一層一層傳遞進去,像這樣(僞代碼):

<A callback=this.cb>
    <B callback=a.callback >
        <C callback=b.callback>
        </C>
    </B>
</A>
複製代碼

可能明明是個業務性的全局數據,硬是要用這種方式去傳。有 ViewController 和 Activity 其實這個問題還不算特別地明顯,由於不一樣場景下可使用不一樣的 ViewController,子流程的數據和父流程的數據可使用構造函數的方式進行隔離。在前端 OPA 中不一樣的業務流程若是須要使用同一個狀態就很麻煩了。

這時候咱們有了 Redux,它的官網宣傳 4 個特性:

  1. 可預測:行爲一致性
  2. 中心化:狀態持久化
  3. 可調試:「時空旅行式」調試
  4. 擴展性:插件生態

Redux

以上 1 和 3 特性咱們能夠很簡單的用 Immutable 來涵蓋。2 的話是 Redux 提供了一個全局 Store 來搞定這件事(這也太簡單了吧),Store 這個狀態管理很是有用,由於咱們徹底能夠在服務端渲染這個頁面的時候,就初始化這個 Store,使用全局變量的方式直接給瀏覽器的 Response 中賦值。這樣在咱們進行 Server Rendering 的時候,不用經過狀態遷移,就能夠得到最終狀態,再一次提高了前端渲染的效率。

React-Redux

Redux 概念提出後,就天然而然地出現了 React-Redux 項目。它的做用只有一個,把 Redux 的 Store 天然而然地融入到 React 的生態中去。提供的 API 很是簡單且有用,經過connect()這個 API,生成高階組件(High-Order-Component)的方式,爲每一個 React 組件注入 Store,connect() 原型以下:

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?) 複製代碼

react-redux.js.org/api/connect

這個函數其實看名字特別簡單,4個參數都是可選,它的返回值是一個高階函數,高階函數的形參就是你本身的 ReactComponent,封裝這個 ReactComponent 產生一個高階 Component 給業務方使用,咱們簡單聊一聊 connect 函數。

咱們知道,Redux 有個 Store,這個 Store 裏面存的是全局的 State,那麼如何使用這個全局的 State?業務組件能不能只關心這個全局 State 中本身的部分?這個事情就是mapStateToProps這個形參作的事了,它是一個函數,原型是這樣(state) -> props

咱們知道每個 ReactComponent 都是有一些屬性(Props)的,這些屬性和狀態不一樣,它是不可變的(Immutable),既然有 Pure Function 的概念,咱們也能夠有 Pure Component 的概念,對應 Flutter 裏面的 Stateless Component 這個概念 —— 只有 Props,沒有 State,這些組件越多越好。 那麼當全局的 State 發生改變的時候,咱們就須要一個函數用來把這個全局的 State 映射成當前組件的 Props,這個工做就是mapStateToProps來完成,每一個組件只關心 State 中和本身有關的部分就行了。

經過上面的方式,咱們就完成了一個組件對全局狀態改變從而影響全局 View 的途徑。

那麼如何產出這個動做呢?React-Redux 爲咱們引入了一個函數叫dispatch,dispatch 調用的內容就是 action 和它的形參。這個理解其實很簡單,咱們能夠簡單的理解成 dispatch 調用了 fun.bind(xxx),那麼這個動做對全局的 State 會有影響,會生成一個新的 State,狀態機會往下走一步。使用 React-Redux 的應用程序常常看見的代碼就是:

dispatch({type: 「INCREMENT」, value: 1});
複製代碼

實質上是調用一個和 INCREMENT 相關聯的純函數,這個函數接受形參和 previous state,返回一個新的 state:

function action(state, params) {
	// ....
	return { ...state, xxxx }
}
複製代碼

而後返回的 State 會被 Store 存起來,同時全部被 connect 的 Component 會收到一個通知用來更改本身的 Properties。

這,就是在沒有網絡環境下 React-Redux 的邏輯閉環,以上邏輯閉環咱們一般會這麼描述:

component -> action -> reducer -> state 的單向數據流轉問題。

Side Effects

一旦接入了 API 調用,咱們的邏輯一會兒就複雜起來了,由於 RPC 的調用基本不可預測,你哪怕是調用冪等的接口,你也有可能由於網絡的不通暢致使咱們的狀態機進入的 State 開始變得不惟一了

State Machine

沒錯,萬惡的 API,它不 Pure 了。注意,這還僅僅是冪等接口的狀況下,若是是不冪等的接口,那狀態可能更多。 破壞 Pure Function 最大的第一個問題就是函數的可測試性被破壞了(Testable),這時候你想寫測試用例的話,assert() 根本不知道怎麼去寫,由於你也不知道它的返回值是什麼。

首先爲了解決異步調用的問題(action 須要異步獲取數據),有不少 library 選擇:

  • redux-thunk
  • redux-promise
  • redux-saga

關於 dva 的爲何選擇 redux-saga,能夠看看支付寶這邊的理由: github.com/sorrycc/blo…

redux-thunk 和 redux-promise 改變了 action 的含義,action 變得不那麼純粹(Pure)

他們都爲 action 帶來了反作用。那麼看看 redux-saga 是怎麼解決這個問題的。

redux-saga

redux-saga.js.org/

上面是 redux-saga 的首頁。

saga 最核心的解決方式是使用 Generator 爲咱們的不肯定性增長了一分肯定,咱們在須要調用 API 的接口中,咱們能夠經過 Generator 拿到經過了分支邏輯調用出去的一個狀態 —— 無論這個異步調用的返回值是什麼,咱們能拿到發出這個異步調用的一個動做,Saga 把這件事稱爲:聲明式反作用(Declarative Effects)

redux-saga.js.org/docs/basics…

咱們能夠看下如何能拿到剛剛說的的東西,首先它拋出一個測試上的問題。

function* fetchProducts() {
  const products = yield Api.fetch('/products')
  console.log(products)
}

const iterator = fetchProducts()
assert.deepEqual(iterator.next().value, ??) // what do we expect ?
複製代碼

這是咱們剛剛提的問題,咱們指望的值是什麼?咱們若是想肯定這個值,有兩種方式:

  1. 鏈接真正的服務器
  2. mock 數據

那麼在測試中,使用 1 的方式進行測試是很是愚蠢的(你怎麼測試「註冊」這個接口?由於不冪等)。 那麼只能使用 mock,mock這件事實際上是下策,mock 使咱們的測試變得困難且不可靠,若是咱們業務改了,mock 的代碼還要改,這樣工做量就提高了不少,很是吃力。

那麼 saga 參考了Eric Elliott 的文章,原話是:

(...)equal(), by nature answers the two most important questions every unit test must answer, but most don’t:

What is the actual output? What is the expected output? If you finish a test without answering those two questions, you don’t have a real unit test. You have a sloppy, half-baked test.

翻譯過來,就是咱們須要考慮清楚到底什麼是真正的輸出和指望的輸出。咱們能夠不根據業務的實際結果,咱們去測試 API 接口的時候,只指望能輸入正確的,符合咱們和後端文檔定義的參數就行。由於業務返回結果不是前端能決定的,這個決定方是 API 提供方,他們要經過他們的測試保證在網絡正常的狀況下,符合接口文檔的定義。 注意,咱們前端關注的是,事件響應對於 API 調用的行爲,由於 redux-saga 是基於 Generator 的,這個行爲變得很好獲取,咱們的 assert 就變成了:

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)
複製代碼

咱們指望它發生了一次對於/producs這個 API 接口的調用。 這樣,咱們就不須要 mock 任何接口就搞定了這件事,結果可能不一致,可是行爲必定是一致的,經過行爲的一致,咱們保證了 action 的純粹性:

輸入一致的參數,輸出了同樣的結果(行爲)。

你們這裏能夠細細品一下,我再把剛剛「聲明式 Effects」的連接貼一下:

redux-saga.js.org/docs/basics…

dva

那麼以上介紹了 React, Redux, React-Redux 和 React-Saga。 dva 事實上是對以上幾個組件的封裝,固然我這邊再也不講 react-router 這種前端路由的東西,我相信你們都還好理解。

引入 saga 和 router 解決了純函數的問題,也誕生了新的問題:

  1. Redux 項目模塊太分散
  2. 建立 saga 很是麻煩,這個看文檔你們就清楚

這部分在支付寶前端應用架構的發展和選擇裏面有講到,dva 把這些邏輯進行了封裝,使用聲明式路由和 model 的方案解決了以上的兩個問題,讓咱們更爽地使用以上一整套方案。

理解完 redux-saga 以後,使用 dva 能讓工程效率提高很多。

客戶端開發者的困境

客戶端,或者說 native client 開發者由於沒有 function first-class 這種語言級別的待遇(可能)和冗長的流程,使得咱們對於數據流的思考遠遠不如前端同窗的多,從 Android LiveData 和 Flutter 這樣的組件開發能夠看出來,歷來都是大廠主導,你們學習這麼個進程來的,再怎麼說,前端領域仍是出現了像 Vue.js 這種「民間」組織出來的框架,雖然有 Google Angular 和 Facebook React,可是民間力量不容小覷。

Android 的 LiveData / LifeCycle 其實不少參考了 React 的編程模型,那麼 Flutter 就更不用說了,API 的設計以及文檔都已經說了是 React 模型下的產物。看來 React 的組件化和狀態的概念已經深刻人(大廠)心,加上 React 有 Redux,Flutter 有 fish-redux 也解決了狀態管理的問題。

愁的就是 Native 端了,LiveData / LifeCycle 遠沒有把狀態管理作好,RecyclerView 配合 Paging Library 使用的時候,加載更多這個動做居然沒辦法通知到全局。iOS / macOS 的 SwiftUI 遙遙無期(算了。。不吐槽了,你懂的), native 任重而道遠。

React

總結

以上這麼多碎碎念和知識普及但願能拋磚引玉,由於我這幾天做爲一個 macOS 開發新手,實在是受不了超多層的 delegate,所以忽然很懷念兩年前寫 dva 那種行雲流水的感受。但願 SwiftUI 能儘快成熟,但同時也但願 Apple 領域能從 MVC 這種很(老)穩(掉)固(牙)的設計模式中儘量的有創新,帶給更多開發者耳目一新的感受,否則你憑啥阻止 Flutter 在 AppStore 發佈應用呢?

歡迎關注個人公衆號「TalkWithMobile」

公衆號
相關文章
相關標籤/搜索