乾貨react-coat,React+Redux微框架,支持typescript,支持SPA單頁和SSR

項目github庫css

4.0 發佈

  • 繼承並擴展 3.0 的基本理念
  • 去除 redux-saga,改用原生的 async 和 await 來組織和管理 effect
  • 同時支持 SPA(單頁應用)和 SSR(服務器渲染)、完整的支持客戶端與服務端同構

react-coat 特色

  • 集成 react、redux、react-router、history 等相關框架
  • 僅爲以上框架的糖衣外套,不改變其基本概念,無強侵入與破壞性
  • 結構化前端工程、業務模塊化,支持按需加載
  • 同時支持 SPA(單頁應用)和 SSR(服務器渲染)
  • 使用 typescript 嚴格類型,更好的靜態檢查與智能提示
  • 開源微框架,源碼不到千行,幾乎不用學習便可上手

安裝 react-coat

$ npm install react-coat
複製代碼

兼容性

各主流瀏覽器、IE9 或 IE9 以上前端

快速上手及 Demo

本框架上手簡單react

  • 8 個新概念:git

    Effect、ActionHandler、Module、ModuleState、RootState、Model、View、Componentgithub

  • 4 步建立:ajax

    exportModel(), exportView(), exportModule(), createApp()typescript

  • 3 個 Demo:npm

入手:Helloworldredux

進階:SPA(單頁應用)api

升級:SPA(單頁應用)+SSR(服務器渲染)

API 一覽

查看詳細 API 一覽

與 螞蟻金服 Dav 的異同

本框架與 Dvajs 理念略同,主要差別:

  • 使用 typescript 強類型推斷和檢查
  • 去除 redux-saga,使用 async、await 替代,簡化代碼的同時對 TS 類型支持更全面
  • 路由組件化、無 Page 概念、更天然的 API 和更簡單的組織結構
  • 更大的靈活性和自由度,不強封裝
  • 支持 SPA(單頁應用)和 SSR(服務器渲染)一鍵切換,
  • 支持模塊異步按需加載和同步加載一鍵切換

差別示例:使用強類型組織全部 reducer 和 effect

// Dva中常這樣寫
dispatch({ type: 'moduleA/query', payload:{username:"jimmy"}} })

//本框架中可直接利用ts類型反射和檢查:
this.dispatch(moduleA.actions.query({username:"jimmy"}))
複製代碼

差別示例:State 和 Actions 支持繼承

// Dva不支持繼承

// 本框架能夠直接繼承

class ModuleHandlers extends ArticleHandlers<State, PhotoResource> {
  constructor() {
    super({}, {api});
  }
  @effect()
  protected async parseRouter() {
    const result = await super.parseRouter();
    this.dispatch(this.actions.putRouteData({showComment: true}));
    return result;
  }
  @effect()
  protected async [ModuleNames.photos + "/INIT"]() {
    await super.onInit();
  }
}

複製代碼

差別示例:在 Dva 中,由於使用 redux-saga,假設在一個 effect 中使用 yield put 派發一個 action,以此來調用另外一個 effect,雖然 yield 能夠等待 action 的派發,但並不能等待後續 effect 的處理:

// 在Dva中,updateState並不會等待otherModule/query的effect處理完畢了才執行
effects: {
    * query (){
        yield put({type: 'otherModule/query',payload:1});
        yield put({type: 'updateState',  payload: 2});
    }
}

// 在本框架中,可以使用awiat關鍵字, updateState 會等待otherModule/query的effect處理完畢了才執行
class ModuleHandlers {
    async query (){
        await this.dispatch(otherModule.actions.query(1));
        this.dispatch(thisModule.actions.updateState(2));
    }
}
複製代碼

差別示例:若是 ModuleA 進行某項操做成功以後,ModuleB 或 ModuleC 都須要 update 自已的 State,因爲缺乏 action 的觀察者模式,因此只能將 ModuleB 或 ModuleC 的刷新動做寫死在 ModuleA 中:

// 在Dva中須要主動Put調用ModuleB或ModuleC的Action
effects: {
    * update (){
        ...
        if(callbackModuleName==="ModuleB"){
          yield put({type: 'ModuleB/update',payload:1});
        }else if(callbackModuleName==="ModuleC"){
          yield put({type: 'ModuleC/update',payload:1});
        }
    }
}

// 在本框架中,可以使用ActionHandler觀察者模式:
class ModuleB {
    //在ModuleB中兼聽"ModuleA/update"方法
    async ["ModuleA/update"] (){
        ....
    }
}

class ModuleC {
    //在ModuleC中兼聽"ModuleA/update"方法
    async ["ModuleA/update"] (){
        ....
    }
}
複製代碼

基本概念與名詞

前提:假設你已經熟悉了 ReactRedux,有過必定的開發經驗

Store、Reducer、Action、State、Dispatch

以上概念與 Redux 基本一致,本框架無強侵入性,遵循 react 和 redux 的理念和原則:

  • M 和 V 之間使用單向數據流
  • 整站保持單個 Store
  • Store 爲 Immutability 不可變數據
  • 改變 Store 數據,必須經過 Reducer
  • 調用 Reducer 必須經過顯式的 dispatch Action
  • Reducer 必須爲 pure function 純函數
  • 有反作用的行爲,所有放到 Effect 函數中
  • 每一個 reducer 只能修改 Store 下的某個節點,但能夠讀取全部節點
  • 路由組件化,不使用集中式配置

Effect

咱們知道在 Redux 中,改變 State 必須經過 dispatch action 以觸發 reducer,在 reducer 中返回一個新的 state, reducer 是一個 pure function 純函數,無任何反作用,只要入參相同,其返回結果也是相同的,而且是同步執行的。而 effect 是相對於 reducer 而言的,與 reducer 同樣,它也必須經過 dispatch action 來觸發,不一樣的是:

  • 它是一個非純函數,能夠包含反作用,能夠無返回,也能夠是異步的。
  • 它不能直接改變 State,要改變 State,它必須再次 dispatch action 來觸發 reducer

ActionHandler

咱們能夠簡單的認爲:在 Redux 中 store.dispatch(action),能夠觸發一個註冊過的 reducer,看起來彷佛是一種觀察者模式。推廣到以上的 effect 概念,effect 一樣是一個觀察者。一個 action 被 dispatch,可能觸發多個觀察者被執行,它們多是 reducer,也多是 effect。因此 reducer 和 effect 統稱爲:ActionHandler

  • 若是有一組 actionHandler 在兼聽某一個 action,那它們的執行順序是什麼呢?

    答:當一個 action 被 dispatch 時,最早執行的是全部的 reducer,它們被依次同步執行。全部的 reducer 執行完畢以後,纔開始全部 effect 執行。

  • 我想等待這一組 actionHandler 所有執行完畢以後,再下一步操做,但是 effect 是異步執行的,我如何知道全部的 effect 都被處理完畢了? 答:本框架改良了 store.dispatch()方法,若是有 effect 兼聽此 action,它會返回一個 Promise,因此你可使用 await store.dispatch({type:"search"}); 來等待全部的 effect 處理完成。

Module

當咱們接到一個複雜的前端項目時,首先要化繁爲簡,進行功能拆解。一般以高內聚、低偶合的原則對其進行模塊劃分,一個 Module 是相對獨立的業務功能的集合,它一般包含一個 Model(用來處理業務邏輯)和一組 View(用來展現數據與交互),須要注意的是:

  • SPA 應用已經沒有了 Page 的邊界,不要以 Page 的概念來劃分模塊
  • 一個 Module 可能包含一組 View,不要以 View 的概念來劃分模塊

Module 雖然是邏輯上的劃分,但咱們習慣於用文件夾目錄來組織與體現,例如:

