在學(xué)習(xí)React中浴滴,我們必定逃脫不了Redux來解決我們遇到的數(shù)據(jù)流問題,這兒根據(jù)《深入React技術(shù)検厣欤》寫的一個(gè)實(shí)例斤吐。
代碼放在我的github上
初始化 Redux 項(xiàng)目
建立一個(gè)文件
mkdir redux-blod && cd redux-blog
新增一個(gè) package.json文件,安裝需要的依賴
npm install --save react react-dom redux react-router react-redux react-router-redux whatwg-fetch
劃分目錄結(jié)構(gòu):
.
├── node_modules
└── package.json
我們把所有源文件放在 src/ 目錄下黍图,
把測試文件放在 test/ 目錄下曾雕,
把最終生成的、供HTML引用的文件放在 build/ 目錄下
$ mkdir src
$ mkdir test
$ mkdir build
src中目錄劃分即采用類型劃分的特點(diǎn)助被,又添加了功能劃分的特點(diǎn)剖张。
基本上,我們只需要關(guān)注 views/ 和 components/ 這個(gè)兩個(gè)文件夾
設(shè)計(jì)路由
src/
├── components
│ ├── Detail 文章詳情頁
│ └── Home 文章列表頁
└── views
├── Detail.css
├── Detail.js
├── DetailRedux.js
├── Home.css
├── Home.js
└── HomeRedux.js
按照我們的目錄結(jié)構(gòu)揩环,所有的路由應(yīng)該放在 src/routes/ 目錄下搔弄,因此在這個(gè)目錄下新建 index.js 文件,用來配置整個(gè)應(yīng)用的所有路由信息
src/
├── components
├── routes
│ └── index.js
└── views
在index文件中检盼,我們引入所有需要的依賴
// routes/index.js
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';
import Home from '../views/Home';
import Detail from '../views/Detail';
//接下來肯污,使用 react-router 提供的組件來定義應(yīng)用的路由:
const routes = (
<Router history={hashHistory}>
<Route path="/" component={Home} />
<Route path="/detail/:id" component={Detail} />
</Router>
);
優(yōu)化構(gòu)建腳本
添加 webpack-dev-server 作為項(xiàng)目依賴
$ npm install -D webpack-dev-server
將下面的腳本添加到 npm scripts中,我們后續(xù)用 npm run watch 命令執(zhí)行
./node_modules/.bin/webpack-dev-server --hot --inline --content-base
添加布局文件
在 package.json
的 scripts 中添加一條新的記錄可以解決這個(gè)問題:"watch":"./node_modules/.bin/webpack --watch"
吨枉。然后在終端中執(zhí)行 npm run watch 命令蹦渣。
新建src/layouts 目錄终抽,添加兩個(gè)文件 --Frame.js和 Nav.js
src/
├── components
├── layouts
│ ├── Frame.js
│ └── Nav.js
├── routes
└── views
// Nav.js
import React, { Component } from 'react';
import { Link } from 'react-router';
class Nav extends Component {
render() {
return (
<nav>
<Link to='/'>Home</Link>
</nav>
)
}
}
引入一個(gè)新的組件 Frame.js
import React, { Component } from 'react';
import Nav from './Nav';
class Frame extends Component {
render() {
return (
<div className="frame">
<section className="header">
<Nav />
</section>
<section className="container">
{this.props.children}
</section>
</div>
);
}
}
對index.js進(jìn)行改造
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';
import Frame from '../layouts/Frame';
import Home from '../views/Home';
import Detail from '../views/Detail';
const routes = {
<Router history={hashHistory}>
<Route path='/' component={Frame}>
<IndexRoute component={Home}>
<Route path='/detail/:id' component={Detail} />
</Route>
</Router>
}
export default routes;
準(zhǔn)備首頁數(shù)據(jù)
在src/components/Home/ 文件夾下添加幾個(gè)新文件
src/
├── components
│ ├── Detail
│ └── Home
│ ├── Preview.css
│ ├── Preview.js
│ ├── PreviewList.js
│ └── PreviewListRedux.js
├── layouts
├── routes
└── views
在Preview.js 中定義一個(gè)純渲染柒凉、無狀態(tài)的文章預(yù)覽組件
import React, { Component } from 'react';
import './Preview.css';
class Preview extends Component {
static propTypes = {
title: React.PropTypes.string,
link: React.PropTypes.string,
};
render() {
return (
<article className="article-preview-item">
<h1 className="title">{this.props.title}</h1>
<span className="date">{this.props.date}</span>
<p className="desc">{this.props.description}</p>
</article>
)
}
}
PreviewList.js的代碼
import React, { Component } from 'react';
import Preview from './Preview';
class PreviewList extends Component {
static propTypes = {
articleList: React.PropTypes.arrayOf(React.PropTypes.object)
};
render() {
return this.props.articleList.map(item => (
<Preview {...item} key={item.id} />
))
}
}
在介紹 Redux 應(yīng)用目錄結(jié)構(gòu)時(shí),我們提到過Redux.js 里包含了.js 這個(gè)組件需要的reducer寨辩、action creator 和 constants圃庭。
const initialState = {
loading: true,
error: false,
articleList: [],
};
// 3 個(gè)常量定義和一個(gè)函數(shù)定義在邏輯上屬于一個(gè)整體
const LOAD_ARTICLES = 'LOAD_ARTICLES';
const LOAD_ARTICLES_SUCCESS = 'LOAD_ARTICLES_SUCCESS';
const LOAD_ARTICLES_ERROR = 'LOAD_ARTICLES_ERROR';
// 而 loadArticles() 就是一個(gè) action creator锄奢。因?yàn)槊看握{(diào)用 loadArticles() 函數(shù)時(shí),它都會返回一個(gè) action剧腻,所以 action creator 之名恰如其分
export function loadArticles() {
return {
types: [LOAD_ARTICLES, LOAD_ARTICLES_SUCCESS, LOAD_ARTICLES_ERROR],
url: '/api/articles.json',
};
}
function previewList(state = initialState, action) {
switch (action.type) {
case LOAD_ARTICLES: {
return {
...state,
loading: true,
error: false,
};
}
case LOAD_ARTICLES_SUCCESS: {
return {
...state,
loading: false,
error: false,
articleList: action.payload.articleList,
};
}
case LOAD_ARTICLES_ERROR: {
return {
...state,
loading: false,
error: true,
};
}
default:
return state;
}
}
export default previewList;
連接 Redux
- 讓容器型組件關(guān)聯(lián)數(shù)據(jù)
// views/HomeRedux.js包含了 Home 頁面所有組件相關(guān)的 reducer及actionCreator
import { combineReducers } from 'redux';
// 引入 reducer 及 actionCreator
import list from '../components/Home/PreviewListRedux';
export default combineReducers({
list,
});
export * as listAction from '../components/Home/PreviewListRedux'
可以看到拘央,views/ 目錄下的 *Redux.js 文件在更大程度上只是起到一個(gè)整合分發(fā)的作用。和components/ 目錄下的 *Redux.js 文件一樣书在,它默認(rèn)導(dǎo)出的是當(dāng)前路由需要的所有 reducer 的集合灰伟。這里我們引入了 Redux 官方提供的combineReducers 方法,通過這個(gè)方法儒旬,我們可以方便地將多個(gè) reducer 合并為一個(gè)栏账。
此外帖族,HomeRedux.js 還 將PreviewListRedux.js 中所有導(dǎo)出的對象合并后,導(dǎo)出一個(gè)listAction 對象挡爵。稍后竖般,就會看到我們?yōu)槭裁匆@么組織文件。
重新對 views/Home.js做一些修改茶鹃,讓它和Redux連接起來
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PreviewList from '../components/Home/PreviewList';
import { listAction } from './HomeRedux';
class Home extends Component {
render() {
<div>
<h1>Home</h1>
<PreviewList
{...this.props.list}
{...this.props.listAction}
/>
</div>
}
}
export default connect(state => {
return {
list: state.home.list,
}
}, dispatch => {
return {
listAction: bindActionCreators(listActions, dispatch)
}
})(Home)
connect 最多接受 4 個(gè)參數(shù)涣雕,分別如下
[mapStateToProps(state, [ownProps]): stateProps](類型:函數(shù)):接受完整的 Redux
狀態(tài)樹作為參數(shù),返回當(dāng)前組件相關(guān)部分的狀態(tài)樹闭翩,返回對象的所有 key 都會成為組件
的 props胞谭。[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (類型:對象或函數(shù)):
接受 Redux 的 dispatch 方法作為參數(shù),返回當(dāng)前組件相關(guān)部分的 action creator男杈,并可以
在這里將 action creator 與 dispatch 綁定,減少冗余代碼调俘。[mergeProps(stateProps, dispatchProps, ownProps): props] (類型:函數(shù)):如果指定
這個(gè)函數(shù)伶棒,你將分別獲得 mapStateToProps、mapDispatchToProps 返回值以及當(dāng)前組件的
props 作為參數(shù)彩库,最終返回你期望的肤无、完整的 props。-
[options](類型:對象):可選的額外配置項(xiàng)骇钦,有以下兩項(xiàng)宛渐。
- [pure = true](類型:布爾):該值設(shè)為 true 時(shí),將為組件添加 shouldComponentUpdate()
生命周期函數(shù)眯搭,并對 mergeProps 方法返回的 props 進(jìn)行淺層對比窥翩。 - [withRef = false](類型:布爾):若設(shè)為 true,則為組件添加一個(gè) ref 值鳞仙,后續(xù)可
以使用 getWrappedInstance() 方法來獲取該 ref寇蚊,默認(rèn)為 false。
- [pure = true](類型:布爾):該值設(shè)為 true 時(shí),將為組件添加 shouldComponentUpdate()
- 讓展示型組件使用數(shù)據(jù)
相比于容器型組件與 Redux 的復(fù)雜交互棍好,展示型組件實(shí)現(xiàn)起來則簡單得多仗岸,畢竟一切需要的
東西都已經(jīng)通過 props 傳進(jìn)來了
import React, { PropTypes, Component } from 'react';
import Preview from './Preview';
class PreviewList extends Component {
static propTypes = {
loading: PropTypes.bool,
error: PropTypes.bool,
articleList: PropTypes.arrayOf(PropTypes.object),
loadArticles: PropTypes.func,
};
componentDidMount() {
this.props.loadArticles();
}
render() {
const { loading, error, articleList } = this.props;
if (error) {
return <p className="message">Oops, something is wrong.</p>;
}
if (loading) {
return <p className="message">Loading...</p>;
}
return articleList.map(item => (<Preview {...item} key={item.id} />));
}
- 注入Redux
在“讓容器型組件關(guān)聯(lián)數(shù)據(jù) ”一節(jié)中,我們學(xué)習(xí)了如何使用 connect 方法關(guān)聯(lián) Redux 狀態(tài)
樹中的部分狀態(tài)借笙。問題是扒怖,完整的 Redux 狀態(tài)樹是哪里來的呢?
src/
├── app.js
├── components
├── layouts
├── redux
│ ├── configureStore.js
│ └── reducers.js
├── routes
└── views
先來看看 reducers.js业稼,這個(gè)文件里匯總了整個(gè)應(yīng)用所有的 reducer盗痒,而匯總的方法則十分簡單。
因?yàn)槲覀冊?views/ 文件夾中已經(jīng)對各個(gè)路由需要的 reducer 做過一次整理聚合盼忌,所以在 reducers.js
中直接引用 views/*Redux.js 中默認(rèn)導(dǎo)出的 reducer 即可积糯。
而 configureStore.js 則是生成 Redux store 的關(guān)鍵文件掂墓,其中將看到 5.1 節(jié)中提到的 Redux 的
核心 API——createStore 方法
import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { routerReducer } from 'react-router-redux';
import ThunkMiddleware from 'redux-thunk';
import rootReducer from './reducers';
const finalCreateStore = compose(
applyMiddleware(ThunkMiddleware)
)(createStore);
const reducer = combineReducers(Object.assign({}, rootReducer, {
routing: routerReducer,
}));
export default function configureStore(initialState) {
const store = finalCreateStore(reducer, initialState);
return store;
}
新建一個(gè)實(shí)例
// app.js
import ReactDOM from 'react-dom';
import React from 'react';
import configureStore from './redux/configureStore';
import { Provider } from 'react-redux';
import { syncHistoryWithStore } from 'react-router-redux';
import { hashHistory } from 'react-router';
import routes from './routes';
const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store);
ReactDOM.render((
<Provider store={store}>
{routes(history)}
</Provider>
), document.getElementById('root'));
引入 Redux Devtools
需要單獨(dú)下載這些依賴
$ npm install --save-dev redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor
現(xiàn)在講 DevTools 初始化的相關(guān)代碼統(tǒng)一放在 src/redux/DevTools.js 中
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
const DevTools = createDevTools(
<DockMonitor toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'>
<LogMonitor theme='tomorrow' />
</DockMonitor>
);
export default DevTools;
DockMonitor 決定了 DevTools 在屏幕上顯示的位置,我們可以按 Control+Q 鍵切換位置看成,或者按 Control+H 鍵隱藏 DevTool君编。而LogMonitor 決定了 DevTools 中顯示的內(nèi)容默認(rèn)包含了 action的類型、完整的 action 參數(shù)以及 action 處理完成后新的 state川慌。
利用 middleware 實(shí)現(xiàn)Ajax請求發(fā)送
利用redux-composable-fetch 這個(gè) middleware 實(shí)現(xiàn)異步請求
修改configureStore
import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { routerReducer } from 'react-router-redux';
import ThunkMiddleware from 'redux-thunk';
// 引入請求 middleware 的工廠方法
import createFetchMiddleware from 'redux-composable-fetch';
import rootReducer from './reducers';
// 創(chuàng)建一個(gè)請求 middleware 的示例
const FetchMiddleware = createFetchMiddleware();
const finalCreateStore = compose(
applyMiddleware(
ThunkMiddleware,
// 將請求 middleware 注入 store 增強(qiáng)器中
FetchMiddleware
)
)(createStore);
const reducer = combineReducers(Object.assign({}, rootReducer, {
routing: routerReducer,
}));
export default function configureStore(initialState) {
const store = finalCreateStore(reducer, initialState);
return store;
}
利用webpack-dev-server 在本地啟動一個(gè)簡單的http服務(wù)器來響應(yīng)頁面
頁面之間的跳轉(zhuǎn)
在 Redux 應(yīng)用中吃嘿,路由狀態(tài)也屬于整個(gè)應(yīng)用狀態(tài)的一部分,所以更合理的方案應(yīng)該是通過分發(fā)action來更新路由
使用 react-router-redux 中提供的 routerMiddleware
// redux/configureStore.js
import { hashHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
import rootReducer from './reducers';
const finalCreateStore = compose(
applyMiddleware(
// 引入其他 middleware
// ...
// 引入 react-router-redux 提供的 middleware
routerMiddleware(hashHistory)
)
)(createStore);
引入新的 middleware 之后梦重,就可以像下面這樣簡單修改當(dāng)前路由了:
import { push } from 'react-router-redux';
// 在任何可以拿到 store.dispatch 方法的環(huán)境中
store.dispatch(push('/'))
跳轉(zhuǎn)修改
// components/Home/Preview.js
import React, { Component, PropTypes } from 'react';
class Preview extends Component {
static propTypes = {
title: PropTypes.string,
link: PropTypes.string,
push: PropTypes.func,
};
handleNavigate(id, e) {
// 阻止原生鏈接跳轉(zhuǎn)
e.preventDefault();
// 使用 react-router-redux 提供的方法跳轉(zhuǎn)兑燥,以便更新對應(yīng)的 store
this.props.push(id);
}
render() {
return (
<article className="article-preview-item">
<h1 className="title">
<a href={`/detail/${this.props.id}`} onClick={this.handleNavigate.bind(this,
this.props.id)}>
{this.props.title}
</a>
</h1>
<span className="date">{this.props.date}</span>
<p className="desc">{this.props.description}</p>
</article>
);
}
}
優(yōu)化與改進(jìn)
調(diào)整代碼以及構(gòu)建腳本,最終實(shí)現(xiàn)在開發(fā)環(huán)境中加載 Redux DevTools琴拧,而在生產(chǎn)環(huán)境中不進(jìn)行任何加載
要實(shí)現(xiàn)這樣的需求降瞳,首先添加一款 webpack 插件-- DefinePlugin,這款插件允許我們定義任意的字符串,并將所有文件中包含這些字符串的地方都替換為指定值蚓胸。
我們需要了解一種常見的定義 Node.js 應(yīng)用環(huán)境的方法——環(huán)境變量挣饥。一般意義上來說,我們習(xí)慣使用 process.env.NODE_ENV 這個(gè)變量的值來確定當(dāng)前是在什么環(huán)境中運(yùn)行應(yīng)用沛膳。當(dāng)讀取不到該值時(shí)扔枫,默認(rèn)當(dāng)前是開發(fā)環(huán)境;而當(dāng)process.env.NODE_ENV=production 時(shí)锹安,我們認(rèn)為當(dāng)前是生產(chǎn)環(huán)境短荐。
而在生產(chǎn)環(huán)境中,配合另一款插件UglifyJS 的無用代碼移除功能叹哭,可以方便地將任何不必要的依賴統(tǒng)統(tǒng)移除忍宋。
if ( process.env.NODE_ENV === 'production' ) {
// 這里的代碼只會在生產(chǎn)環(huán)境執(zhí)行
} else {
// 這里的代碼只會在開發(fā)環(huán)境執(zhí)行
}