在上一篇咱们介绍了Webpack自动化构建React应用,咱们的本地开发服务器能够较好的支持咱们编写React应用,而且支持代码热更新。本节将开始详细分析如何搭建一个React应用架构。javascript
我的博客html
如今已经有不少脚手架工具,如create-react-app,支持一键建立一个React应用项目结构,很方便,可是享受方便的同时,也失去了对项目架构及技术栈完整学习的机会,并且一般脚手架建立的应用技术架构并不能彻底知足咱们的业务需求,须要咱们本身修改,完善,因此若是但愿对项目架构有更深掌控,最好仍是从0到1理解一个项目。前端
咱们此次的实践不许备使用任何脚手架,因此咱们须要本身建立每个文件,引入每个技术和三方库,最终造成完整的应用,包括咱们选择的完整技术栈。java
第一步,固然是建立目录,咱们在上一篇已经弄好,若是你尚未代码,能够从Github获取:node
git clone https://github.com/codingplayboy/react-blog.git
cd react-blog
复制代码
生成项目结构以下图:react
src
为应用源代码目录;webpack
为webpack配置目录;webpack.config.js
为webpack配置入口文件;package.json
为项目依赖管理文件;yarn.lock
为项目依赖版本锁文件;.babelrc
文件,babel的配置文件,使用babel编译React和JavaScript代码;eslintrc
和eslintignore
分别为eslint语法检测配置及须要忽略检查的内容或文件;postcss.config.js
为CSS后编译器postcss的配置文件;API.md
为API文档入口;docs
为文档目录;README.md
为项目说明文档;接下来的工做主要就是丰富src
目录,包括搭建项目架构,开发应用功能,还有自动化,单元测试等,本篇主要关注项目架构的搭建,而后使用技术栈实践开发几个模块。webpack
项目架构搭建很大部分依赖于项目的技术栈,因此先对整个技术栈进行分析,总结:ios
根据以上划分决定选用如下第三方库和工具构成项目的完整技术栈:git
针对以上分析,完善后的项目结构如图:
React应用开发目前已经有诸多调试工具,经常使用的如redux-devtools,Reactron等。
redux-devtools是支持热重载,回放action,自定义UI的一款Redux开发工具。
首先须要按照对应的浏览器插件,而后再Redux应用中添加相关配置,就能在浏览器控制台中查看到redux工具栏了,详细文档点此查看。
而后安装项目依赖库:
yarn add --dev redux-devtools
复制代码
而后在建立redux store时将其做为redux强化器传入createStore
方法:
import { applyMiddleware, compose, createStore, combineReducers } from 'redux'
// 默认为redux提供的组合函数
let composeEnhancers = compose
if (__DEV__) {
// 开发环境,开启redux-devtools
const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
if (typeof composeWithDevToolsExtension === 'function') {
// 支持redux开发工具拓展的组合函数
composeEnhancers = composeWithDevToolsExtension
}
}
// create store
const store = createStore(
combineReducers(...),
initialState,
// 组合redux中间价和增强器,强化redux
composeEnhancers(
applyMiddleware(...middleware),
...enhancers
)
)
复制代码
Reactotron是一款跨平台调试React及React Native应用的桌面应用,能动态实时监测并输出React应用等redux,action,saga异步请求等信息,如图:
首先安装:
yarn add --dev reactotron-react-js
复制代码
而后初始化Reactotron相关配置:
import Reactotron from 'reactotron-react-js';
import { reactotronRedux as reduxPlugin } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga';
if (Config.useReactotron) {
// refer to https://github.com/infinitered/reactotron for more options!
Reactotron
.configure({ name: 'React Blog' })
.use(reduxPlugin({ onRestore: Immutable }))
.use(sagaPlugin())
.connect();
// Let's clear Reactotron on every time we load the app
Reactotron.clear();
// Totally hacky, but this allows you to not both importing reactotron-react-js
// on every file. This is just DEV mode, so no big deal.
console.tron = Reactotron;
}
复制代码
而后启使用console.tron.overlay
方法拓展入口组件:
import './config/ReactotronConfig';
import DebugConfig from './config/DebugConfig';
class App extends Component {
render () {
return (
<Provider store={store}>
<AppContainer />
</Provider>
)
}
}
// allow reactotron overlay for fast design in dev mode
export default DebugConfig.useReactotron
? console.tron.overlay(App)
: App
复制代码
至此就可使用Reactotron客户端捕获应用中发起的全部的redux和action了。
React组件化开发原则是组件负责渲染UI,组件不一样状态对应不一样UI,一般遵循如下组件设计思路:
展现型组件 | 容器组件 | |
---|---|---|
目标 | UI展现 (HTML结构和样式) | 业务逻辑(获取数据,更新状态) |
感知Redux | 无 | 有 |
数据来源 | props | 订阅Redux store |
变动数据 | 调用props传递的回调函数 | Dispatch Redux actions |
可重用 | 独立性强 | 业务耦合度高 |
如今的任何大型web应用若是少了状态管理容器,那这个应用就缺乏了时代特征,可选的库诸如mobx,redux等,实际上大同小异,各取所需,以redux为例,redux是最经常使用的React应用状态容器库,对于React Native应用也适用。
Redux是一个JavaScript应用的可预测状态管理容器,它不依赖于具体框架或类库,因此它在多平台的应用开发中有着一致的开发方式和效率,另外它还能帮咱们轻松的实现时间旅行,即action的回放。
Redux中间件,和Node中间件同样,它能够在action分发至任务处理reducer以前作一些额外工做,dispatch发布的action将依次传递给全部中间件,最终到达reducer,因此咱们使用中间件能够拓展诸如记录日志,添加监控,切换路由等功能,因此中间件本质上只是拓展了store.dispatch
方法。
有些时候咱们可能并不知足于拓展dispatch
方法,还但愿能加强store,redux提供以加强器形式加强store的各个方面,甚至能够彻底定制一个store对象上的全部接口,而不只仅是store.dispatch
方法。
const logEnhancer = (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
const originalDispatch = store.dispatch
store.dispatch = (action) => {
console.log(action)
originalDispatch(action)
}
return store
}
复制代码
最简单的例子代码如上,新函数接收redux的createStore方法和建立store须要的参数,而后在函数内部保存store对象上某方法的引用,从新实现该方法,在里面处理完加强逻辑后调用原始方法,保证原始功能正常执行,这样就加强了store的dispatch方法。
能够看到,加强器彻底能实现中间件的功能,其实,中间件就是以加强器方式实现的,它提供的compose
方法就能够组合将咱们传入的加强器拓展到store,而若是咱们传入中间件,则须要先调用applyMiddleware
方法包装,内部以加强器形式将中间件功能拓展到store.dispatch
方法
Redux是一个独立的JavaScript应用状态管理容器库,它能够与React、Angular、Ember、jQuery甚至原生JavaScript应用配合使用,因此开发React应用时,须要将Redux和React应用链接起来,才能统一使用Redux管理应用状态,使用官方提供的react-redux库。
class App extends Component {
render () {
const { store } = this.props
return (
<Provider store={store}>
<div>
<Routes />
</div>
</Provider>
)
}
}
复制代码
react-redux库提供
Provider
组件经过context方式向应用注入store,而后可使用connect
高阶方法,获取并监听store,而后根据store state和组件自身props计算获得新props,注入该组件,而且能够经过监听store,比较计算出的新props判断是否须要更新组件。
更多关于react-redux的内容能够阅读以前的文章:React-Redux分析。
使用redux提供的createStore
方法建立redux store,可是在实际项目中咱们经常须要拓展redux添加某些自定义功能或服务,如添加redux中间件,添加异步任务管理saga,加强redux等:
// creates the store
export default (rootReducer, rootSaga, initialState) => {
/* ------------- Redux Configuration ------------- */
// Middlewares
// Build the middleware for intercepting and dispatching navigation actions
const blogRouteMiddleware = routerMiddleware(history)
const sagaMiddleware = createSagaMiddleware()
const middleware = [blogRouteMiddleware, sagaMiddleware]
// enhancers
const enhancers = []
let composeEnhancers = compose
// create store
const store = createStore(
combineReducers({
router: routerReducer,
...reducers
}),
initialState,
composeEnhancers(
applyMiddleware(...middleware),
...enhancers
)
)
sagaMiddleware.run(saga)
return store;
}
复制代码
redux默认提供了combineReducers
方法整合reduers至redux,然而该默认方法指望接受原生JavaScript对象而且它把state做为原生对象处理,因此当咱们使用createStore
方法而且接受一个Immutable对象做应用初始状态时,reducer
将会返回一个错误,源代码以下:
if (!isPlainObject(inputState)) {
return (
`The ${argumentName} has unexpected type of "` + ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
".Expected argument to be an object with the following +
`keys:"${reducerKeys.join('", "')}"`
)
}
复制代码
如上代表,原始类型reducer接受的state参数应该是一个原生JavaScript对象,咱们须要对combineReducers
其进行加强,以使其能处理Immutable对象,redux-immutable
即提供建立一个能够和Immutable.js协做的Redux combineReducers。
import { combineReducers } from 'redux-immutable';
import Immutable from 'immutable';
import configureStore from './CreateStore';
// use Immutable.Map to create the store state tree
const initialState = Immutable.Map();
export default () => {
// Assemble The Reducers
const rootReducer = combineReducers({
...RouterReducer,
...AppReducer
});
return configureStore(rootReducer, rootSaga, initialState);
}
复制代码
如上代码,能够看见咱们传入的initialState
是一个Immutable.Map
类型数据,咱们将redux整个state树丛根源开始Immutable化,另外传入了能够处理Immutable state的reducers和sagas。
另外每个state树节点数据都是Immutable结构,如AppReducer
:
const initialState = Immutable.fromJS({
ids: [],
posts: {
list: [],
total: 0,
totalPages: 0
}
})
const AppReducer = (state = initialState, action) => {
case 'RECEIVE_POST_LIST':
const newState = state.merge(action.payload)
return newState || state
default:
return state
}
复制代码
这里默认使用Immutable.fromJS()方法状态树节点对象转化为Immutable结构,而且更新state时使用Immutable方法state.merge()
,保证状态统一可预测。
在React web单页面应用中,页面级UI组件的展现和切换彻底由路由控制,每个路由都有对应的URL及路由信息,咱们能够经过路由统一高效的管理咱们的组件切换,保持UI与URL同步,保证应用的稳定性及友好体验。
React Router是完整的React 路由解决方案,也是开发React应用最常使用的路由管理库,只要用过它,绝对会喜欢上它的设计,它提供简单的API,以声明式方式实现强大的路由功能,诸如按需加载,动态路由等。
使用react-router v4版本能够定义跨平台的应用动态路由结构,所谓的动态路由(Dynamic Routing)即在渲染过程当中发生路由的切换,而不须要在建立应用前就配置好,这也正是其区别于静态路由(Static Routing)所在,动态路由提升更灵活的路由组织方式,并且更方便编码实现路由按需加载组件。
在react-router v2和v3版本中,开发React应用须要在开始渲染前就定义好完整的应用路由结构,全部的路由都须要同时初始化,才能在应用渲染后生效,会产生不少嵌套化路由,丧失了动态路由的灵活性和简洁的按需加载编码方式。
在react-router 2.x和3.x版本中,定义一个应用路由结构一般以下:
import React from 'react'
import ReactDOM from 'react-dom'
import { browserHistory, Router, Route, IndexRoute } from 'react-router'
import App from '../components/App'
import Home from '../components/Home'
import About from '../components/About'
import Features from '../components/Features'
ReactDOM.render(
<Router history={browserHistory}>
<Route path='/' component={App}>
<IndexRoute component={Home} />
<Route path='about' component={About} />
<Route path='features' component={Features} />
</Route>
</Router>,
document.getElementById('app')
)
复制代码
很简单,可是全部的路由结构都须要在渲染应用前,统必定义,层层嵌套;并且若是要实现异步按需加载还须要在这里对路由配置对象进行修改,使用getComponent
API,并侵入改造该组件,配合webpack的异步打包加载API,实现按需加载:
getComponent
,增长路由配置对象的复杂性;<Route>
只是一个声明路由的辅助标签,自己无心义;而使用react-router v4.x则以下:
// react-dom (what we'll use here)
import { BrowserRouter } from 'react-router-dom'
ReactDOM.render((
<BrowserRouter>
<App/>
</BrowserRouter>
), el)
const App = () => (
<div>
<nav>
<Link to="/about">Dashboard</Link>
</nav>
<Home />
<div>
<Route path="/about" component={About}/>
<Route path="/features" component={Features}/>
</div>
</div>
)
复制代码
相比以前版本,减小了配置化的痕迹,更凸显了组件化的组织方式,并且在渲染组件时才实现该部分路由,而若是指望按需加载该组件,则能够经过封装实现一个支持异步加载组件的高阶组件,将通过高阶组件处理后返回的组件传入<Route>
便可,依然遵循组件化形式:
component
,保证路由声明的简洁性;<Route>
做为一个真实组件建立路由,能够渲染;另外须要注意的是,相对于以前版本提供onEnter
, onUpdate
, onLeave
等钩子方法API在必定程度上提升了对路由的可控性,可是实质只是覆盖了渲染组件的生命周期方法,如今咱们能够经过路由渲染组件的生命周期方法直接控制路由,如使用componentDidMount
或 componentWillMount
代替 onEnter
。
同时使用React-Router和Redux时,大多数状况是正常的,可是也可能出现路由变动组件未更新的状况,如:
connect
方法将组件链接至redux:connect(Home)
;Route>
组件形式:<Route component={Home} />
声明渲染的;这是为何呢?,由于Redux会实现组件的shouldComponentUpdate
方法,当路由变化时,该组件并无接收到props代表发生了变动,须要更新组件。
那么如何解决问题呢?,要解决这个问题只须要简单的使用react-router-dom
提供的withRouter
方法包裹组件:
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Home))
复制代码
在使用Redux之后,须要遵循redux的原则:单一可信数据来源,即全部数据来源都只能是reudx store,react路由状态也不该例外,因此须要将路由state与store state链接。
链接React Router与Redux,须要使用react-router-redux
库,并且react-router v4版本须要指定安装@next
版本和hsitory
库:
yarn add react-router-redux@next
yarn add history
复制代码
而后,在建立store时,须要实现以下配置:
建立一个history对象,对于web应用,咱们选择browserHisotry,对应须要从history/createBrowserHistory
模块引入createHistory
方法以建立history对象;
添加routerReducer
和routerMiddleware
中间件“,其中routerMiddleware
中间件接收history对象参数,链接store和history,等同于旧版本的syncHistoryWithStore
;
import createHistory from 'history/createBrowserHistory'
import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux'
// Create a history of your choosing (we're using a browser history in this case)
export const history = createHistory()
// Build the middleware for intercepting and dispatching navigation actions
const middleware = routerMiddleware(history)
// Add the reducer to your store on the `router` key
// Also apply our middleware for navigating
const store = createStore(
combineReducers({
...reducers,
router: routerReducer
}),
applyMiddleware(middleware)
)
return store
复制代码
在渲染根组件时,咱们抽象出两个组件:
<Provider>
组件包裹,注入store;import createStore from './store/'
import Routes from './routes/'
import appReducer from './store/appRedux'
const store = createStore({}, {
app: appReducer
})
/** * 项目根组件 * @class App * @extends Component */
class App extends Component {
render () {
const { store } = this.props
return (
<Provider store={store}> <div> <Routes /> </div> </Provider>
)
}
}
// 渲染根组件
ReactDOM.render(
<App store={store} />, document.getElementById('app') ) 复制代码
上面的<Routes>
组件是项目的路由组件:
import { history } from '../store/'
import { ConnectedRouter } from 'react-router-redux'
import { Route } from 'react-router'
class Routes extends Component {
render () {
return (
<ConnectedRouter history={history}>
<div>
<BlogHeader />
<div>
<Route exact path='/' component={Home} />
<Route exact path='/posts/:id' component={Article} />
</div>
</div>
</ConnectedRouter>
)
}
}
复制代码
首先使用react-router-redux
提供的ConnectedRouter
组件包裹路由配置,该组件将自动使用<Provider>
组件注入的store
,咱们须要作的是手动传入history
属性,在组件内会调用history.listen
方法监听浏览器LOCATION_CHANGE
事件,最后返回react-router
的<Router >
组件,处理做为this.props.children
传入的路由配置,ConnectedRouter组件内容传送。
配置上面代码后,就可以以dispatch action的方式触发路由切换和组件更新了:
import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
复制代码
这个reducer所作的只是将App导航路由状态合并入store。
咱们知道浏览器默认有资源的缓存功能而且提供本地持久化存储方式如localStorage,indexDb,webSQL等,一般能够将某些数据存储在本地,在必定周期内,当用户再次访问时,直接从本地恢复数据,能够极大提升应用启动速度,用户体验更有优点,咱们可使用localStorage存储一些数据,若是是较大量数据存储可使用webSQL。
另外不一样于以往的直接存储数据,启动应用时本地读取而后恢复数据,对于redux应用而言,若是只是存储数据,那么咱们就得为每个reducer拓展,当再次启动应用时去读取持久化的数据,这是比较繁琐并且低效的方式,是否能够尝试存储reducer key,而后根据key恢复对应的持久化数据,首先注册Rehydrate reducer,当触发action时根据其reducer key恢复数据,而后只须要在应用启动时分发action,这也很容易抽象成可配置的拓展服务,实际上三方库redux-persist已经为咱们作好了这一切。
要实现redux的持久化,包括redux store的本地持久化存储及恢复启动两个过程,若是彻底本身编写实现,代码量比较复杂,可使用开源库redux-persist
,它提供persistStore
和autoRehydrate
方法分别持久化本地存储store及恢复启动store,另外还支持自定义传入持久化及恢复store时对store state的转换拓展。
yarn add redux-persist
复制代码
以下在建立store时会调用persistStore相关服务-RehydrationServices.updateReducers()
:
// configure persistStore and check reducer version number
if (ReduxPersistConfig.active) {
RehydrationServices.updateReducers(store);
}
复制代码
该方法内实现了store的持久化存储:
// Check to ensure latest reducer version
storage.getItem('reducerVersion').then((localVersion) => {
if (localVersion !== reducerVersion) {
// 清空 store
persistStore(store, null, startApp).purge();
storage.setItem('reducerVersion', reducerVersion);
} else {
persistStore(store, null, startApp);
}
}).catch(() => {
persistStore(store, null, startApp);
storage.setItem('reducerVersion', reducerVersion);
})
复制代码
会在localStorage存储一个reducer版本号,这个是在应用配置文件中能够配置,首次执行持久化时存储该版本号及store,若reducer版本号变动则清空原来存储的store,不然传入store给持久化方法persistStore
便可。
persistStore(store, [config], [callback])
复制代码
该方法主要实现store的持久化以及分发rehydration action :
接收参数主要以下:
和persisStore同样,依然是在建立redux store时初始化注册rehydrate拓展:
// add the autoRehydrate enhancer
if (ReduxPersist.active) {
enhancers.push(autoRehydrate());
}
复制代码
该方法实现的功能很简单,即便用 持久化的数据恢复(rehydrate) store 中数据,它实际上是注册了一个autoRehydarte reducer,会接收前文persistStore方法分发的rehydrate action,而后合并state。
固然,autoRehydrate不是必须的,咱们能够自定义恢复store方式:
import {REHYDRATE} from 'redux-persist/constants';
//...
case REHYDRATE:
const incoming = action.payload.reducer
if (incoming) {
return {
...state,
...incoming
}
}
return state;
复制代码
须要注意的是redux-persist库已经发布到v5.x,而本文介绍的以v5.x为例,v4.x参考此处,新版本有一些更新,能够选择性决定使用哪一个版本,详细请点击查看。
前面已经提到Redux与Immutable的整合,上文使用的redux -persist默认也只能处理原生JavaScript对象的redux store state,因此须要拓展以兼容Immutable。
使用redux-persist-immutable库能够很容易实现兼容,所作的仅仅是使用其提供的persistStore
方法替换redux-persist所提供的方法:
import { persistStore } from 'redux-persist-immutable';
复制代码
咱们知道持久化store时,针对的最好是原生JavaScript对象,由于一般Immutable结构数据有不少辅助信息,不易于存储,因此须要定义持久化及恢复数据时的转换操做:
import R from 'ramda';
import Immutable, { Iterable } from 'immutable';
// change this Immutable object into a JS object
const convertToJs = (state) => state.toJS();
// optionally convert this object into a JS object if it is Immutable
const fromImmutable = R.when(Iterable.isIterable, convertToJs);
// convert this JS object into an Immutable object
const toImmutable = (raw) => Immutable.fromJS(raw);
// the transform interface that redux-persist is expecting
export default {
out: (state) => {
return toImmutable(state);
},
in: (raw) => {
return fromImmutable(raw);
}
};
复制代码
如上,输出对象中的in和out分别对应持久化及恢复数据时的转换操做,实现的只是使用fromJS()
和toJS()
转换Js和Immutable数据结构,使用方式以下:
import immutablePersistenceTransform from '../services/ImmutablePersistenceTransform'
persistStore(store, {
transforms: [immutablePersistenceTransform]
}, startApp);
复制代码
在项目中引入Immutable之后,须要尽可能保证如下几点:
关于Immutable及Redux,Reselect等的实践考验查看以前写的一篇文章:Immutable.js与React,Redux及reselect的实践。
前面两点已经在前面两节阐述过,第三点react-router兼容Immutable,其实就是使应用路由状态兼容Immutable,在React路由一节已经介绍如何将React路由状态链接至Redux store,可是若是应用使用了Immutable库,则还须要额外处理,将react-router state转换为Immutable格式,routeReducer不能处理Immutable,咱们须要自定义一个新的RouterReducer:
import Immutable from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = Immutable.fromJS({
location: null
});
export default (state = initialState, action) => {
if (action.type === LOCATION_CHANGE) {
return state.set('location', action.payload);
}
return state;
};
复制代码
将默认初始路由状态转换为Immutable,而且路由变动时使用Immutable API操做state。
当引入Immutable.js后,对应用状态数据结构的使用API就得遵循Immutable API,而不能再使用原生JavaScript对象,数组等的操做API了,诸如,数组解构([a, b] = [b, c]),对象拓展符(...)等,存在一些问题:
针对这些问题,社区有了seamless-immutable
可供替换选择:
seamless-immutable
库更轻小;最后要介绍的模块是异步任务管理,在应用开发过程当中,最主要的异步任务就是数据HTTP请求,因此咱们讲异步任务管理,主要关注在数据HTTP请求的流程管理。
本项目中使用axios做为HTTP请求库,axios是一个Promise格式的HTTP客户端,选择此库的缘由主要有如下几点:
redux-saga是一个致力于使应用中如数据获取,本地缓存访问等异步任务易于管理,高效运行,便于测试,能更好的处理异常的三方库。
Redux-saga是一个redux中间件,它就像应用中一个单独的进程,只负责管理异步任务,它能够接受应用主进程的redux action以决定启动,暂停或者是取消进程任务,它也能够访问redux应用store state,而后分发action。
redux-saga是一个中间件,因此首先调用createSagaMiddleware
方法建立中间件,而后使用redux的applyMiddleware
方法启用中间件,以后使用compose辅助方法传给createStore
建立store,最后调用run
方法启动根saga:
import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas/'
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
enhancers.push(applyMiddleware(...middleware));
const store = createStore(rootReducer, initialState, compose(...enhancers));
// kick off root saga
sagaMiddleware.run(rootSaga);
复制代码
在项目中一般会有不少并列模块,每一个模块的saga流也应该是并列的,须要以多分支形式并列,redux-saga提供的fork
方法就是以新开分支的形式启动当前saga流:
import { fork, takeEvery } from 'redux-saga/effects'
import { HomeSaga } from './Home/flux.js'
import { AppSaga } from './Appflux.js'
const sagas = [
...AppSaga,
...HomeSaga
]
export default function * root() {
yield sagas.map(saga => fork(saga))
}
复制代码
如上,首先收集全部模块根saga,而后遍历数组,启动每个saga流根saga。
以AppSaga为例,咱们指望在应用启动时就发起一些异步请求,如获取文章列表数据将其填充至redux store,而不等待使用数据的组件渲染完才开始请求数据,提升响应速度:
const REQUEST_POST_LIST = 'REQUEST_POST_LIST'
const RECEIVE_POST_LIST = 'RECEIVE_POST_LIST'
/**
* 请求文章列表ActionCreator
* @param {object} payload
*/
function requestPostList (payload) {
return {
type: REQUEST_POST_LIST,
payload: payload
}
}
/**
* 接收文章列表ActionCreator
* @param {*} payload
*/
function receivePostList (payload) {
return {
type: RECEIVE_POST_LIST,
payload: payload
}
}
/**
* 处理请求文章列表Saga
* @param {*} payload 请求参数负载
*/
function * getPostListSaga ({ payload }) {
const data = yield call(getPostList)
yield put(receivePostList(data))
}
// 定义AppSaga
export function * AppSaga (action) {
// 接收最近一次请求,而后调用getPostListSaga子Saga
yield takeLatest(REQUEST_POST_LIST, getPostListSaga)
}
复制代码
takeLatest
:在AppSaga
内使用takeLatest
方法监听REQUEST_POST_LIST
action,若短期内连续发起屡次action,则会取消前面未响应的action,只发起最后一次action;getPostListSaga
子Saga:当接收到该action时,调用getPostListSaga
,并将payload传递给它,getPostListSaga
是AppSaga的子级Saga,在里面处理具体异步任务;getPostList
:getPostListSaga
会调用getPostList
方法,发起异步请求,拿到响应数据后,调用receivePostList
ActionCreator,建立并分发action,而后由reducer处理相应逻辑;getPostList
方法内容以下:
/**
* 请求文章列表方法
* @param {*} payload 请求参数
* eg: {
* page: Num,
* per_page: Num
* }
*/
function getPostList (payload) {
return fetch({
...API.getPostList,
data: payload
}).then(res => {
if (res) {
let data = formatPostListData(res.data)
return {
total: parseInt(res.headers['X-WP-Total'.toLowerCase()], 10),
totalPages: parseInt(res.headers['X-WP-TotalPages'.toLowerCase()], 10),
...data
}
}
})
}
复制代码
put
是redux-saga提供的可分发action方法,take,call等都是redux-saga
提供的API,更多内容查看API文档。
以后即可以在项目路由根组件注入ActionCreator,建立action,而后saga就会接收进行处理了。
前面已经配置好可使用Reactotron捕获应用全部redux和action,而redux-saga是一类redux中间件,因此捕获sagas须要额外配置,建立store时,在saga中间件内添加sagaMonitor服务,监听saga:
const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
...
复制代码
本文较详细的总结了我的从0到1搭建一个项目架构的过程,对React, Redux应用和项目工程实践都有了更深的理解及思考,在大前端成长之路继续砥砺前行。
注:文中列出的全部技术栈,博主计划一步一步推动,目前源码中使用的技术有React,React Router,Redux,react-redux,react-router-redux,Redux-saga,axios。后期计划推动Immutable,Reactotron,Redux Persist。