从零开始搭建React应用(二)——React应用架构

上一篇文章——从零开始搭建 React 应用(一)——基础搭建讲述了如何使用 webpack 搭建一个很是基础的 react 开发环境。本文将详细讲述搭建一个 React 应用的架构。css

仓库地址:github.com/MrZhang123/…html

redux

在咱们开发过程当中,不少时候,咱们须要让组件共享某些数据,虽然能够经过组件传递数据实现数据共享,可是若是组件之间不是父子关系的话,数据传递是很是麻烦的,并且容易让代码的可读性下降,这时候咱们就须要一个 state(状态)管理工具。常见的状态管理工具备 redux,mobx,这里选择 redux 进行状态管理。值得注意的是 React 16.3 带来了全新的Context API,咱们也可使用新的 Context API 作状态管理。vue

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可让你构建一致化的应用,运行于不一样的环境(客户端、服务器、原生应用),而且易于测试。不只于此,它还提供很是好的开发体验,好比有一个时间旅行调试器能够编辑后实时预览。react

redux 的数据流以下图所示:webpack

redux 的三大原则:ios

  1. 整个应用的state都被存储在一棵 object tree 中,而且这个 object tree 只存在于惟一的 store 中,可是这并不意味使用 redux 就须要将全部的 state 存到 redux 上。
  2. state 是只读的,惟一改变 state 的方式是触发actionaction是一个用于描述已发生事件的普通对象。
  3. 使用纯函数来执行修改,为了描述 action 如何改变 state tree,须要编写 reducers。

中间件(Redux middleware)

Redux middleware 提供位于 action 发起以后,到达 reducer 以前的扩展点。dispatch 发起的 action 依次通过中间件,最终到达 reducer。咱们能够利用 Redux middleware 来进行日志记录、建立崩溃报告、调用异步接口或者路由等等。本质上来说中间件只是拓展了 store.dispatch 方法git

加强器(Store enhancer)

store enhancer 用于加强 store 的功能,一个 store enhancer 实际上就是一个高阶函数,返回一个新的强化过的 store creator。github

const logEnhancer = createStore => (reducer, initialState, enhancer) => {
  const store = createStore(reducer, initialState, enhancer)
  function dispatch(action) {
    console.log(`dispatch an action: ${JSON.stringify(action)}`)
    const res = store.dispatch(action)
    const newState = store.getState()
    console.log(`current state: ${JSON.stringify(newState)}`)
    return res
  }
  return { ...store, dispatch }
}
复制代码

能够看到logEnhancer改变了 store 的默认行为,在每次dispatch先后,都会输出日志。web

react-redux

redux 自己是一个状态 JS 的状态库,能够结合 react,vue,angular 甚至是原生 JS 应用使用,为了让 redux 帮咱们管理 react 应用的状态,须要把 redux 与 react 链接,官方提供了react-redux库。chrome

react-redux 提供Provider组件经过 context 的方式向应用注入 store,而后组件使用connect高阶方法获取并监听 store,而后根据 store state 和组件自身的 props 计算获得新的 props,注入该组件,而且能够经过监听 store,比较计算出的新 props 判断是否须要更新组件。

render(
  <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>,
  document.getElementById('app')
)
复制代码

整合 redux 到 react 应用

合并 reducer

在一个 react 应用中只有一个 store,组件经过调用 action 函数,传递数据到 reducer,reducer 根据数据更改对应的 state。可是随着应用复杂度的提高,reducer 也会变得愈来愈大,此时能够考虑将 reducer 拆分红多个单独的函数,拆分后的每一个函数负责独立管理 state 的一部分。

redux 提供combineReducers辅助函数,将分散的 reducer 合并成一个最终的 reducer 函数,而后在 createStore 的时候使用。

整合 middleware

有时候咱们须要多个 middleware 组合在一块儿造成 middleware 链来加强store.dispatch,在建立 store 时候,咱们须要将 middleware 链整合到 store 中,官方提供applyMiddleware(...middleware)将 middleware 链在一块儿。

整合 store enhancer

store enhancer 用于加强 store,若是咱们有多个 store enhancer 时须要将多个 store enhancer 整合,这时候就会用到compose(...functions)

使用compose合并多个函数,每一个函数都接受一个参数,它的返回值将做为一个参数提供给它左边的函数以此类推,最右边的函数能够接受多个参数。compose(funA,funB,funC)能够理解为compose(funA(funB(funC()))),最终返回从右到左接收到的函数合并后的最终函数。

建立 Store

redux 经过createStore建立一个 Redux store 来以存放应用中全部的 statecreateStore的参数形式以下:

