作者:高天阳
邮箱:13683265113@163.com
Copy 更改历史
* 2019-08-22 高天阳 初始化文档
背景
redux存在的问题
项目中redux的样板文件太分散,书写和维护都比较麻烦
快速开始
Copy npm install @rematch/core
Init
init 用来配置你的 reducers, devtools & store。
index.js
Copy import { init } from '@rematch/core'
import * as models from './models'
const store = init ({
models ,
})
对于更高级的设置,查看插件 和Redux配置选项 。
Models
该model促使state, reducers, async actions 和 action creators 放在同一个地方。
models.js
Copy export const count = {
state : 0 , // initial state
reducers : {
// handle state changes with pure functions
increment (state , payload) {
return state + payload
}
} ,
effects : {
// handle state changes with impure functions.
// use async/await for async actions
async incrementAsync (payload , rootState) {
await new Promise (resolve => setTimeout (resolve , 1000 ))
this .increment (payload)
}
}
}
查看reducer 文档以了解更多信息,包括如何从其他 modal 触发 actions。
理解模型与回答几个问题一样简单:
我如何处理异步 action? effects with async/await
Dispatch
dispatch 是我们如何在你的model中触发 reducers 和 effects。 Dispatch 标准化了你的action,而无需编写action types 或者 action creators。
Copy import { dispatch } from '@rematch/core'
// state = { count: 0 }
// reducers
dispatch ({ type : 'count/increment' , payload : 1 }) // state = { count: 1 }
dispatch . count .increment ( 1 ) // state = { count: 2 }
// effects
dispatch ({ type : 'count/incrementAsync' , payload : 1 }) // state = { count: 3 } after delay
dispatch . count .incrementAsync ( 1 ) // state = { count: 4 } after delay
Dispatch 能被直接调用,或者用 dispatch[model][action](payload)
简写。
View
Copy import React from 'react'
import ReactDOM from 'react-dom'
import { Provider , connect } from 'react-redux'
import store from './index'
const Count = props => (
< div >
The count is { props .count}
< button onClick = { props .increment}>increment</ button >
< button onClick = { props .incrementAsync}>incrementAsync</ button >
</ div >
)
const mapState = state => ({
count : state .count
})
const mapDispatch = ({ count: { increment , incrementAsync }}) => ({
increment : () => increment ( 1 ) ,
incrementAsync : () => incrementAsync ( 1 )
})
const CountContainer = connect (mapState , mapDispatch)(Count)
ReactDOM .render (
< Provider store = {store}>
< CountContainer />
</ Provider > ,
document .getElementById ( 'root' )
)
API
Copy import { init } from '@rematch/core' ;
const store = init ({
models : {
count : {
state : 0 ,
reducers : {
add : (state , payload) => state + payload ,
del : (state , payload) => state - payload ,
'otherModel/actionName' : (state , payload) => state + payload ,
} ,
effets : {
async loadData (payload , rootState) {
const response = await fetch ( 'http://example.com/data' )
const data = await response .json ()
this .add (data)
}
}
} ,
list : {}
} ,
redux : {
reducers : {} ,
middlewares : [thunk] ,
} ,
plugins : [loading]
})
init
对rematch进行初始化,返回一个store对象,包含了使用redux初始化store对象的所有字段。
models: { [string]: model }
一个对象,属性的键作为rootState上的的键
model.state: any
用来初始化model
model.reducers: { [string]: (state, payload) => any }
一个对象,属性是用来改变model state的方法,第一个参数是这个model的上一个state, 第二个参数是payload,函数返回model下一个state。这些方法应该是纯函数。
model.effects: { [string]: (payload, rootState) }
一个对象,异步或者非纯函数的方法放在这个对象中,可以与async/await一起使用
redux
通过这个属性,可以兼容老项目中的redux配置。
plugins
rematch是一个插件系统,通过这个字段可以配置第三方的插件。
redux流程:
rematch流程:
示例
简单示例
index.js
Copy import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { init } from '@rematch/core'
import App from './App'
const count = {
state : 0 ,
reducers : {
increment : s => s + 1 ,
} ,
effects : dispatch => ({
async asyncIncrement () {
await new Promise (resolve => {
setTimeout (resolve , 1000 )
})
dispatch . count .increment ()
} ,
}) ,
}
const store = init ({
module : {
count ,
}
})
// Use react-redux's <Provider /> and pass it the store.
ReactDOM .render (
< Provider store = {store}>
< App />
</ Provider > ,
document .getElementById ( 'root' )
)
App.js
Copy import React from 'react'
import { connect } from 'react-redux'
// Make a presentational component.
// It knows nothing about redux or rematch.
const App = ({ count , asyncIncrement , increment }) => (
< div >
< h2 >
count is < b style = {{ backgroundColor : '#ccc' }}>{count}</ b >
</ h2 >
< h2 >
< button onClick = {increment}>Increment count</ button >{ ' ' }
< em style = {{ backgroundColor : 'yellow' }}>(normal dispatch)</ em >
</ h2 >
< h2 >
< button onClick = {asyncIncrement}>
Increment count (delayed 1 second)
</ button >{ ' ' }
< em style = {{ backgroundColor : 'yellow' }}>(an async effect!!!)</ em >
</ h2 >
</ div >
)
const mapState = state => ({
count : state .count ,
})
const mapDispatch = dispatch => ({
increment : dispatch . count .increment ,
asyncIncrement : dispatch . count .asyncIncrement ,
})
// Use react-redux's connect
export default connect (
mapState ,
mapDispatch
)(App)
复杂数据结构
一般来讲state中的数据结构是以对象的形式存储的,因为不只存储一个数据,那么例子需要作出适当调整
index.js
Copy import React from 'react' ;
import ReactDOM from 'react-dom' ;
import { Provider } from 'react-redux' ;
import { init } from '@rematch/core' ;
import App from './App' ;
const count = {
state : {
num : 0 ,
flag : true ,
flagShow : true
} ,
reducers : {
increment : (state , payload) => {
return {
... state ,
num : payload
}
} ,
toggle : (state , payload) => {
return {
... state ,
flag : payload
}
} ,
toggleShow : (state , payload) => {
return {
... state ,
flagShow : payload
}
} ,
} ,
effects : dispatch => ({
async asyncIncrement (s) {
// 需要处理数据可在这里进行处理
s = ++ s
await new Promise (resolve => {
setTimeout (resolve , 1000 )
})
dispatch . count .increment (s)
} ,
}) ,
}
const store = init ({
models : {
count
}
})
// Use react-redux's <Provider /> and pass it the store.
ReactDOM .render (
< Provider store = {store}>
< App />
</ Provider > ,
document .getElementById ( 'root' )
)
app.js
Copy import React from 'react' ;
import { connect } from 'react-redux'
import { Componet } from './componet' ;
class App extends React . Component {
// 需要初始化本模块state的时候通过constructor
// constructor(props) {
// super(props)
// }
increment () {
this . props .increment ( this . props . count .num + 1 )
}
asyncIncrement () {
this . props .asyncIncrement ( this . props . count .num + 1 )
}
// if切换
toggle () {
this . props .toggle ( ! this . props . count .flag)
}
// show 切换
toggleShow () {
this . props .toggleShow ( ! this . props . count .flagShow)
}
// 子组件传值
handleClick (msg , name) {
console .log ( '子组件传回父组件' )
console .log (msg , name);
}
// 通过匿名函数绑定this
// handleClick = (msg, name) => {
// console.log('子组件传回父组件')
// console.log(msg, name);
// }
render () {
return (
< div >
< h2 >
count is < b style = {{backgroundColor : '#ccc' }}>{ this . props . count .num}</ b >
</ h2 >
< h2 >
< button onClick = { this . increment .bind ( this )}>增加count</ button >
{ ' ' }
< em style = {{backgroundColor : 'yellow' }}>(normal dispatch)</ em >
</ h2 >
< h2 >
< button onClick = { this . asyncIncrement .bind ( this )}>
Increment count (delayed 1 second)
</ button >
{ ' ' }
< em style = {{backgroundColor : 'yellow' }}>(an async effect!!!)</ em >
</ h2 >
< h2 >
< button onClick = { this . toggle .bind ( this )}>类v-if实现</ button >
< button onClick = { this . toggleShow .bind ( this )}>类v-show实现</ button >
{ ' ' }
< em style = {{backgroundColor : 'yellow' }}>(normal dispatch)</ em >
</ h2 >
< h2 >
{ 'v-if当前值' }{ this . props . count .flag ? 'true' : 'false' }
</ h2 >
< h2 >
{ 'v-show当前值' }{ this . props . count .flagShow ? 'true' : 'false' }
</ h2 >
< Componet
title = "仿v-if、v-show"
msg = "仿v-if、v-show"
v-if = { this . props . count .flag}
v-show = { this . props . count .flagShow}
onClick = { this . handleClick .bind ( this )}
// 通过匿名函数绑定this
// onClick={this.handleClick}
/>
</ div >
)
}
}
const mapState = state => ({
count : state .count ,
})
const mapDispatch = dispatch => ({
increment : dispatch . count .increment ,
toggle : dispatch . count .toggle ,
toggleShow : dispatch . count .toggleShow ,
asyncIncrement : dispatch . count .asyncIncrement ,
})
export default connect (
mapState ,
mapDispatch
)(App)
仿v-if、v-show效果展示
component.js
Copy import React , { Component } from 'react' ;
let name = 'zhangsan' ;
let msg = {
name : 'zhangsan' ,
age : 1212
};
export const Componet = (props) => {
// console.log(props);
if (props[ 'v-if' ]) {
let isShow = props[ 'v-show' ] ? 'block' : 'none' ;
return (
// 通过匿名函数绑定this
// <ul onClick={props.onClick.bind(this, msg, name)} style={{display: isShow}} >
< ul onClick = {() => props .onClick (msg , name)} style = {{display : isShow}} >
< li >${name}</ li >
< li >${ props .title}</ li >
< li >${ props .msg}</ li >
< li >zhangsan</ li >
</ ul >
);
} else {
return (< div ></ div >);
}
};
rematch功能划分
rematch流程走向
最佳实践
业务逻辑实现
如下图所示,当选择公司性质为 “默认” 或者 “个人” 时候 ‘公司全称’字段是隐藏。当我们选择公司性质为“公司”时 ‘公司全称’字段展示。
默认状态:
公司性质为“公司”
这个功能该如何实现呢?
思路:
在‘公司性质’这个 select框上面绑定一个 onchange事件,每次变化 获取到当前select框 对应的值。
将获取到当前select框的值写一个接口去调用它(如果使用react, 则在 reducer里面定义),并将每次返回的值在传递给前端页面;
根据接口返回的select的值来对应显示“公司性质”的显示或者隐藏。
代码如下:
newEdit.js — view层 — 父组件
Copy import Form from './components/basicForm' ;
onSwitchAccountCategory = (value) => { /*将从接口获取到的数据子组件 ,这里传递了所有的props数据,和一个函数onSwitchAccountCategory ,传递的函数用于从子组件获取到select框的值传递给父组件*/
this . props .dispatch ({
type : ` ${ this .module } /switchAccountCategory` ,
payload : value
});
}
buildForm = (props) => {
return (
< Form { ... props} onSwitchAccountCategory = { this .onSwitchAccountCategory} />
);
}
basicForm.js — view层—子组件
Copy import { FormInputField } from 'components/common' ;
render = () => {
let { entity } = this .props;
return (
< Form >
< FormInputField
getFieldDecorator = {getFieldDecorator}
label = "公司性质"
fieldProps = {{style : {width : 120 } , onChange : this . props .onSwitchAccountCategory}} /*onChange 方法 */
field = "accountCategory"
fieldDecoratorOptions = {
{
rules : [
{
required : true ,
message : '请选择公司性质'
}
] ,
validateTrigger : [ 'onSubmit' ]
}
}
entity = {entity}
datasource = { CompanyNature .toArray () .map (item => ({ text : item .text , id : item .value }))}
type = "select" />
/*---------------------------------------------------------------------------------------*/
/* 将获取的select框的值转换,判断是否等于公司来控制其 显示 或者 隐藏*/
{
CompanyNature .getAliasFromValue ( entity .accountCategory) === '公司' &&
< FormInputField
getFieldDecorator = {getFieldDecorator}
label = "公司全称"
field = "companyName"
required = { false }
fieldDecoratorOptions = {
{
rules : [
{
required : false ,
whitespace : true ,
message : "请输入公司全称"
} ,
{
validator : this .verifyCompanyName ,
}
] ,
validateTrigger : [ 'onSubmit' ]
}
}
entity = {entity}
key = "companyName" />}
</ Form >)
)
edit.js — Model层
Copy reducers : {
switchAccountCategory (state , action) { //切换公司性质
return {
... state ,
entity : {
... state .entity ,
accountCategory : action .payload /* 将获取到的select框的值传递给view层 */
}
};
} ,
}
老项目接入
主要针对已经使用thunk中间键的老项目。
安装依赖,并删除依赖中的redux
yarn add @rematch/core
yarn remove redux (删除redux可能会造成eslint报错)
修改redux入口文件
Copy src / store / index .js
import { init } from '@rematch/core' ;
import thunk from 'redux-thunk' ;
import reduxReducerConfig from '@/reducers' ;
import models from '../models' ;
const store = init ({
models ,
redux : {
reducers : {
... reduxReducerConfig
} ,
middlewares : [thunk] ,
} ,
});
export default store;
修改reducers的入口文件
Copy import { routerReducer as routing } from 'react-router-redux' ;
- import { combineReducers } from 'redux' ;
import dispatchConfigReducer from './dispatch-config' ;
import counterReducer from './count' ;
- export default combineReducers ({
- routing ,
- dispatchConfigReducer ,
- counterReducer ,
- });
+ export default {
+ routing ,
+ dispatchConfigReducer ,
+ counterReducer ,
+ };
增加model的入口文件
Copy + src / models
+ src / models / re - count .js
+ src / models / config - list .js
+ src / models / index .js
index .js
import reCount from './re-count' ;
import configList from './config-list' ;
export default {
reCount ,
configList ,
};
如果老项目中没有使用redux,可以使用yarn remove thunk
删除thunk的依赖和reducers这个文件夹, 并且在init初始化的时候可以不用传redux这个配置。如果接入rematch,需要锁定版本, rematch中引入的redux版本为4.0.0,所以老项目中的
新项目配置
Copy index .js
import React from 'react' ;
import { render } from 'react-dom' ;
import { browserHistory , Router } from 'react-router' ;
import { syncHistoryWithStore } from 'react-router-redux' ;
import { Provider } from 'react-redux' ;
import routes from '@/routes' ;
import store from '@/store' ;
import '@/styles/index.less' ;
const history = syncHistoryWithStore (browserHistory , store);
render (
< Provider store = {store}>
< Router history = {history} routes = {routes} />
</ Provider > ,
document .getElementById ( 'root' ) ,
);
---------------------------------------------------------------------------------------
// 新建store文件夹,并添加index.js
import { init } from '@rematch/core' ;
import { routerReducer as routing } from 'react-router-redux' ;
import models from '../models' ;
const store = init ({
models ,
redux : {
reducers : {
routing ,
} ,
} ,
});
export default store;
---------------------------------------------------------------------------------------
// 新建models文件夹,并添加index
models结构
├── common
│ ├── bizLineList .js
│ └── index .js
└── index .js
bug
Redux DevTools 要升级到最新版,2.16.0有bug
同类技术比较
基于redux数据流的管理方案:Dva
、mirror
和rematch
Dva
Dva是蚂蚁金服开源的一个数据流管理方案,基于redux和redux-saga,简化了开发体验。 Dva是一揽子的解决方案,可以使用侵入性很强的dva-cli来快速搭建项目,提供了路由层面的适配; 也可以使用dva-core来引入核心的代码,减少侵入性。
缺点
如果使用Dva的一整套框架,现有的项目会有较大的改动
Dva使用redux-saga来处理异步,学习成本比较高
mirror
mirror 类似于Dva的一个redux数据流方案,最新一次更新在两个月之前,一直没有发布1.0的版本
rematch
rematch 的灵感来自于Dva和mirror,将两者的有点结合了起来。
优点
使用了类似Dva的model文件结构,统一管理同步和异步操作
通过中间键实现了async/await的方式来处理异步,舍弃了Dva中的redux-saga
缺点
将model中reducers和effects的方法挂载在dispatch函数上,造成dispatch既是一个函数,又是一个对象
参考资料