React应用架构设计指南

在上一篇咱们介绍了Webpack自动化构建React应用,咱们的本地开发服务器能够较好的支持咱们编写React应用,而且支持代码热更新。本节将开始详细分析如何搭建一个React应用架构。javascript

完整项目代码见githubcss

我的博客html

前言

如今已经有不少脚手架工具,如create-react-app,支持一键建立一个React应用项目结构,很方便,可是享受方便的同时,也失去了对项目架构及技术栈完整学习的机会,并且一般脚手架建立的应用技术架构并不能彻底知足咱们的业务需求,须要咱们本身修改,完善,因此若是但愿对项目架构有更深掌控,最好仍是从0到1理解一个项目。前端

项目结构与技术栈

咱们此次的实践不许备使用任何脚手架,因此咱们须要本身建立每个文件,引入每个技术和三方库,最终造成完整的应用,包括咱们选择的完整技术栈。java

第一步,固然是建立目录,咱们在上一篇已经弄好,若是你尚未代码,能够从Github获取:node

git clone https://github.com/codingplayboy/react-blog.git
cd react-blog
复制代码

生成项目结构以下图:react

React项目初始结构

  1. src为应用源代码目录;
  2. webpack为webpack配置目录;
  3. webpack.config.js为webpack配置入口文件;
  4. package.json为项目依赖管理文件;
  5. yarn.lock为项目依赖版本锁文件;
  6. .babelrc文件,babel的配置文件,使用babel编译React和JavaScript代码;
  7. eslintrceslintignore分别为eslint语法检测配置及须要忽略检查的内容或文件;
  8. postcss.config.js为CSS后编译器postcss的配置文件;
  9. API.md为API文档入口;
  10. docs为文档目录;
  11. README.md为项目说明文档;

接下来的工做主要就是丰富src目录,包括搭建项目架构,开发应用功能,还有自动化,单元测试等,本篇主要关注项目架构的搭建,而后使用技术栈实践开发几个模块。webpack

技术栈

项目架构搭建很大部分依赖于项目的技术栈,因此先对整个技术栈进行分析,总结:ios

  1. react和react-dom库是项目前提;
  2. react路由;
  3. 应用状态管理容器;
  4. 是否须要Immutable数据;
  5. 应用状态的持久化;
  6. 异步任务管理;
  7. 测试及辅助工具或函数;
  8. 开发调试工具;

根据以上划分决定选用如下第三方库和工具构成项目的完整技术栈:git

  1. react,react-dom;
  2. react-router管理应用路由;
  3. redux做为JavaScript状态容器,react-redux将React应用与redux链接;
  4. Immutable.js支持Immutable化状态,redux-immutable使整个redux store状态树Immutable化;
  5. 使用redux-persist支持redux状态树的持久化,并添加redux-persist-immutable拓展以支持Immutable化状态树的持久化;
  6. 使用redux-saga管理应用内的异步任务,如网络请求,异步读取本地数据等;
  7. 使用jest集成应用测试,使用lodash,ramda等可选辅助类,工具类库;
  8. 可选使用reactotron调试工具

针对以上分析,完善后的项目结构如图:

React-Redux项目结构

开发调试工具

React应用开发目前已经有诸多调试工具,经常使用的如redux-devtools,Reactron等。

redux-devtools

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
  )
)
复制代码
  1. 在开发环境下获取redux-devtools提供的拓展组合函数;
  2. 建立store时使用拓展组合函数组合redux中间件和加强器,redux-dev-tools便得到了应用redux的相关信息;

Reactotron

Reactotron是一款跨平台调试React及React Native应用的桌面应用,能动态实时监测并输出React应用等redux,action,saga异步请求等信息,如图:

Reactotron

首先安装:

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,一般遵循如下组件设计思路:

  1. 布局组件:仅仅涉及应用UI界面结构的组件,不涉及任何业务逻辑,数据请求及操做;
  2. 容器组件:负责获取数据,处理业务逻辑,一般在render()函数内返回展现型组件;
  3. 展现型组件:负责应用的界面UI展现;
  4. UI组件:指抽象出的可重用的UI独立组件,一般是无状态组件;
展现型组件 容器组件
目标 UI展现 (HTML结构和样式) 业务逻辑(获取数据,更新状态)
感知Redux
数据来源 props 订阅Redux store
变动数据 调用props传递的回调函数 Dispatch Redux actions
可重用 独立性强 业务耦合度高

Redux

如今的任何大型web应用若是少了状态管理容器,那这个应用就缺乏了时代特征,可选的库诸如mobx,redux等,实际上大同小异,各取所需,以redux为例,redux是最经常使用的React应用状态容器库,对于React Native应用也适用。

Redux是一个JavaScript应用的可预测状态管理容器,它不依赖于具体框架或类库,因此它在多平台的应用开发中有着一致的开发方式和效率,另外它还能帮咱们轻松的实现时间旅行,即action的回放。

redux-flow

  1. 数据单一来源原则:使用Redux做为应用状态管理容器,统一管理应用的状态树,它推从数据单一可信来源原则,全部数据都来自redux store,全部的数据更新也都由redux处理;
  2. redux store状态树:redux集中管理应用状态,组织管理形式就比如DOM树和React组件树同样,以树的形式组织,简单高效;
  3. redux和store:redux是一种Flux的实现方案,因此建立了store一词,它相似于商店,集中管理应用状态,支持将每个发布的action分发至全部reducer;
  4. action:以对象数据格式存在,一般至少有type和payload属性,它是对redux中定义的任务的描述;
  5. reducer:一般是以函数形式存在,接收state(应用局部状态)和action对象两个参数,根据action.type(action类型)执行不一样的任务,遵循函数式编程思想;
  6. dispatch:store提供的分发action的功能方法,传递一个action对象参数;
  7. createStore:建立store的方法,接收reducer,初始应用状态,redux中间件和加强器,初始化store,开始监听action;

中间件(Redux Middleware)

Redux中间件,和Node中间件同样,它能够在action分发至任务处理reducer以前作一些额外工做,dispatch发布的action将依次传递给全部中间件,最终到达reducer,因此咱们使用中间件能够拓展诸如记录日志,添加监控,切换路由等功能,因此中间件本质上只是拓展了store.dispatch方法。

redux-middleware-enhancer

加强器(Store Enhancer)

有些时候咱们可能并不知足于拓展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方法

react-redux

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分析

createStore

使用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与Immutable

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路由

在React web单页面应用中,页面级UI组件的展现和切换彻底由路由控制,每个路由都有对应的URL及路由信息,咱们能够经过路由统一高效的管理咱们的组件切换,保持UI与URL同步,保证应用的稳定性及友好体验。

react-router

React Router是完整的React 路由解决方案,也是开发React应用最常使用的路由管理库,只要用过它,绝对会喜欢上它的设计,它提供简单的API,以声明式方式实现强大的路由功能,诸如按需加载,动态路由等。

  1. 声明式:语法简洁,清晰;
  2. 按需加载:延迟加载,根据使用须要判断是否须要加载;
  3. 动态路由:动态组合应用路由结构,更灵活,更符合组件化开发模式;

动态路由与静态路由

使用react-router v4版本能够定义跨平台的应用动态路由结构,所谓的动态路由(Dynamic Routing)即在渲染过程当中发生路由的切换,而不须要在建立应用前就配置好,这也正是其区别于静态路由(Static Routing)所在,动态路由提升更灵活的路由组织方式,并且更方便编码实现路由按需加载组件。

在react-router v2和v3版本中,开发React应用须要在开始渲染前就定义好完整的应用路由结构,全部的路由都须要同时初始化,才能在应用渲染后生效,会产生不少嵌套化路由,丧失了动态路由的灵活性和简洁的按需加载编码方式。

react-router v4.x

在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')
)
复制代码

很简单,可是全部的路由结构都须要在渲染应用前,统必定义,层层嵌套;并且若是要实现异步按需加载还须要在这里对路由配置对象进行修改,使用getComponentAPI,并侵入改造该组件,配合webpack的异步打包加载API,实现按需加载:

  1. 路由层层嵌套,必须在渲染应用前统一声明;
  2. API不一样,须要使用getComponent,增长路由配置对象的复杂性;
  3. <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>便可,依然遵循组件化形式:

  1. 灵活性:路由能够在渲染组件中声明,不需依赖于其余路由,不须要集中配置;
  2. 简洁:统一传入component,保证路由声明的简洁性;
  3. 组件化:<Route>做为一个真实组件建立路由,能够渲染;

路由钩子方法

另外须要注意的是,相对于以前版本提供onEnter, onUpdate, onLeave等钩子方法API在必定程度上提升了对路由的可控性,可是实质只是覆盖了渲染组件的生命周期方法,如今咱们能够经过路由渲染组件的生命周期方法直接控制路由,如使用componentDidMountcomponentWillMount 代替 onEnter

路由与Redux

同时使用React-Router和Redux时,大多数状况是正常的,可是也可能出现路由变动组件未更新的状况,如:

  1. 咱们使用redux的connect方法将组件链接至redux:connect(Home);
  2. 组件不是一个路由渲染组件,即不是使用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之后,须要遵循redux的原则:单一可信数据来源,即全部数据来源都只能是reudx store,react路由状态也不该例外,因此须要将路由state与store state链接。

react-router-redux

链接React Router与Redux,须要使用react-router-redux库,并且react-router v4版本须要指定安装@next版本和hsitory库:

yarn add react-router-redux@next
yarn add history
复制代码

而后,在建立store时,须要实现以下配置:

  1. 建立一个history对象,对于web应用,咱们选择browserHisotry,对应须要从history/createBrowserHistory模块引入createHistory方法以建立history对象;

    点此查看更多history相关内容

  2. 添加routerReducerrouterMiddleware中间件“,其中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
复制代码

在渲染根组件时,咱们抽象出两个组件:

  1. 初始化渲染根组件,挂载至DOM的根组件,由<Provider>组件包裹,注入store;
  2. 路由配置组件,在根组件中,声明路由配置组件,初始化必要的应用路由定义及路由对象;
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切换路由

配置上面代码后,就可以以dispatch action的方式触发路由切换和组件更新了:

import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
复制代码

这个reducer所作的只是将App导航路由状态合并入store。

redux持久化

咱们知道浏览器默认有资源的缓存功能而且提供本地持久化存储方式如localStorage,indexDb,webSQL等,一般能够将某些数据存储在本地,在必定周期内,当用户再次访问时,直接从本地恢复数据,能够极大提升应用启动速度,用户体验更有优点,咱们可使用localStorage存储一些数据,若是是较大量数据存储可使用webSQL。

另外不一样于以往的直接存储数据,启动应用时本地读取而后恢复数据,对于redux应用而言,若是只是存储数据,那么咱们就得为每个reducer拓展,当再次启动应用时去读取持久化的数据,这是比较繁琐并且低效的方式,是否能够尝试存储reducer key,而后根据key恢复对应的持久化数据,首先注册Rehydrate reducer,当触发action时根据其reducer key恢复数据,而后只须要在应用启动时分发action,这也很容易抽象成可配置的拓展服务,实际上三方库redux-persist已经为咱们作好了这一切。

redux-persist

要实现redux的持久化,包括redux store的本地持久化存储及恢复启动两个过程,若是彻底本身编写实现,代码量比较复杂,可使用开源库redux-persist,它提供persistStoreautoRehydrate方法分别持久化本地存储store及恢复启动store,另外还支持自定义传入持久化及恢复store时对store state的转换拓展。

yarn add redux-persist
复制代码

持久化store

以下在建立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 :

  1. 订阅 redux store,当其发生变化时触发store存储操做;
  2. 从指定的StorageEngine(如localStorage)中获取数据,进行转换,而后经过分发 REHYDRATE action,触发 REHYDRATE 过程;

