# 配置 Store
在[基礎](../basics/README.md)這一章節我們構建了一個 Todo list 應用,介紹了一些 Redux 基礎知識。
接下來我們將會探索如何定制 store 來添加額外的功能。我們將會接著基礎部分的源代碼繼續寫,你可以在[文檔](../basics/ExampleTodoList.md),或 [倉庫中的示例部分](https://github.com/reduxjs/redux/tree/master/examples/todos/src),或者用 [CodeSandbox 在瀏覽器中](https://codesandbox.io/s/github/reduxjs/redux/tree/master/examples/todos)閱讀這些代碼。
## 創建 store
首先我們來看看在原先的 `index.js` 中我們是如何創建 store 的:
```js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
```
代碼中,我們將 reducer 傳入 Redux 提供的 `createStore` 函數中,它返回了一個 `store` 對象。接著我們將這個對象傳入 `react-redux` 提供的 `Provider` 組件中,然后將它掛載在組件樹的根部。
這樣做可以保證我們在任何時候通過 `react-redux` 的 `connect` 連接到 Redux 時,store 可以在組件中正常使用。
## 拓展 Redux 功能
大多數的應用都會使用 middleware 或 enhancer 來拓展 Redux store 的功能。**(注:middleware 很常見,enhancer 不太常見)** middleware 拓展了 Redux `dispatch` 函數的功能;enhancer 拓展了 Redux store 的功能。
我們將會添加如下兩個 middleware 和 一個 enhancer:
- [`redux-thunk` middleware](https://github.com/reduxjs/redux-thunk),允許了簡單的 dispatch 異步用法。
- 一個記錄 dispatch 的 action 和得到的新 state 的 middleware。
- 一個記錄 reducer 處理每個 action 所用時間的 enhancer。
#### 安裝 `redux-thunk`
```
npm install --save redux-thunk
```
#### middleware/logger.js
```js
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd()
return result
}
export default logger
```
#### enhancers/monitorReducer.js
```js
const round = number => Math.round(number * 100) / 100
const monitorReducerEnhancer = createStore => (
reducer,
initialState,
enhancer
) => {
const monitoredReducer = (state, action) => {
const start = performance.now()
const newState = reducer(state, action)
const end = performance.now()
const diff = round(end - start)
console.log('reducer process time:', diff)
return newState
}
return createStore(monitoredReducer, initialState, enhancer)
}
export default monitorReducerEnhancer
```
我們把這些東西添加到 `index.js` 中。
- 首先我們引入 `redux-thunk`,我們自己寫的 `loggerMiddleware` 和 `monitorReducerEnhancer`,還有兩個 Redux 提供的函數:`applyMiddleware` 和 `compose`。
- 接下來我們用 `applyMiddleware` 來創建一個 store enhancer,通過它我們可以將 `loggerMiddleware` 和 `thunkMiddleware` 應用到 dispatch 函數。
- 下一步,我們使用 `compose` 將新的 `middlewareEnhancer` 和 `monitorReducerEnhancer` 組合到一起。這一步必須要做,因為 `createStore` 只接受一個 enhancer 作為參數,所以你只能將它倆組合(compose)成一個大的 enhancer,正如示例中所示。
- 最后,我們將這個 `composedEnhancers` 函數傳入 `createStore` 中作為第三個參數。 **注:這里我們忽略掉了 createStore 的第二個參數,你可以通過它給 store 傳入一個初始 state。**
```js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { applyMiddleware, createStore, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './reducers'
import loggerMiddleware from './middleware/logger'
import monitorReducerEnhancer from './enhancers/monitorReducer'
import App from './components/App'
const middlewareEnhancer = applyMiddleware(loggerMiddleware, thunkMiddleware)
const composedEnhancers = compose(
middlewareEnhancer,
monitorReducerEnhancer
)
const store = createStore(rootReducer, undefined, composedEnhancers)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
```
## 這種做法的問題
這些代碼的確能正常運行,但對于一個典型(typical)的應用來說還不夠理想。
大多數應用使用的 middleware 大于一個,而且每個 middleware 經常需要一些額外的初始化工作。這些“噪聲”的加入會使 `index.js` 很快變得難以維護,因為這些邏輯并沒有被清晰合理地組織起來。
## 解決方案:`configureStore`
這個問題的解決方案是創造一個封裝了 store 創建邏輯的新的函數 `configureStore`。它放在一個單獨的文件中,所以可以被任意拓展。
我們的 `index.js` 的終極目標會是這樣:
```js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'
const store = configureStore()
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
```
所有配置 store 相關的邏輯(包括引入 reducer、middleware、enhancer),都放置于一個單獨用于處理它們的文件中。
為了達到這個目標,`configureStore` 函數應該是這樣:
```js
import { applyMiddleware, compose, createStore } from 'redux'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
return store
}
```
這個函數遵循了上述步驟,其中有一些邏輯拆分開來,為后續拓展做好準備。
- `middlwares` 和 `enhancers` 都被定義為數組,與實際使用它們的函數分離開來。這樣做我們可以很容易地為不同情況添加更多的 middleware 或 enhancer。
例如,一種很常見的情況是,只有在開發模式下才添加某些 middleware。這個可以在一個 if 條件句中向 middleware 數組添加新的 middleware 來輕松實現:
```js
if (process.env === 'development') {
middlewares.push(secretMiddleware)
}
```
- 將一個 `preloadedState` 變量傳入 `createStore`,以便后續使用。
這樣我們調試 `createStore` 也更輕松了——每個步驟都分離的清清楚楚,目前正在發生什么一目了然。
## 集成 devtools 擴展程序
另外一個你很想在你的應用中加入的功能就是 `redux-devtools-extension` 的集成。
這個擴展程序是一系列工具的集合,通過它們你可以獲得 Redux store 完全的控制:它允許你觀察或重放 action,在不同時刻查看 state,直接向 store 中 dispatch 一個 action,......等等很多功能。[點擊此處閱讀更多](https://github.com/zalmoxisus/redux-devtools-extension)。
有很多方案可以集成此擴展程序,但我們會用最方便的那個。
首先,使用 npm 安裝:
```
npm install --save-dev redux-devtools-extension
```
接下來,我們移除掉從 `redux` 引入的 `compose` 函數,然后用一個從 `redux-devtools-extension` 中引入的 `composeWithDevTools` 函數來替換它。
最終的代碼是這樣:
```js
import { applyMiddleware, createStore } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
return store
}
```
這樣就成了!
現在用安裝了 devtools 擴展程序的瀏覽器訪問我們的應用,我們即可使用這一強大的工具來探索和調試了。
## 熱加載
還有一個強大的工具可以使你的開發過程更加直觀,那就是熱加載(hot reloading),它可以在無需重啟整個應用的情況下替換代碼。
比如說,你啟動了應用程序,使用了一會兒,決定對其中一個 reducer 做一些改動。正常情況下,你做了改動后應用會重啟,并且 Redux state 會重置到其初始狀態。
當使用熱加載時,只有你做了更改的 reducer 會被重啟。這樣一來你就無需在每次修改代碼之后重置整個 state,開發過程會更加迅速。
我們將 Redux reducer 和 React 組件都啟用熱加載。
首先,將它引入 `configureStore` 函數:
```js
import { applyMiddleware, compose, createStore } from 'redux'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
```
新的代碼放置在 `if` 條件句中,所以它只在非生產環境中運行,而且只在能用 `module.hot` 時運行。
像 Webpack 和 Parcel 這樣的打包工具支持 `module.hot.accept` 方法,它用于指定哪一個模塊(module)應該啟用熱加載,以及當模塊改變時應該做什么。此例中,我們監控 `./reducers` 模塊,當這個模塊改變時將新計算出的 `rootReducer` 傳入 `store.replaceReducer` 函數。
在 `index.js` 中,我們也會使用相同的模式來熱加載 React 組件:
```js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'
const store = configureStore()
const renderApp = () =>
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./components/App', renderApp)
}
renderApp()
```
這里只有一個地方與剛剛不同,就是我們將應用的渲染封裝進了 `renderApp` 函數,調用它將會重新渲染整個應用。
## 下一步
現在你已經學會了將配置 store 的方法進行封裝,讓它更易于維護。你現在可以[學習 Redux 提供的高級特性](../advanced/README.md),或者看看一些[Redux 生態系統提供的擴展程序](../introduction/Ecosystem.md#debuggers-and-viewers)。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- Action
- Reducer
- Store
- 數據流
- 搭配 React
- 示例:Todo List
- 高級
- 異步 Action
- 異步數據流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 遷移到 Redux
- 使用對象展開運算符
- 減少樣板代碼
- 服務端渲染
- 編寫測試
- 計算衍生數據
- 實現撤銷重做
- 子應用隔離
- 組織 Reducer
- Reducer 基礎概念
- Reducer 基礎結構
- Reducer 邏輯拆分
- Reducer 重構示例
- combineReducers 用法
- combineReducers 進階
- State 范式化
- 管理范式化數據
- Reducer 邏輯復用
- 不可變更新模式
- 初始化 State
- 結合 Immutable.JS 使用 Redux
- 常見問題
- 綜合
- Reducer
- 組織 State
- 創建 Store
- Action
- 不可變數據
- 代碼結構
- 性能
- 設計哲學
- React Redux
- 其它
- 排錯
- 詞匯表
- API 文檔
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文檔
- API
- 排錯