react-coat 支持 SPA 单页和 SSR 的 React 微框架开源项目

我要开发同款
匿名用户2019年01月23日
192阅读

技术信息

开源地址
https://github.com/wooline/react-coat
授权协议
MIT

作品详情

react生态圈的开放、自由、繁荣,也导致开发配置繁琐、选择迷茫。react-coat放弃某些灵活性、以约定替代某些配置,固化某些最佳实践方案,从而提供给开发者一个更简洁的糖衣外套。

你还在老老实实按照原生redux教程维护store么?试试简单到几乎不用学习就能上手的react-coat吧:

4.0发布

去除redux-saga,改用原生的asyc和await来组织和管理effect

同时支持SPA(单页应用)和SSR(服务器渲染)、完整的支持客户端与服务端同构

react-coat特点

集成react、redux、react-router、history等相关框架

仅为以上框架的糖衣外套,不改变其基本概念,无强侵入与破坏性

结构化前端工程、业务模块化,支持按需加载

同时支持SPA(单页应用)和SSR(服务器渲染)

使用typescript严格类型,更好的静态检查与智能提示

开源微框架,源码不到千行,几乎不用学习即可上手

安装react-coat$ pm istall react-coat

依赖周边生态库:

"peerDepedecies": {    "@types/ode": "^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",    "coected-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"  },

如果你想省心,并且对以上依赖版本没有特别要求,你可以安装"alli1"的 react-coat-pkg,它将自动包含以上库,并测试通过各版本不冲突:

$ pm istall react-coat-pkg兼容性

各主流浏览器、IE9或IE9以上

本框架依赖于完整版"Promise",低版本浏览器请自行安装polyfill,推荐安装@babel/polyfill,该库可模拟uhadledrejectioerror,当你需要在客户端捕捉错误并上报时需要。

快速上手及Demo

本框架上手简单

8个新概念:

Effect、ActioHadler、Module、ModuleState、RootState、Model、View、Compoet

4步创建:

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

3个Demo,循序渐进:

入手:Helloworld

进阶:SPA(单页应用)

升级:SPA(单页应用)+SSR(服务器渲染)

API一览

查看详细API一览

BaseModuleHadlers, BaseModuleState, buildApp, delayPromise, effect, ERROR, errorActio, exportModel, exportModule, exportView, GetModule, INIT, LoadigState, loadModel, loadView, LOCATION_CHANGE, logger, ModelStore, Module, ModuleGetter, reducer, rederApp, RootState, RouterParser, setLoadig, setLoadigDepthTime与蚂蚁金服Dav的异同

本框架与 Dvajs 理念略同,主要差异:

引入ActioHadler观察者模式,更优雅的处理模块之间的协作

去除redux-saga,使用asyc、await替代,简化代码的同时对TS类型支持更全面

原生使用typescript组织和开发,更全面的类型安全

路由组件化、无Page概念、更自然的API和更简单的组织结构

更大的灵活性和自由度,不强封装脚手架等

支持SPA(单页应用)和SSR(服务器渲染)一键切换,

支持模块异步按需加载和同步加载一键切换

差异示例:使用强类型组织所有reducer和effect

// Dva中常这样写dispatch({ type: 'moduleA/query', payload:{userame:"jimmy"}} })//本框架中可直接利用ts类型反射和检查:this.dispatch(moduleA.actios.query({userame:"jimmy"}))

差异示例:State和Actios支持继承

// Dva不支持继承// 本框架可以直接继承class ModuleHadlers exteds ArticleHadlers<State, PhotoResource> {  costructor() {    super({}, {api});  }  @effect()  protected asyc parseRouter() {    cost result = await super.parseRouter();    this.dispatch(this.actios.putRouteData({showCommet: true}));    retur result;  }  @effect()  protected asyc [ModuleNames.photos + "/INIT"]() {    await super.oIit();  }}

差异示例:在Dva中,因为使用redux-saga,假设在一个effect中使用yieldput派发一个actio,以此来调用另一个effect,虽然yield可以等待actio的派发,但并不能等待后续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 ModuleHadlers {    asyc query (){        await this.dispatch(otherModule.actios.query(1));        this.dispatch(thisModule.actios.updateState(2));    }}

差异示例:如果ModuleA进行某项操作成功之后,ModuleB或ModuleC都需要update自已的State,由于缺少actio的观察者模式,所以只能将ModuleB或ModuleC的刷新动作写死在ModuleA中:

// 在Dva中需要主动Put调用ModuleB或ModuleC的Actioeffects: {    * update (){        ...        if(callbackModuleName==="ModuleB"){          yield put({type: 'ModuleB/update',payload:1});        }else if(callbackModuleName==="ModuleC"){          yield put({type: 'ModuleC/update',payload:1});        }    }}// 在本框架中,可使用ActioHadler观察者模式:class ModuleB {    //在ModuleB中兼听"ModuleA/update" actio    asyc ["ModuleA/update"] (){        ....    }}class ModuleC {    //在ModuleC中兼听"ModuleA/update" actio    asyc ["ModuleA/update"] (){        ....    }}基本概念与名词

前提:假设你已经熟悉了 React 和 Redux,有过一定的开发经验

Store、Reducer、Actio、State、Dispatch

以上概念与Redux基本一致,本框架无强侵入性,遵循react和redux的理念和原则:

M和V之间使用单向数据流

整站保持单个Store

Store为Immutability不可变数据

改变Store数据,必须通过Reducer

调用Reducer必须通过显式的dispatchActio

Reducer必须为purefuctio纯函数

有副作用的行为,全部放到Effect函数中

每个reducer只能修改Store下的某个节点,但可以读取所有节点

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

Effect

我们知道在Redux中,改变State必须通过dispatchactio以触发reducer,在reducer中返回一个新的state,reducer是一个purefuctio纯函数,无任何副作用,只要入参相同,其返回结果也是相同的,并且是同步执行的。而effect是相对于reducer而言的,与reducer一样,它也必须通过dispatchactio来触发,不同的是:

它是一个非纯函数,可以包含副作用,可以无返回,也可以是异步的。

它不能直接改变State,要改变State,它必须再次dispatchactio来触发reducer

ActioHadler

我们可以简单的认为:在Redux中store.dispatch(actio),可以触发一个注册过的reducer,看起来似乎是一种观察者模式。推广到以上的effect概念,effect同样是一个观察者。一个actio被dispatch,可能触发多个观察者被执行,它们可能是reducer,也可能是effect。所以reducer和effect统称为:ActioHadler

如果有一组actioHadler在兼听某一个actio,那它们的执行顺序是什么呢?

答:当一个actio被dispatch时,最先执行的是所有的reducer,它们被依次同步执行。所有的reducer执行完毕之后,才开始所有effect执行。

我想等待这一组actioHadler全部执行完毕之后,再下一步操作,可是effect是异步执行的,我如何知道所有的effect都被处理完毕了?答:本框架改良了store.dispatch()方法,如果有effect兼听此actio,它会返回一个Promise,所以你可以使用awaitstore.dispatch({type:"search"});来等待所有的effect处理完成。

Module

当我们接到一个复杂的前端项目时,首先要化繁为简,进行功能拆解。通常以高内聚、低偶合的原则对其进行模块划分,一个Module是相对独立的业务功能的集合,它通常包含一个Model(用来处理业务逻辑)和一组View(用来展示数据与交互),需要注意的是:

SPA应用已经没有了Page的边界,不要以Page的概念来划分模块

一个Module可能包含一组View,不要以View的概念来划分模块

Module虽然是逻辑上的划分,但我们习惯于用文件夹目录来组织与体现,例如:

src├── modules│       ├── user│       │     ├── userOverview(Module)│       │     ├── userTrasactio(Module)│       │     └── blacklist(Module)│       ├── aget│       │     ├── agetOverview(Module)│       │     ├── agetBous(Module)│       │     └── agetSale(Module)│       └── app(Module)

通过以上可以看出,此工程包含7大模块app、userOverview、userTrasactio、blacklist、agetOverview、agetBous、agetSale,虽然modules目录下面还有子目录user、aget,但它们仅属于归类,不属于模块。我们约定:

每个Module是一个独立的文件夹

Module本身只有一级,但是可以放在多级的目录中进行归类

每个Module文件夹名即为该Module名,因为所有Module都是平级的,所以需要保证Module名不重复,实践中,我们可以通过Typescript的eum类型来保证,你也可以将所有Module都放在一级目录中。

每个Module保持一定的独立性,它们可以被同步、异步、按需、动态加载

ModuleState、RootState

系统被划分为多个相对独立且平级的Module,不仅体现在文件夹目录,更体现在Store上。每个Module负责维护和管理Store下的一个节点,我们称之为 ModuleState,而整个Store我们习惯称之为RootState

例如:某个Store数据结构:

{router:{...},// StoreReducerapp:{...}, // ModuleStateuserOverview:{...}, // ModuleStateuserTrasactio:{...}, // ModuleStateblacklist:{...}, // ModuleStateagetOverview:{...}, // ModuleStateagetBous:{...}, // ModuleStateagetSale:{...} // ModuleState}

每个Module管理并维护Store下的某一个节点,我们称之为ModuleState

每个ModuleState都是Store的根子节点,并以Module名为Key

每个Module只能修改自已的ModuleState,但是可以读取其它ModuleState

每个Module修改自已的ModuleState,必须通过dispatchactio来触发

每个Module可以观察者身份,监听其它Module发出的actio,来配合修改自已的ModuleState

你可能注意到上面Store的子节点中,第一个名为router,它并不是一个ModuleState,而是一个由第三方Reducer生成的节点。我们知道Redux中允许使用多个Reducer来共同维护Stroe,并提供combieReducers方法来合并。由于ModuleState的key名即为Module名,所以:Module名自然也不能与其它第三方Reducer生成节点重名。

Model

在Module内部,我们可进一步划分为一个model(维护数据)和一组view(展现交互),此处的Model实际上指的是viewmodel,它主要包含两大功能:

ModuleState的定义

ModuleState的维护,前面有介绍过ActioHadler,实际上就是对ActioHadler的编写

数据流是从Model单向流入View,所以Model是独立的,是不依赖于View的。所以理论上即使没有View,整个程序依然是可以通过命令行来驱动的。

我们约定:

集中在一个名为model.js的文件中编写Model,并将此文件放在本模块根目录下

集中在一个名为ModuleHadlers的class中编写所有的ActioHadler,每个reducer、effect都对应该class中的一个方法

例如,userOverview模块中的Model:

src├── modules│       ├── user│       │     ├── userOverview(Module)│       │     │         ├──views│       │     │         └──model.ts│       │     │

src/modules/user/userOverview/model.ts

// 定义本模块的ModuleState类型export iterface State exteds BaseModuleState {  listSearch: {userame:strig; page:umber; pageSize:umber};  listItems: {uid:strig; userame:strig; age:umber}[];  listSummary: {page:umber; pageSize:umber; total:umber};  loadig: {    searchLoadig: LoadigState;  };}// 定义本模块所有的ActioHadlerclass ModuleHadlers exteds BaseModuleHadlers<State, RootState, ModuleNames> {  costructor() {    // 定义本模块ModuleState的初始值    cost iitState: State = {      listSearch: {userame:ull, page:1, pageSize:20},      listItems: ull,      listSummary: ull,      loadig: {        searchLoadig: LoadigState.Stop,      },    };    super(iitState);  }  // 一个reducer,用来update本模块的ModuleState  @reducer  public putSearchList({listItems, listSummary}): State {    retur {...this.state, listItems, listSummary};  }  // 一个effect,使用ajax查询数据,然后dispatch actio来触发以上putSearchList  // this.dispatch是store.dispatch的引用  // searchLoadig指明将这个effect的执行状态注入到State.loadig.searchLoadig中  @effect("searchLoadig")  public asyc searchList(optios: {userame?:strig; page?:umber; pageSize?:umber} = {}) {    // this.state指向本模块的ModuleState    cost listSearch = {...this.state.listSearch, ...optios};    cost {listItems, listSummary} = await api.searchList(listSearch);    this.dispatch(this.actio.putSearchList({listItems, listSummary}));  }  // 一个effect,监听其它Module发出的Actio,然后改变自已的ModuleState  // 因为是监听其它Module发出的Actio,所以它不需要主动触发,使用非public权限对外隐藏  // @effect(ull)表示不需要跟踪此effect的执行状态  @effect(ull)  protected asyc ["@@router/LOCATION_CHANGE]() {      // this.rootState指向整个Store      if(this.rootState.router.locatio.pathame === "/list"){          // 使用await 来等待所有的actioHadler处理完成之后再返回          await this.dispatch(this.actio.searchList());      }  }}

需要特别说明的是以上代码的最后一个ActioHadler:

protected asyc ["@@router/LOCATION_CHANGE](){    // this.rootState指向整个Store    if(this.rootState.router.locatio.pathame === "/list"){        await this.dispatch(this.actio.searchList());    }}

前面有强调过两点:

Module可以兼听其它Module发出的Actio,并配合来完成自已ModuleState的更新。

Module只能更新自已的ModuleState节点,但是可以读取整个Store。

另外注意到语句:awaitthis.dispatch(this.actio.searchList()):

dispatch派发一个名为searchList的actio可以理解,可是为什么前面还能awiat?难道dispatchactio也是异步的?

答:dispatch派发actio本身是同步的,我们前面讲过ActioHadler的概念,一个actio被dispatch时,可能有一组reducer或effect在兼听它,reducer是同步处理的,可是effect可能是异步处理的,如果你想等所有的兼听都执行完成之后,再做下一步操作,此处就可以使用await,否则,你可以不使用await。

View、Compoet

在Module内部,我们可进一步划分为一个model(维护数据)和一组view(展现交互)。所以一个Module中的view可能有多个,我们习惯在Module根目录下创建一个名为views的文件夹:

例如,userOverview模块中的views:

src├── modules│       ├── user│       │     ├── userOverview(Module)│       │     │         ├──views│       │     │         │     ├──imgs│       │     │         │     ├──List│       │     │         │     │     ├──idex.css│       │     │         │     │     └──idex.ts│       │     │         │     ├──Mai│       │     │         │     │    ├──idex.css│       │     │         │     │    └──idex.ts│       │     │         │     └──idex.ts│       │     │         ││       │     │         ││       │     │         └──model.ts│       │     │

每个view其实是一个ReactCompoet类,所以使用大写字母打头

对于css和img等附属资源,如果是属于某个view私有的,跟随view放到一起,如果是多个view公有的,提出来放到公共目录中。

view可以嵌套,包括可以给别的Module中的view嵌套,如果需要给别的Module使用,必须在views/idex.ts中使用exportView()导出。

在view中通过dispatchactio的方式触发Model中的ActioHadler,除了可以dispatch本模块的actio,也能dispatch其它模块的actio

例如,某个LogiForm:

iterface Props exteds DispatchProp {  logiig: boolea;}class Compoet exteds React.PureCompoet<Props> {  public oLogi = (evt: ay) => {    evt.stopPropagatio();    evt.prevetDefault();    // 发出本模块的actio,将触发本model中定义的名为logi的ActioHadler    this.props.dispatch(thisModule.actios.logi({userame: "", password: ""}));  };  public reder() {    cost {logiig} = this.props;    retur (      <form className="app-Logi" oSubmit={this.oLogi}>        <h3>请登录</h3>        <ul>          <li><iput ame="userame" placeholder="Userame" /></li>          <li><iput ame="password" type="password" placeholder="Password" /></li>          <li><iput type="submit" value="Logi" disabled={logiig} /></li>        </ul>      </form>    );  }}cost mapStateToProps = (state: RootState) => {  retur {    logiig: state.app.loadig.logi !== LoadigState.Stop,  };};export default coect(mapStateToProps)(Compoet);

从以上代码可看出,View就是一个Compoet,那View和Compoet有区别吗?编码上没有,逻辑上是有的:

view体现的是ModuleState的视图展现,更偏重于表现特定的具体的业务逻辑,所以它的props一般是直接用mapStateToPropscoect到store。

compoet体现的是一个没有业务逻辑上下文的纯组件,它的props一般来源于父级传递。

compoet通常是公共的,而view通常非公用

路由与动态加载

react-coat赞同react-router4 组件化路由的理念,路由即组件,嵌套路由好比嵌套compoet一样简单,无需繁琐的配置。如:

import {BottomNav} from "modules/avs/views"; // BottomNav 来自于 avs 模块import LogiForm from "./LogiForm"; // LogiForm 来自于本模块// PhotosView 和 VideosView 分别来自于 photos 模块和 videos 模块,使用异步按需加载cost PhotosView = loadView(moduleGetter, ModuleNames.photos, "Mai");cost VideosView = loadView(moduleGetter, ModuleNames.videos, "Mai");<div className="g-page">    <Switch>        <Route exact={false} path="/photos" compoet={PhotosView} />        <Route exact={false} path="/videos" compoet={VideosView} />        <Route exact={true} path="/logi" compoet={LogiForm} />    </Switch>    <BottomNav /></div>

以上某个view中以不同加载方式嵌套了多个其它view:

BottomNav是一个名为avs模块下的view,直接嵌套意味着它会同步加载到本view中

LogiForm是本模块下的一个view,所以直接用相对路径引用,同样直接嵌套,意味着它会同步加载

PhotosView和VideosView来自于别的模块,但是是通过loadView()获取和Route嵌套,意味着它们会异步按需加载,当然你也可以直接import{PhotosView}from"modules/photos/views"来同步按需加载

所以本框架对于模块和视图的加载灵活简单,无需复杂配置与修改:

不管是同步、异步、按:需、动态加载,要改变的仅仅是加载方式,而不用修改被加载的模块。模块本身并不需要事先拟定自已将被谁、以何种方式加载,保证的模块的独立性。

前面讲过,view是model数据的展现,那嵌入其它模块view时,是否还要导入其它模块的model呢?无需,框架将自动导入。

几个特殊的Actio

@@router/LOCATION_CHANGE:本框架集成了coected-react-router,路由发生变化时将触发此actio,你可以在moduleHadlers中监听此actio

"@@framework/ERROR:本框架catch了未处理的error,发生error时将自动派发此actio,你可以在moduleHadlers中监听此actio

module/INIT:模块初次载入时会触发此actio,来向store注入初始moduleState

module/LOADING:触发加载进度时会触发此actio,比如@effect(logi)

功能介绍

react 生态圈的开放、自由、繁荣,也导致开发配置繁琐、选择迷茫。react-coat 放弃某些灵活性、以约定替代某些配置,固化某些最佳实践方案,从而提供给开发者一个更简洁的糖衣外套。 你还在...

声明:本文仅代表作者观点,不代表本站立场。如果侵犯到您的合法权益,请联系我们删除侵权资源!如果遇到资源链接失效,请您通过评论或工单的方式通知管理员。未经允许,不得转载,本站所有资源文章禁止商业使用运营!
下载安装【程序员客栈】APP
实时对接需求、及时收发消息、丰富的开放项目需求、随时随地查看项目状态

评论