src
├── modules
│       ├── user
│       │     ├── userOverview(Module)
│       │     ├── userTransaction(Module)
│       │     └── blacklist(Module)
│       ├── agent
│       │     ├── agentOverview(Module)
│       │     ├── agentBonus(Module)
│       │     └── agentSale(Module)
│       └── app(Module)
複製代碼

經過以上能夠看出,此工程包含 7 大模塊 app、userOverview、userTransaction、blacklist、agentOverview、agentBonus、agentSale,雖然 modules 目錄下面還有子目錄 user、angent,但它們僅屬於歸類,不屬於模塊。咱們約定:

  • 每一個 Module 是一個獨立的文件夾
  • Module 自己只有一級,可是能夠放在多級的目錄中進行歸類
  • 每一個 Module 文件夾名即爲該 Module 名,由於全部 Module 都是平級的,因此須要保證 Module 名不重複,實踐中,咱們能夠經過 Typescript 的 enum 類型來保證,你也能夠將全部 Module 都放在一級目錄中。
  • 每一個 Module 保持必定的獨立性,它們能夠被同步、異步、按需、動態加載

ModuleState、RootState

系統被劃分爲多個相對獨立且平級的 Module,不只體如今文件夾目錄,更體如今 Store 上。每一個 Module 負責維護和管理 Store 下的一個節點,咱們稱之爲 ModuleState,而整個 Store 咱們習慣稱之爲RootState

例如:某個 Store 數據結構:

{
router:{...},// StoreReducer
app:{...}, // ModuleState
userOverview:{...}, // ModuleState
userTransaction:{...}, // ModuleState
blacklist:{...}, // ModuleState
agentOverview:{...}, // ModuleState
agentBonus:{...}, // ModuleState
agentSale:{...} // ModuleState
}
複製代碼
  • 每一個 Module 管理並維護 Store 下的某一個節點,咱們稱之爲 ModuleState
  • 每一個 ModuleState 都是 Store 的根子節點,並以 Module 名爲 Key
  • 每一個 Module 只能修改自已的 ModuleState,可是能夠讀取其它 ModuleState
  • 每一個 Module 修改自已的 ModuleState,必須經過 dispatch action 來觸發
  • 每一個 Module 能夠觀察者身份,監聽其它 Module 發出的 action,來配合修改自已的 ModuleState

你可能注意到上面 Store 的子節點中,第一個名爲 router,它並非一個 ModuleState,而是一個由第三方 Reducer 生成的節點。咱們知道 Redux 中容許使用多個 Reducer 來共同維護 Stroe,並提供 combineReducers 方法來合併。因爲 ModuleState 的 key 名即爲 Module 名,因此:Module名天然也不能與其它第三方Reducer生成節點重名

Model

在 Module 內部,咱們可進一步劃分爲一個model(維護數據)一組view(展示交互),此處的 Model 實際上指的是 view model,它主要包含兩大功能:

  • ModuleState 的定義
  • ModuleState 的維護,前面有介紹過 ActionHandler,實際上就是對 ActionHandler 的編寫

數據流是從 Model 單向流入 View,因此 Model 是獨立的,是不依賴於 View 的。因此理論上即便沒有 View,整個程序依然是能夠經過命令行來驅動的。

咱們約定:

  • 集中在一個名爲model.js的文件中編寫 Model,並將此文件放在本模塊根目錄下
  • 集中在一個名爲ModuleHandlers的 class 中編寫 全部的 ActionHandler,每一個 reducer、effect 都對應該 class 中的一個方法

例如,userOverview 模塊中的 Model:

src
├── modules
│       ├── user
│       │     ├── userOverview(Module)
│       │     │         ├──views
│       │     │         └──model.ts
│       │     │
複製代碼

src/modules/user/userOverview/model.ts

// 定義本模塊的ModuleState類型
export interface State extends BaseModuleState {
  listSearch: {username:string; page:number; pageSize:number};
  listItems: {uid:string; username:string; age:number}[];
  listSummary: {page:number; pageSize:number; total:number};
  loading: {
    searchLoading: LoadingState;
  };
}