createStore(reducer, [preloadedState], enhancer)
复制代码

因此咱们建立 store 的代码以下:

import thunk from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'

import reducers from '../reducers'

const initialState = {}

const store = createStore(reducers, initialState, applyMiddleware(thunk))

export default store
复制代码

以后将建立的 store 经过Provider组件注入 react 应用便可将 redux 与 react 应用整合在一块儿。

注:应用中应有且仅有一个 store

React Router

React Router 是完整的 React 的路由解决方案,它保持 UI 与 URL 的同步。项目中咱们整合最新版的 React Router v4。

在 react-router v4 中 react-router 被划分为三个包:react-router,react-router-dom 和 react-router-native,区别以下:

  • react-router:提供核心路由组件和函数
  • react-router-dom:供浏览器使用的 react router
  • react-router-native:供 react native 使用的 react router

redux 与 react router

React Router 与 Redux 一块儿使用时大部分状况下都是正常的,可是偶尔会出现路由更新可是子路由或活动导航连接没有更新。这个状况发生在:

  1. 组件经过connect()(Comp)链接 redux。
  2. 组件不是一个“路由组件”,即组件并无像<Route component={SomeConnectedThing} />这样渲染。

这个问题的缘由是 Redux 实现了shouldComponentUpdate,当路由变化时,该组件并无接收到 props 更新。

解决这个问题的方法很简单,找到connect而且将它用withRouter包裹:

// before
export default connect(mapStateToProps)(Something)

// after
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Something))
复制代码

将 redux 与 react-router 深度整合

有时候咱们可能但愿将 redux 与 react router 进行更深度的整合,实现:

  • 将 router 的数据与 store 同步,而且从 store 访问
  • 经过 dispatch actions 导航
  • 在 redux devtools 中支持路由改变的时间旅行调试

这些能够经过 connected-react-router 和 history 两个库将 react-router 与 redux 进行深度整合实现。

官方文档中提到的是 react-router-redux,而且它已经被整合到了 react-router v4 中,可是根据 react-router-redux 的文档,该仓库再也不维护,推荐使用 connected-react-router。

首先安装 connected-react-router 和 history 两个库:

$ npm install --save connected-react-router
$ npm install --save history
复制代码

而后给 store 添加以下配置:

  • 建立history对象,由于咱们的应用是浏览器端,因此使用createBrowserHistory建立
  • 使用connectRouter包裹 root reducer 而且提供咱们建立的history对象,得到新的 root reducer
  • 使用routerMiddleware(history)实现使用 dispatch history actions,这样就可使用push('/path/to/somewhere')去改变路由(这里的 push 是来自 connected-react-router 的)
import thunk from 'redux-thunk'
import { createBrowserHistory } from 'history'

import { createStore, applyMiddleware } from 'redux'
import { connectRouter, routerMiddleware } from 'connected-react-router'

import reducers from '../reducers'

export const history = createBrowserHistory()
const initialState = {}

const store = createStore(
  connectRouter(history)(reducers),
  initialState,
  applyMiddleware(thunk, routerMiddleware(history))
)

export default store
复制代码

在根组件中,咱们添加以下配置:

  • 使用ConnectedRouter包裹路由,而且将 store 中建立的history对象引入,做为 props 传入应用
  • ConnectedRouter组件要做为Provider的子组件
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'

import App from './App'
import store from './redux/store'
import { history } from './redux/store'

render(
  <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>,
  document.getElementById('app')
)
复制代码

这样咱们就将 redux 与 react-router 整合完毕。

使用dispatch切换路由

完成以上配置后,就可使用dispatch切换路由了:

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

react-router-config

react-router v4 以前——静态路由

在 react-router v4 以前的版本中,咱们能够直接使用静态路由来配置应用程序的路由,它容许在渲染以前对路由进行检查和匹配。

在 router.js 中通常会有这样的代码:

const routes = (
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User} />
      </Route>
      <Route path="*" component={NoMatch} />
    </Route>
  </Router>
)
export default routes
复制代码

而后在初始化的时候把路由导入,而后渲染:

import ReactDOM from 'react-dom'
import routes from './config/routes'

ReactDOM.render(routes, document.getElementById('app'))
复制代码

react-router v4——动态路由

从 v4 版本开始,react-router 使用动态组件代替路径配置,即 react-router 就是 react 应用的一个普通组件,随用随写,没必要像以前那样,路由跟组件分离。所以 react 应用添加 react-router,首先引入咱们须要的东西。

import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
复制代码

这里咱们将BrowserRouter引入并从新命名为RouterBrowserRouter容许 react-router 将应用的路由信息经过context传递给任何须要的组件。所以要让 react-router 正常工做,须要在应用程序的根结点中渲染BrowserRouter

import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <Router> <div> <div> <Link to="/">Home</Link> </div> <hr /> <Route exact path="/" component={Home} /> </div> </Router> ) } } 复制代码

以还使用了Route,当应用程序的 location 匹配到某个路由时,Route将渲染制定的 component,不然渲染null

想要加入更多的路由,添加Route组件便可,可是这样的写法也许咱们会感受到有点儿乱,由于毕竟路由被分散到组件各处,很难像之前那样很容易的看到整个应用的路由,并且若是项目以前是用的 react-router v4 以前的版本,那么升级 v4 也是成本很大的,官方为解决该问题,提供了专门用来处理静态路由配置的库——react-router-config。

添加 react-router-config 实现使用静态路由

添加了 react-router-config 以后,咱们就能够写咱们熟悉的静态路由了。同时,利用它,能够将路由配置分散在各个组件中,最后使用renderRoutes将分散的路由片断在根组件合并,渲染便可。

配置静态路由:

import Home from './views/Home'
import About from './views/About'

const routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]
export default routes
复制代码

而后在根组件中合并,渲染:

import { renderRoutes } from 'react-router-config'

import HomeRoute from './views/Home/router'
import AboutRoute from './views/About/router'
// 合并路由
const routes = [...HomeRoute, ...AboutRoute]

class App extends Component {
  render() {
    return (
      <Router> <div className="screen">{renderRoutes(routes)}</div> </Router>
    )
  }
}
复制代码

renderRoutes其实帮咱们作了相似的事儿:

const routes = (
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User} />
      </Route>
      <Route path="*" component={NoMatch} />
    </Route>
  </Router>
)
复制代码

这样就给 React 应用添加了静态路由。

添加模块热替换(Hot Module Replacement)

模块热替换(HMR)功能会在应用程序运行过程当中替换、添加或删除模块,而无需从新加载整个页面。主要经过如下几种方式:

  • 保留在彻底从新加载页面时丢失的应用状态
  • 只更新变动的内容以节省开发时间
  • 更改样式不须要刷新页面

在开发模式中,HMR 能够替代 LiveReload,webpack-dev-server 支持hot模式,在试图从新加载整个页面以前,hot模式尝试使用 HMR 来更新。

启用 HMR

在 webpack 配置文件中添加 HMR 插件:

plugins: [new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin()]
复制代码

这里添加的NamedModulesPlugin插件,

设置 webpack-dev-server 开启hot模式:

const server = new WebpackDevServer(compiler, {
+  hot: true,
  // noInfo: true,
  quiet: true,
  historyApiFallback: true,
  filename: config.output.filename,
  publicPath: config.output.publicPath,
  stats: {
    colors: true
  }
});
复制代码

这样,当修改 react 代码的时候,页面会自动刷新,修改 css 文件,页面不刷新,直接呈现样式。

可是会发现一个问题,页面的自动刷新会致使咱们 react 组件的状态丢失,那么可否作到更改 react 组件像更改 css 文件那样,页面不刷新(保存页面的状态),直接替换呢?答案是确定的,可使用 react-hot-loader。

添加 react-hot-loader

添加 react-hot-loader 很是简单,只须要在根组件导出的时候添加高阶方法hot便可:

import { hot } from "react-hot-loader";

class App extends Component {
	...
}

export default hot(module)(App);
复制代码

这样,整个应用在开发时候就能够修改 react 组件而保持状态了。

注:

在开发过程当中,查阅了一些文章说,为了配合 redux,须要在 store.js 中添加以下代码:

if (process.env.NODE_ENV === 'development') {
  if (module.hot) {
    module.hot.accept('../reducers/index.js', () => {
      // const nextReducer = combineReducers(require('../reducers'))
      // store.replaceReducer(nextReducer)
      store.replaceReducer(require('../reducers/index.js').default)
    })
  }
}
复制代码

可是,在 react-hot-loader v4 中,是不须要的,直接添加hot就能够了。

异步加载组件(Code Splitting)

完成以上配置后,咱们的主体已经搭建的差很少了,可是当打开开发者工具会发现,应用开始加载的时候直接把整个应用的 JS 所有加载进来,可是咱们指望进入哪一个页面加载哪一个页面的代码,那么如何实现应用的 Code Splitting 呢?

其实实现 React Code Splitting 的库有不少,例如:

选用其中之一便可,我项目中选用的是 react-loadable。

以前咱们已经在项目中配置了静态路由,组件是直接引入的,咱们只须要对以前的直接引入的组件作处理就能够,代码以下:

import loadable from 'react-loadable'
import Loading from '../../components/Loading'

export const Home = loadable({
  loader: () => import('./Home'),
  loading: Loading
})
export const About = loadable({
  loader: () => import('./About'),
  loading: Loading
})

const routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]
export default routes
复制代码

异步任务流管理

实现异步操做的思路

大部分状况下咱们的应用中都是同步操做,即 dispatch action 时,state 会被当即更新,可是有些时候咱们须要作异步操做。同步操做只要发出一种 Action 便可,可是异步操做须要发出三种 Acion。

  • 操做发起时的 Action
  • 操做成功时的 Action
  • 操做失败时的 Action

为了区分这三种 action,可能在 action 里添加一个专门的status字段做为标记位:

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
复制代码

或者为它们定义不一样的 type:

{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
复制代码

因此想要实现异步操做须要作到:

  • 操做开始时,发出一个 Action,触发 State 更新为“正在操做”,View 从新渲染
  • 操做结束后,再发出一个 Action,触发 State 更新为“操做结束”,View 再次从新渲染

redux-thunk

异步操做至少送出两个 Action,第一个 Action 跟同步操做同样,直接送出便可,那么如何送出第二个 Action 呢?

咱们能够在送出第一个 Action 的时候送一个 Action Creator 函数,这样第二个 Action 能够在异步执行完成后自动送出。

componentDidMount() {
   store.dispatch(fetchPosts())
}
复制代码

在组件加载成功后,送出一个 Action 用来请求数据,这里的fetchPosts就是 Action Creator。fetchPosts 代码以下:

export const SET_DEMO_DATA = createActionSet('SET_DEMO_DATA')

export const fetchPosts = () => async (dispatch, getState) => {
  store.dispatch({ type: SET_DEMO_DATA.PENDING })
  await axios
    .get('https://jsonplaceholder.typicode.com/users')
    .then(response => store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response }))
    .catch(err => store.dispatch({ type: SET_DEMO_DATA.ERROR, payload: err }))
}
复制代码

fetchPosts是一个 Action Creator,执行返回一个函数,该函数执行时dispatch一个 action,代表立刻要进行异步操做;异步执行完成后,根据请求结果的不一样,分别dispatch不一样的 action 将异步操做的结果返回回来。

这里须要说明几点:

  1. fetchPosts返回了一个函数,而普通的 Action Creator 默认返回一个对象。
  2. 返回的函数的参数是dispatchgetState这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。
  3. 在返回的函数之中,先发出一个 store.dispatch({type: SET_DEMO_DATA.PENDING}),表示异步操做开始。
  4. 异步操做结束以后,再发出一个 store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response }),表示操做结束。

可是有一个问题,store.dispatch正常状况下,只能发送对象,而咱们要发送函数,为了让store.dispatch能够发送函数,咱们使用中间件——redux-thunk。

引入 redux-thunk 很简单,只须要在建立 store 的时候使用applyMiddleware(thunk)引入便可。

开发调试工具

开发过程当中免不了调试,经常使用的调试工具备不少,例如 redux-devtools-extension,redux-devtools,storybook 等。

redux-devtools-extension

redux-devtools-extension 是一款调试 redux 的工具,用来监测 action 很是方便。

首先根据浏览器在Chrome Web Store或者Mozilla Add-ons中下载该插件。

而后在建立 store 时候,将其加入到 store enhancer 配置中便可:

import thunk from "redux-thunk";
import { createBrowserHistory } from "history";

import { createStore, applyMiddleware } from "redux";
+ import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
import { connectRouter, routerMiddleware } from "connected-react-router";

import reducers from "../reducers";

export const history = createBrowserHistory();
const initialState = {};

+  const composeEnhancers = composeWithDevTools({
+   // options like actionSanitizer, stateSanitizer
+ });

const store = createStore(
  connectRouter(history)(reducers),
  initialState,
+  composeEnhancers(applyMiddleware(thunk, routerMiddleware(history)))
);
复制代码

写在最后

本文梳理了本身对 React 应用架构的认识以及相关库的具体配置,进一步加深了对 React 应用架构的理解,可是像数据 Immutable ,持久化,webpack优化等这些,本文并未涉及,将来会继续研究相关的东西,力求搭建更加完善的 React 应用。

另外在搭建项目过程当中升级最新的 babel 后发现@babel/preset-stage-0 即将弃用,建议使用其余代替,更多细节参考:

关键字:

  • redux
  • react-router
  • react-router-config
  • 异步加载(Code Splitting)
  • 热更新
  • 异步任务管理——redux-thunk
  • react-redux
  • redux-devtools-extension

部分用到的库

参考

相关文章
相关标签/搜索