上一篇文章——從零開始搭建 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 即將棄用,建議使用其餘代替,更多細節參考: