上一篇文章——从零开始搭建 React 应用(一)——基础搭建讲述了如何使用 webpack 搭建一个很是基础的 react 开发环境。本文将详细讲述搭建一个 React 应用的架构。css
仓库地址:github.com/MrZhang123/…html
在咱们开发过程当中,不少时候,咱们须要让组件共享某些数据,虽然能够经过组件传递数据实现数据共享,可是若是组件之间不是父子关系的话,数据传递是很是麻烦的,并且容易让代码的可读性下降,这时候咱们就须要一个 state(状态)管理工具。常见的状态管理工具备 redux,mobx,这里选择 redux 进行状态管理。值得注意的是 React 16.3 带来了全新的Context API,咱们也可使用新的 Context API 作状态管理。vue
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可让你构建一致化的应用,运行于不一样的环境(客户端、服务器、原生应用),而且易于测试。不只于此,它还提供很是好的开发体验,好比有一个时间旅行调试器能够编辑后实时预览。react
redux 的数据流以下图所示:webpack
redux 的三大原则:ios
state
都被存储在一棵 object tree 中,而且这个 object tree 只存在于惟一的 store 中,可是这并不意味使用 redux 就须要将全部的 state 存到 redux 上。action
,action
是一个用于描述已发生事件的普通对象。Redux middleware 提供位于 action 发起以后,到达 reducer 以前的扩展点。dispatch 发起的 action 依次通过中间件,最终到达 reducer。咱们能够利用 Redux middleware 来进行日志记录、建立崩溃报告、调用异步接口或者路由等等。本质上来说中间件只是拓展了 store.dispatch
方法。git
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
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')
)
复制代码
在一个 react 应用中只有一个 store,组件经过调用 action 函数,传递数据到 reducer,reducer 根据数据更改对应的 state。可是随着应用复杂度的提高,reducer 也会变得愈来愈大,此时能够考虑将 reducer 拆分红多个单独的函数,拆分后的每一个函数负责独立管理 state 的一部分。
redux 提供combineReducers
辅助函数,将分散的 reducer 合并成一个最终的 reducer 函数,而后在 createStore 的时候使用。
有时候咱们须要多个 middleware 组合在一块儿造成 middleware 链来加强store.dispatch
,在建立 store 时候,咱们须要将 middleware 链整合到 store 中,官方提供applyMiddleware(...middleware)
将 middleware 链在一块儿。
store enhancer 用于加强 store,若是咱们有多个 store enhancer 时须要将多个 store enhancer 整合,这时候就会用到compose(...functions)
。
使用compose
合并多个函数,每一个函数都接受一个参数,它的返回值将做为一个参数提供给它左边的函数以此类推,最右边的函数能够接受多个参数。compose(funA,funB,funC)
能够理解为compose(funA(funB(funC())))
,最终返回从右到左接收到的函数合并后的最终函数。
redux 经过createStore
建立一个 Redux store 来以存放应用中全部的 state
,createStore
的参数形式以下:
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 的路由解决方案,它保持 UI 与 URL 的同步。项目中咱们整合最新版的 React Router v4。
在 react-router v4 中 react-router 被划分为三个包:react-router,react-router-dom 和 react-router-native,区别以下:
React Router 与 Redux 一块儿使用时大部分状况下都是正常的,可是偶尔会出现路由更新可是子路由或活动导航连接没有更新。这个状况发生在:
connect()(Comp)
链接 redux。<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 进行更深度的整合,实现:
这些能够经过 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 reducerrouterMiddleware(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
切换路由了:
import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
复制代码
在 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'))
复制代码
从 v4 版本开始,react-router 使用动态组件代替路径配置,即 react-router 就是 react 应用的一个普通组件,随用随写,没必要像以前那样,路由跟组件分离。所以 react 应用添加 react-router,首先引入咱们须要的东西。
import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
复制代码
这里咱们将BrowserRouter
引入并从新命名为Router
,BrowserRouter
容许 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 以后,咱们就能够写咱们熟悉的静态路由了。同时,利用它,能够将路由配置分散在各个组件中,最后使用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 应用添加了静态路由。
模块热替换(HMR)功能会在应用程序运行过程当中替换、添加或删除模块,而无需从新加载整个页面。主要经过如下几种方式:
在开发模式中,HMR 能够替代 LiveReload,webpack-dev-server 支持hot
模式,在试图从新加载整个页面以前,hot
模式尝试使用 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 很是简单,只须要在根组件导出的时候添加高阶方法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
就能够了。
完成以上配置后,咱们的主体已经搭建的差很少了,可是当打开开发者工具会发现,应用开始加载的时候直接把整个应用的 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 里添加一个专门的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,第一个 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 将异步操做的结果返回回来。
这里须要说明几点:
fetchPosts
返回了一个函数,而普通的 Action Creator 默认返回一个对象。dispatch
和getState
这两个 Redux 方法,普通的 Action Creator 的参数是 Action 的内容。store.dispatch({type: SET_DEMO_DATA.PENDING})
,表示异步操做开始。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 的工具,用来监测 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 即将弃用,建议使用其余代替,更多细节参考: