<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          react-coat支持 SPA 單頁和 SSR 的 React 微框架

          聯(lián)合創(chuàng)作 · 2023-09-24 16:41

          react 生態(tài)圈的開放、自由、繁榮,也導(dǎo)致開發(fā)配置繁瑣、選擇迷茫。react-coat 放棄某些靈活性、以約定替代某些配置,固化某些最佳實踐方案,從而提供給開發(fā)者一個更簡潔的糖衣外套。

          你還在老老實實按照原生redux教程維護(hù)store么?試試簡單到幾乎不用學(xué)習(xí)就能上手的react-coat吧:

          4.0 發(fā)布

          • 去除 redux-saga,改用原生的 async 和 await 來組織和管理 effect

          • 同時支持 SPA(單頁應(yīng)用)和 SSR(服務(wù)器渲染)、完整的支持客戶端與服務(wù)端同構(gòu)

          react-coat 特點

          • 集成 react、redux、react-router、history 等相關(guān)框架

          • 僅為以上框架的糖衣外套,不改變其基本概念,無強侵入與破壞性

          • 結(jié)構(gòu)化前端工程、業(yè)務(wù)模塊化,支持按需加載

          • 同時支持 SPA(單頁應(yīng)用)和 SSR(服務(wù)器渲染)

          • 使用 typescript 嚴(yán)格類型,更好的靜態(tài)檢查與智能提示

          • 開源微框架,源碼不到千行,幾乎不用學(xué)習(xí)即可上手

          安裝 react-coat

          $ npm install react-coat

          依賴周邊生態(tài)庫:

          "peerDependencies": {
              "@types/node": "^9.0.0 || ^10.0.0",
              "@types/history": "^4.0.0",
              "@types/react": "^16.0.0",
              "@types/react-dom": "^16.0.0",
              "@types/react-redux": "^5.0.0 || ^6.0.0",
              "@types/react-router-dom": "^4.0.0",
              "connected-react-router": "^4.0.0 || ^5.0.0",
              "history": "^4.0.0",
              "react": "^16.0.0",
              "react-dom": "^16.0.0",
              "react-redux": "^5.0.0",
              "react-router-dom": "^4.0.0",
              "redux": "^3.0.0 || ^4.0.0"
            },

          如果你想省心,并且對以上依賴版本沒有特別要求,你可以安裝"all in 1"的 react-coat-pkg,它將自動包含以上庫,并測試通過各版本不沖突:

          $ npm install react-coat-pkg

          兼容性

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

          本框架依賴于完整版"Promise",低版本瀏覽器請自行安裝 polyfill,推薦安裝@babel/polyfill,該庫可模擬 unhandledrejection error,當(dāng)你需要在客戶端捕捉錯誤并上報時需要。

          快速上手及 Demo

          本框架上手簡單

          API 一覽

          查看詳細(xì) API 一覽

          BaseModuleHandlers, BaseModuleState, buildApp, delayPromise, effect, ERROR, errorAction, exportModel, exportModule, exportView, GetModule, INIT, LoadingState, loadModel, loadView, LOCATION_CHANGE, logger, ModelStore, Module, ModuleGetter, reducer, renderApp, RootState, RouterParser, setLoading, setLoadingDepthTime

          與 螞蟻金服 Dav 的異同

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

          • 引入 ActionHandler 觀察者模式,更優(yōu)雅的處理模塊之間的協(xié)作

          • 去除 redux-saga,使用 async、await 替代,簡化代碼的同時對 TS 類型支持更全面

          • 原生使用 typescript 組織和開發(fā),更全面的類型安全

          • 路由組件化、無 Page 概念、更自然的 API 和更簡單的組織結(jié)構(gòu)

          • 更大的靈活性和自由度,不強封裝腳手架等

          • 支持 SPA(單頁應(yīng)用)和 SSR(服務(wù)器渲染)一鍵切換,

          • 支持模塊異步按需加載和同步加載一鍵切換

          差異示例:使用強類型組織所有 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,假設(shè)在一個 effect 中使用 yield put 派發(fā)一個 action,以此來調(diào)用另一個 effect,雖然 yield 可以等待 action 的派發(fā),但并不能等待后續(xù) effect 的處理:

          // 在Dva中,updateState并不會等待otherModule/query的effect處理完畢了才執(zhí)行
          effects: {
              * query (){
                  yield put({type: 'otherModule/query',payload:1});
                  yield put({type: 'updateState',  payload: 2});
              }
          }
          
          // 在本框架中,可使用awiat關(guān)鍵字, updateState 會等待otherModule/query的effect處理完畢了才執(zhí)行
          class ModuleHandlers {
              async query (){
                  await this.dispatch(otherModule.actions.query(1));
                  this.dispatch(thisModule.actions.updateState(2));
              }
          }

          差異示例:如果 ModuleA 進(jìn)行某項操作成功之后,ModuleB 或 ModuleC 都需要 update 自已的 State,由于缺少 action 的觀察者模式,所以只能將 ModuleB 或 ModuleC 的刷新動作寫死在 ModuleA 中:

          // 在Dva中需要主動Put調(diào)用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" action
              async ["ModuleA/update"] (){
                  ....
              }
          }
          
          class ModuleC {
              //在ModuleC中兼聽"ModuleA/update" action
              async ["ModuleA/update"] (){
                  ....
              }
          }

          基本概念與名詞

          前提:假設(shè)你已經(jīng)熟悉了 React 和 Redux,有過一定的開發(fā)經(jīng)驗

          Store、Reducer、Action、State、Dispatch

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

          • M 和 V 之間使用單向數(shù)據(jù)流

          • 整站保持單個 Store

          • Store 為 Immutability 不可變數(shù)據(jù)

          • 改變 Store 數(shù)據(jù),必須通過 Reducer

          • 調(diào)用 Reducer 必須通過顯式的 dispatch Action

          • Reducer 必須為 pure function 純函數(shù)

          • 有副作用的行為,全部放到 Effect 函數(shù)中

          • 每個 reducer 只能修改 Store 下的某個節(jié)點,但可以讀取所有節(jié)點

          • 路由組件化,不使用集中式配置

          Effect

          我們知道在 Redux 中,改變 State 必須通過 dispatch action 以觸發(fā) reducer,在 reducer 中返回一個新的 state, reducer 是一個 pure function 純函數(shù),無任何副作用,只要入?yún)⑾嗤?,其返回結(jié)果也是相同的,并且是同步執(zhí)行的。而 effect 是相對于 reducer 而言的,與 reducer 一樣,它也必須通過 dispatch action 來觸發(fā),不同的是:

          • 它是一個非純函數(shù),可以包含副作用,可以無返回,也可以是異步的。

          • 它不能直接改變 State,要改變 State,它必須再次 dispatch action 來觸發(fā) reducer

          ActionHandler

          我們可以簡單的認(rèn)為:在 Redux 中 store.dispatch(action),可以觸發(fā)一個注冊過的 reducer,看起來似乎是一種觀察者模式。推廣到以上的 effect 概念,effect 同樣是一個觀察者。一個 action 被 dispatch,可能觸發(fā)多個觀察者被執(zhí)行,它們可能是 reducer,也可能是 effect。所以 reducer 和 effect 統(tǒng)稱為:ActionHandler

          • 如果有一組 actionHandler 在兼聽某一個 action,那它們的執(zhí)行順序是什么呢?

            答:當(dāng)一個 action 被 dispatch 時,最先執(zhí)行的是所有的 reducer,它們被依次同步執(zhí)行。所有的 reducer 執(zhí)行完畢之后,才開始所有 effect 執(zhí)行。

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

          Module

          當(dāng)我們接到一個復(fù)雜的前端項目時,首先要化繁為簡,進(jìn)行功能拆解。通常以高內(nèi)聚、低偶合的原則對其進(jìn)行模塊劃分,一個 Module 是相對獨立的業(yè)務(wù)功能的集合,它通常包含一個 Model(用來處理業(yè)務(wù)邏輯)和一組 View(用來展示數(shù)據(jù)與交互),需要注意的是:

          • SPA 應(yīng)用已經(jīng)沒有了 Page 的邊界,不要以 Page 的概念來劃分模塊

          • 一個 Module 可能包含一組 View,不要以 View 的概念來劃分模塊

          Module 雖然是邏輯上的劃分,但我們習(xí)慣于用文件夾目錄來組織與體現(xiàn),例如:

          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 本身只有一級,但是可以放在多級的目錄中進(jìn)行歸類

          • 每個 Module 文件夾名即為該 Module 名,因為所有 Module 都是平級的,所以需要保證 Module 名不重復(fù),實踐中,我們可以通過 Typescript 的 enum 類型來保證,你也可以將所有 Module 都放在一級目錄中。

          • 每個 Module 保持一定的獨立性,它們可以被同步、異步、按需、動態(tài)加載

          ModuleState、RootState

          系統(tǒng)被劃分為多個相對獨立且平級的 Module,不僅體現(xiàn)在文件夾目錄,更體現(xiàn)在 Store 上。每個 Module 負(fù)責(zé)維護(hù)和管理 Store 下的一個節(jié)點,我們稱之為 ModuleState,而整個 Store 我們習(xí)慣稱之為RootState

          例如:某個 Store 數(shù)據(jù)結(jié)構(gòu):

          {
          router:{...},// StoreReducer
          app:{...}, // ModuleState
          userOverview:{...}, // ModuleState
          userTransaction:{...}, // ModuleState
          blacklist:{...}, // ModuleState
          agentOverview:{...}, // ModuleState
          agentBonus:{...}, // ModuleState
          agentSale:{...} // ModuleState
          }
          • 每個 Module 管理并維護(hù) Store 下的某一個節(jié)點,我們稱之為 ModuleState

          • 每個 ModuleState 都是 Store 的根子節(jié)點,并以 Module 名為 Key

          • 每個 Module 只能修改自已的 ModuleState,但是可以讀取其它 ModuleState

          • 每個 Module 修改自已的 ModuleState,必須通過 dispatch action 來觸發(fā)

          • 每個 Module 可以觀察者身份,監(jiān)聽其它 Module 發(fā)出的 action,來配合修改自已的 ModuleState

          你可能注意到上面 Store 的子節(jié)點中,第一個名為 router,它并不是一個 ModuleState,而是一個由第三方 Reducer 生成的節(jié)點。我們知道 Redux 中允許使用多個 Reducer 來共同維護(hù) Stroe,并提供 combineReducers 方法來合并。由于 ModuleState 的 key 名即為 Module 名,所以:Module名自然也不能與其它第三方Reducer生成節(jié)點重名。

          Model

          在 Module 內(nèi)部,我們可進(jìn)一步劃分為一個model(維護(hù)數(shù)據(jù))一組view(展現(xiàn)交互),此處的 Model 實際上指的是 view model,它主要包含兩大功能:

          • ModuleState 的定義

          • ModuleState 的維護(hù),前面有介紹過 ActionHandler,實際上就是對 ActionHandler 的編寫

          數(shù)據(jù)流是從 Model 單向流入 View,所以 Model 是獨立的,是不依賴于 View 的。所以理論上即使沒有 View,整個程序依然是可以通過命令行來驅(qū)動的。

          我們約定:

          • 集中在一個名為model.js的文件中編寫 Model,并將此文件放在本模塊根目錄下

          • 集中在一個名為ModuleHandlers的 class 中編寫 所有的 ActionHandler,每個 reducer、effect 都對應(yīng)該 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查詢數(shù)據(jù),然后dispatch action來觸發(fā)以上putSearchList
            // this.dispatch是store.dispatch的引用
            // searchLoading指明將這個effect的執(zhí)行狀態(tài)注入到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,監(jiān)聽其它Module發(fā)出的Action,然后改變自已的ModuleState
            // 因為是監(jiān)聽其它Module發(fā)出的Action,所以它不需要主動觸發(fā),使用非public權(quán)限對外隱藏
            // @effect(null)表示不需要跟蹤此effect的執(zhí)行狀態(tài)
            @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());
              }
          }

          前面有強調(diào)過兩點:

          • Module 可以兼聽其它 Module 發(fā)出的 Action,并配合來完成自已 ModuleState 的更新。

          • Module 只能更新自已的 ModuleState 節(jié)點,但是可以讀取整個 Store。

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

          • dispatch 派發(fā)一個名為 searchList 的 action 可以理解,可是為什么前面還能 awiat?難道 dispatch action 也是異步的?

            答:dispatch 派發(fā) action 本身是同步的,我們前面講過 ActionHandler 的概念,一個 action 被 dispatch 時,可能有一組 reducer 或 effect 在兼聽它,reducer 是同步處理的,可是 effect 可能是異步處理的,如果你想等所有的兼聽都執(zhí)行完成之后,再做下一步操作,此處就可以使用 await,否則,你可以不使用 await。

          View、Component

          在 Module 內(nèi)部,我們可進(jìn)一步劃分為一個model(維護(hù)數(shù)據(jù))一組view(展現(xiàn)交互)。所以一個 Module 中的 view 可能有多個,我們習(xí)慣在 Module 根目錄下創(chuàng)建一個名為 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()導(dǎo)出。

          • 在 view 中通過 dispatch action 的方式觸發(fā) 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();
              // 發(fā)出本模塊的action,將觸發(fā)本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 有區(qū)別嗎?編碼上沒有,邏輯上是有的:

          • view 體現(xiàn)的是 ModuleState 的視圖展現(xiàn),更偏重于表現(xiàn)特定的具體的業(yè)務(wù)邏輯,所以它的 props 一般是直接用 mapStateToProps connect 到 store。

          • component 體現(xiàn)的是一個沒有業(yè)務(wù)邏輯上下文的純組件,它的 props 一般來源于父級傳遞。

          • component 通常是公共的,而 view 通常非公用

          路由與動態(tài)加載

          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 嵌套,意味著它們會異步按需加載,當(dāng)然你也可以直接 import {PhotosView} from "modules/photos/views"來同步按需加載

          所以本框架對于模塊和視圖的加載靈活簡單,無需復(fù)雜配置與修改:

          • 不管是同步、異步、按:需、動態(tài)加載,要改變的僅僅是加載方式,而不用修改被加載的模塊。模塊本身并不需要事先擬定自已將被誰、以何種方式加載,保證的模塊的獨立性。

          • 前面講過,view 是 model 數(shù)據(jù)的展現(xiàn),那嵌入其它模塊 view 時,是否還要導(dǎo)入其它模塊的 model 呢?無需,框架將自動導(dǎo)入。

          幾個特殊的 Action

          • @@router/LOCATION_CHANGE:本框架集成了 connected-react-router,路由發(fā)生變化時將觸發(fā)此 action,你可以在 moduleHandlers 中監(jiān)聽此 action

          • "@@framework/ERROR:本框架 catch 了未處理的 error,發(fā)生 error 時將自動派發(fā)此 action,你可以在 moduleHandlers 中監(jiān)聽此 action

          • module/INIT:模塊初次載入時會觸發(fā)此 action,來向 store 注入初始 moduleState

          • module/LOADING:觸發(fā)加載進(jìn)度時會觸發(fā)此 action,比如 @effect(login)

          瀏覽 10
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          編輯 分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          編輯 分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲乱伦黄色小说视频网址 | 国产欧美日韩视频在线观看 | 国产大鸡巴免费视频 | 亚洲青娱乐精品91 | 日本一区二区三视频 |