前言
本章介绍react-redux的状态管理、redux-saga的异步流程管理、react-router的路由管理,这些都被Dva框架集成进去,并加以封装。有了这些基础知识,理解Dva就会轻松很多。
react-redux
react-redux是redux基于react框架做的封装。主要工作就是将redux中的state注入到组件中(mapStateToProps),将组件中的用户行为dispatch到redux中(mapDispatchToProps),然后使用connect这个高阶函数包裹组件,自动注入上述逻辑,Provider作为最外层的高阶组件,给组件树中注入store。 下面介绍这几个API的核心原理:
- mapStateToProps
const mapStateToProps = (state) => {
return {
color: state.color,
name: state.name,
...
}
}
mapStateToProps就是一个函数,接受store中的state,然后返回一个对象(包含部分state属性)
- mapDispatchToProps
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', color: color })
}
}
}
mapDispatchToProps也是一个函数,接受store中的dispatch,然后返回一个对象(组件和store中行为的映射关系)
- connect
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {} // 防止 mapStateToProps 没有传入
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {} // 防止 mapDispatchToProps 没有传入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
//例子
ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)
connect会将mapStateToProps和mapDispatchToProps执行的结果合并为props,注入到WrappedComponent中,在WrappedComponent内部就可以通过this.props.xxx来使用state和dispatch用户行为了。
- Provider
class Provider extends Component {
static propTypes = {
store: PropTypes.object,
children: PropTypes.any
}
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
return {
store: this.props.store
}
}
render () {
return (
<div>{this.props.children}</div>
)
}
}
//使用例子,把 Provider 作为组件树的根节点
ReactDOM.render(
<Provider store={store}>
<Index />
</Provider>,
document.getElementById('root')
)
Provider就是将传入的store注入到context中,然后组件内都可以从context中取到store了。最新版本的react-redux使用了新的React.createContext()来完成context的注入,原理是一样的。
小结
有了这些API,我们就可以在react自由地使用redux管理状态了,核心就是组件的props上被注入了很多属性和方法,方便和store进行交互
redux-saga
一个高效管理副作用,让你的应用方便测试的redux异步流程管理库
saga基本流程
saga本身就是redux的一个中间件,用来处理异步逻辑。 所以第一步就是引入'redux-saga'库的createSagaMiddleware方法,用来生成saga实例。
- 创建一个 Saga middleware 和要运行的 Saga
- 将这个 Saga middleware 连接至 Redux store
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
//...
import { incrementAsync } from './sagas'
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(incrementAsync);
saga作为中间件注入完成后,开始做异步调用了。
这里我们用延迟1秒来模拟api调用。在组件里,dispatch对应的action来触发saga的逻辑
function render() {
ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
}
与 redux-thunk 不同,上面组件 dispatch 的是一个 plain Object 的 action。现在react的组件dispatch了action,接下来我们需要在saga内部监听这个action,进行响应逻辑。
// Our worker Saga: 将执行异步的 increment 任务
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
// Our watcher Saga: 在每个 INCREMENT_ASYNC action spawn 一个新的 incrementAsync 任务
export function* watchIncrementAsync() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
上面的逻辑很简单,saga会监听INCREMENT_ASYNC,并在接收到INCREMENT_ASYNC时,触发incrementAsync函数,执行异步逻辑。watchIncrementAsync会export到启动文件,项目初始化时注入到saga中间件,这样一个简单的saga流程就搭建好了。
saga核心API
接下来介绍,saga框架内部的一些基础知识,主要是一些核心API,这些API都被Dva框架集成进去并加以改造,使用更加方便
- takeEvery
这是一个最常见的api,上文提到过,用来监听主流程的action。
function* watchFetchData() {
yield* takeEvery('FETCH_REQUESTED', fetchData)
}
注:takeEvery 允许多个 fetchData 实例同时启动。在某个特定时刻,尽管之前还有一个或多个 fetchData 尚未结束,我们还是可以启动一个新的 fetchData 任务。
function* watchFetchData() {
yield* takeEvery('FETCH_REQUESTED', fetchData)
yield* takeEvery('DELETE_REQUESTED', fetchData)
}
- takeLatest
如果我们只想得到最新那个请求的响应(例如,始终显示最新版本的数据)。我们可以使用 takeLatest 辅助函数。
import { takeLatest } from 'redux-saga'
function* watchFetchData() {
yield* takeLatest('FETCH_REQUESTED', fetchData)
}
- call
call仅仅只是创建一条描述函数调用的信息。
// Effect -> 调用 Api.fetch 函数并传递 `./products` 作为参数
{
CALL: {
fn: Api.fetch,
args: ['./products']
}
}
语义和js里面绑定函数this的call类似,用指定的参数去执行当前的函数
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}
call 同样支持调用对象方法,你可以使用以下形式,为调用的函数提供一个 this 上下文:
yield call([obj, obj.method], arg1, arg2, ...) // 如同 obj.method(arg1, arg2 ...)
这主要是为了方便测试generator的流程,其实功能实现上,完全可以这么写:
function* fetchProducts() {
const products = yield Api.fetch('/products')
console.log(products)
}
但是在做单元测试的时候,没法比较返回值。关于单元测试的细节,后面会讲解,这里了解下即可。
- put
put用来发起action到store中,上面我们通过call调用了API接口,获取了数据,需要把数据存储到store中。其实功能上完全可以这么写:
function* fetchProducts(dispatch)
const products = yield call(Api.fetch, '/products')
dispatch({ type: 'PRODUCTS_RECEIVED', products })
}
但是,问题还是不方便测试,测试时无法真实模拟一个dispatch函数,所以引入了put。
import { call, put } from 'redux-saga/effects'
//...
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// 创建并 yield 一个 dispatch Effect
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
总结 : call用来处理异步逻辑,比如调用接口,put用于数据获取,更新后,发起action到store中,更新state。call和put都是为了测试generator逻辑而引入的,模拟真实的函数调用,返回一个plain javascript对象,方便测试。
以上就是构建基本saga工作流需要用的概念,下面是一些复杂逻辑所涉及的api
- take
take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action
function* watchAndLog() {
while (true) {
const action = yield take('*')
const state = yield select()
console.log('action', action)
console.log('state after', state)
}
}
与call一样,take也会暂停generator,直到监听的action被发起,这里我们监听任何action。
- fork
当我们 fork 一个 任务,任务会在后台启动,调用者也可以继续它自己的流程,而不用等待被 fork 的任务结束。是无阻塞版的call函数。
举个简单的实例,我们模拟下用户登陆的行为:
import { take, call, put } from 'redux-saga/effects'
import Api from '...'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
return token
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
}
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if(token) {
yield call(Api.storeItem({token}))
yield take('LOGOUT')
yield call(Api.clearItem('token'))
}
}
}
上面的逻辑是用户登陆,调用接口进行身份验证获取用户token,验证通过后,我们监听LOGOUT行为,暂停登陆流程,等待登出行为,匹配时则进行清理逻辑。 好像很正常,没什么问题。但是我们没有考虑请求失败和异步调用阻塞的问题。所以,我们必须完善下上面的流程代码。
- 首先是阻塞问题,generator在
yield call(authorize, user, password)此处等待,如果用户此时点击登出,代码还没有执行到yield take('LOGOUT')无法响应登出行为,就出错了。所以,我们不能让流程在验证接口时阻塞,所以需要引进一个新的函数fork来替代阻塞的call。如果在接口调用过程中,用户登出,我们就取消验证任务。 - 现在加上接口容错处理,只需要加上错误处理逻辑就行,监听一个请求失败action。
yield take(['LOGOUT', 'LOGIN_ERROR'])
进行补充后的完整业务逻辑就完成了:
import { take, put, call, fork, cancel } from 'redux-saga/effects'
// ...
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
// fork return a Task object
const task = yield fork(authorize, user, password)
const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
if(action.type === 'LOGOUT')
yield cancel(task)
yield call(Api.clearItem('token'))
}
}
注:上面api函数的使用的返回值,不用太在意,都是plain javascript对象,方便进行流程测试。
小结
本节主要介绍了saga的几个核心API,通过它们组建完整的异步流程。
- call-发起异步请求,处理副作用
- put-获取数据后,发起一个行为到store中,修改state
- take-暂停generator,告诉 middleware 等待一个特定的 action
- fork-任务会在后台启动,是无阻塞版的call函数
react-router介绍
React Router的作用就是保持URL与组件的映射关系。根据当前页面路径,渲染不同的页面的组件
前端路由主要分为hash模式和history模式。hash的原理主要是监听hashchange事件,处理相应的逻辑。history主要是利用H5的pushState和replaceState实现页面的跳转。其中hash和history的区别和取舍就不详述了,相关的文档资料很多
react-router还有一个重要的知识点就是V4版本之后加入了动态路由,官方推荐大家把Route当作组件来使用,并且不要在Route中嵌套Route,多路由匹配时使用switch包裹,只渲染匹配到的第一个路由
V3中使用路由例子:
import { Router, Route, IndexRoute } from 'react-router'
const PrimaryLayout = props => (
<div className="primary-layout">
<header>
Our React Router 3 App
</header>
<main>
{props.children}
</main>
</div>
)
const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<Router history={browserHistory}>
<Route path="/" component={PrimaryLayout}>
<IndexRoute component={HomePage} />
<Route path="/users" component={UsersPage} />
</Route>
</Router>
)
render(<App />, document.getElementById('root'))
所有的路由都陈列在入口文件,非常直观,但是不够灵活,与react组件化思想相违背
V4中使用路由例子:
import { BrowserRouter, Route } from 'react-router-dom'
const PrimaryLayout = () => (
<div className="primary-layout">
<header>
Our React Router 4 App
</header>
<main>
<Route path="/" exact component={HomePage} />
<Route path="/users" component={UsersPage} />
</main>
</div>
)
const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>
const App = () => (
<BrowserRouter>
<PrimaryLayout />
</BrowserRouter>
)
render(<App />, document.getElementById('root'))
v4 中 react-router 仓库拆分成了多个包进行发布,
react-router 路由基础库
react-router-dom 浏览器中使用的封装
react-router-native React native 封装
示例中 BrowserRouter 组件便来自 react-router-dom 这个包
动态路由散落项目各个地方,Route会当作组件,按需使用
总结
本节介绍了react-redux、redux-saga、react-router的基本知识和使用技巧,这么都是组成react整个生态的核心,掌握了这些,会对react框架的使用和Dva的学习更加得心应手。