// 定義本模塊全部的ActionHandler
class ModuleHandlers extends BaseModuleHandlers<State, RootState, ModuleNames> {
  constructor() {
    // 定義本模塊ModuleState的初始值
    const initState: State = {
      listSearch: {username:null, page:1, pageSize:20},
      listItems: null,
      listSummary: null,
      loading: {
        searchLoading: LoadingState.Stop,
      },
    };
    super(initState);
  }

  // 一個reducer,用來update本模塊的ModuleState
  @reducer
  public putSearchList({listItems, listSummary}): State {
    return {...this.state, listItems, listSummary};
  }


  // 一個effect,使用ajax查詢數據,而後dispatch action來觸發以上putSearchList
  // this.dispatch是store.dispatch的引用
  // searchLoading指明將這個effect的執行狀態注入到State.loading.searchLoading中
  @effect("searchLoading")
  public async searchList(options: {username?:string; page?:number; pageSize?:number} = {}) {
    // this.state指向本模塊的ModuleState
    const listSearch = {...this.state.listSearch, ...options};
    const {listItems, listSummary} = await api.searchList(listSearch);
    this.dispatch(this.action.putSearchList({listItems, listSummary}));
  }

  // 一個effect,監聽其它Module發出的Action,而後改變自已的ModuleState
  // 由於是監聽其它Module發出的Action,因此它不須要主動觸發,使用非public權限對外隱藏
  // @effect(null)表示不須要跟蹤此effect的執行狀態
  @effect(null)
  protected async ["@@router/LOCATION_CHANGE]() {
      // this.rootState指向整個Store
      if(this.rootState.router.location.pathname === "/list"){
          // 使用await 來等待全部的actionHandler處理完成以後再返回
          await this.dispatch(this.action.searchList());
      }
  }
}
複製代碼

須要特別說明的是以上代碼的最後一個 ActionHandler:

protected async ["@@router/LOCATION_CHANGE](){
    // this.rootState指向整個Store
    if(this.rootState.router.location.pathname === "/list"){
        await this.dispatch(this.action.searchList());
    }
}
複製代碼

前面有強調過兩點:

  • Module 能夠兼聽其它 Module 發出的 Action,並配合來完成自已 ModuleState 的更新。
  • Module 只能更新自已的 ModuleState 節點,可是能夠讀取整個 Store。

另外注意到語句:await this.dispatch(this.action.searchList()):

  • dispatch 派發一個名爲 searchList 的 action 能夠理解,但是爲何前面還能 awiat?難道 dispatch action 也是異步的?

    答:dispatch 派發 action 自己是同步的,咱們前面講過 ActionHandler 的概念,一個 action 被 dispatch 時,可能有一組 reducer 或 effect 在兼聽它,reducer 是同步處理的,但是 effect 多是異步處理的,若是你想等全部的兼聽都執行完成以後,再作下一步操做,此處就可使用 await,不然,你能夠不使用 await。

View、Component

在 Module 內部,咱們可進一步劃分爲一個model(維護數據)一組view(展示交互)。因此一個 Module 中的 view 可能有多個,咱們習慣在 Module 根目錄下建立一個名爲 views 的文件夾:

例如,userOverview 模塊中的 views:

src
├── modules
│       ├── user
│       │     ├── userOverview(Module)
│       │     │         ├──views
│       │     │         │     ├──imgs
│       │     │         │     ├──List
│       │     │         │     │     ├──index.css
│       │     │         │     │     └──index.ts
│       │     │         │     ├──Main
│       │     │         │     │    ├──index.css
│       │     │         │     │    └──index.ts
│       │     │         │     └──index.ts
│       │     │         │
│       │     │         │
│       │     │         └──model.ts
│       │     │
複製代碼
  • 每一個 view 實際上是一個 React Component 類,因此使用大寫字母打頭
  • 對於 css 和 img 等附屬資源,若是是屬於某個 view 私有的,跟隨 view 放到一塊兒,若是是多個 view 公有的,提出來放到公共目錄中。
  • view 能夠嵌套,包括能夠給別的 Module 中的 view 嵌套,若是須要給別的 Module 使用,必須在 views/index.ts 中使用exportView()導出。
  • 在 view 中經過 dispatch action 的方式觸發 Model 中的 ActionHandler,除了能夠 dispatch 本模塊的 action,也能 dispatch 其它模塊的 action