接收参数主要以下:

  1. store: 持久化的store;
  2. config:配置对象
    1. storage:一个 持久化引擎,例如 LocalStorage 和 AsyncStorage;
    2. transforms: 在 rehydration 和 storage 阶段被调用的转换器;
    3. blacklist: 黑名单数组,指定持久化忽略的 reducers 的 key;
  3. callback:ehydration 操做结束后的回调;

恢复启动

和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参考此处,新版本有一些更新,能够选择性决定使用哪一个版本,详细请点击查看

持久化与Immutable

前面已经提到Redux与Immutable的整合,上文使用的redux -persist默认也只能处理原生JavaScript对象的redux store state,因此须要拓展以兼容Immutable。

redux-persist-immutable

使用redux-persist-immutable库能够很容易实现兼容,所作的仅仅是使用其提供的persistStore方法替换redux-persist所提供的方法:

import { persistStore } from 'redux-persist-immutable';
复制代码

transform

咱们知道持久化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之后,须要尽可能保证如下几点:

  1. redux store整个state树的统一Immutable化;
  2. redux持久化对Immutable数据的兼容;
  3. React路由兼容Immutable;

关于Immutable及Redux,Reselect等的实践考验查看以前写的一篇文章:Immutable.js与React,Redux及reselect的实践

Immutable与React路由

前面两点已经在前面两节阐述过,第三点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。

seamless-Immutable

当引入Immutable.js后,对应用状态数据结构的使用API就得遵循Immutable API,而不能再使用原生JavaScript对象,数组等的操做API了,诸如,数组解构([a, b] = [b, c]),对象拓展符(...)等,存在一些问题:

  1. Immutable数据辅助节点较多,数据较大:
  2. 必须使用Immutable语法,和JavaScript语法有差别,不能很好的兼容;
  3. 和Redux,react-router等JavaScript库写协做时,须要引入额外的兼容处理库;

针对这些问题,社区有了seamless-immutable可供替换选择:

  1. 更轻:相对于Immutable.jsseamless-immutable库更轻小;
  2. 语法:对象和数组的操做语法更贴近原生JavaScript;
  3. 和其余JavaScript库协做更方便;

异步任务流管理

最后要介绍的模块是异步任务管理,在应用开发过程当中,最主要的异步任务就是数据HTTP请求,因此咱们讲异步任务管理,主要关注在数据HTTP请求的流程管理。

axios

本项目中使用axios做为HTTP请求库,axios是一个Promise格式的HTTP客户端,选择此库的缘由主要有如下几点:

  1. 能在浏览器发起XMLHttpRequest,也能在node.js端发起HTTP请求;
  2. 支持Promise;
  3. 能拦截请求和响应;
  4. 能取消请求;
  5. 自动转换JSON数据;

redux-saga

redux-saga是一个致力于使应用中如数据获取,本地缓存访问等异步任务易于管理,高效运行,便于测试,能更好的处理异常的三方库。

Redux-saga是一个redux中间件,它就像应用中一个单独的进程,只负责管理异步任务,它能够接受应用主进程的redux action以决定启动,暂停或者是取消进程任务,它也能够访问redux应用store state,而后分发action。

初始化saga

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分流

在项目中一般会有不少并列模块,每一个模块的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。

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)
}
复制代码
  1. takeLatest:在AppSaga内使用takeLatest方法监听REQUEST_POST_LISTaction,若短期内连续发起屡次action,则会取消前面未响应的action,只发起最后一次action;
  2. getPostListSaga子Saga:当接收到该action时,调用getPostListSaga,并将payload传递给它,getPostListSaga是AppSaga的子级Saga,在里面处理具体异步任务;
  3. getPostListgetPostListSaga会调用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就会接收进行处理了。

saga与Reactotron

前面已经配置好可使用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。

完整项目代码见github

参考

  1. React
  2. Redux
  3. React Router v4
  4. redux-saga
  5. Redux Persist
相关文章
相关标签/搜索