例如,某個 LoginForm:

interface Props extends DispatchProp {
  logining: boolean;
}

class Component extends React.PureComponent<Props> {
  public onLogin = (evt: any) => {
    evt.stopPropagation();
    evt.preventDefault();
    // 發出本模塊的action,將觸發本model中定義的名爲login的ActionHandler
    this.props.dispatch(thisModule.actions.login({username: "", password: ""}));
  };

  public render() {
    const {logining} = this.props;
    return (
      <form className="app-Login" onSubmit={this.onLogin}>
        <h3>請登陸</h3>
        <ul>
          <li><input name="username" placeholder="Username" /></li>
          <li><input name="password" type="password" placeholder="Password" /></li>
          <li><input type="submit" value="Login" disabled={logining} /></li>
        </ul>
      </form>
    );
  }
}

const mapStateToProps = (state: RootState) => {
  return {
    logining: state.app.loading.login !== LoadingState.Stop,
  };
};

export default connect(mapStateToProps)(Component);
複製代碼

從以上代碼可看出,View 就是一個 Component,那 View 和 Component 有區別嗎?編碼上沒有,邏輯上是有的:

  • view 體現的是 ModuleState 的視圖展示,更偏重於表現特定的具體的業務邏輯,因此它的 props 通常是直接用 mapStateToProps connect 到 store。
  • component 體現的是一個沒有業務邏輯上下文的純組件,它的 props 通常來源於父級傳遞。
  • component 一般是公共的,而 view 一般非公用

路由與動態加載

react-coat 贊同 react-router 4 組件化路由的理念,路由即組件,嵌套路由比如嵌套 component 同樣簡單,無需繁瑣的配置。如:

import {BottomNav} from "modules/navs/views"; // BottomNav 來自於 navs 模塊
import LoginForm from "./LoginForm"; // LoginForm 來自於本模塊

// PhotosView 和 VideosView 分別來自於 photos 模塊和 videos 模塊,使用異步按需加載
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
const VideosView = loadView(moduleGetter, ModuleNames.videos, "Main");

<div className="g-page">
    <Switch>
        <Route exact={false} path="/photos" component={PhotosView} />
        <Route exact={false} path="/videos" component={VideosView} />
        <Route exact={true} path="/login" component={LoginForm} />
    </Switch>
    <BottomNav />
</div>
複製代碼

以上某個 view 中以不一樣加載方式嵌套了多個其它 view:

  • BottomNav 是一個名爲 navs 模塊下的 view,直接嵌套意味着它會同步加載到本 view 中
  • LoginForm 是本模塊下的一個 view,因此直接用相對路徑引用,一樣直接嵌套,意味着它會同步加載
  • PhotosView 和 VideosView 來自於別的模塊,可是是經過 loadView()獲取和 Route 嵌套,意味着它們會異步按需加載,固然你也能夠直接 import {PhotosView} from "modules/photos/views"來同步按需加載

因此本框架對於模塊和視圖的加載靈活簡單,無需複雜配置與修改:

  • 不論是同步、異步、按:需、動態加載,要改變的僅僅是加載方式,而不用修改被加載的模塊。模塊自己並不須要事先擬定自已將被誰、以何種方式加載,保證的模塊的獨立性。
  • 前面講過,view 是 model 數據的展示,那嵌入其它模塊 view 時,是否還要導入其它模塊的 model 呢?無需,框架將自動導入。
相關文章
相關標籤/搜索