Redux 應(yīng)用實(shí)例

在學(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)剖张。

目錄劃分.png

基本上,我們只需要關(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

  1. 讓容器型組件關(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。
  1. 讓展示型組件使用數(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} />));
    }
  1. 注入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í)行
}

添加單元測試

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市话速,隨后出現(xiàn)的幾起案子讶踪,更是在濱河造成了極大的恐慌,老刑警劉巖泊交,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乳讥,死亡現(xiàn)場離奇詭異,居然都是意外死亡廓俭,警方通過查閱死者的電腦和手機(jī)云石,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來研乒,“玉大人汹忠,你說我怎么就攤上這事。” “怎么了宽菜?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵谣膳,是天一觀的道長。 經(jīng)常有香客問我铅乡,道長继谚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任阵幸,我火速辦了婚禮花履,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挚赊。我一直安慰自己诡壁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布荠割。 她就那樣靜靜地躺著妹卿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蔑鹦。 梳的紋絲不亂的頭發(fā)上纽帖,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機(jī)與錄音举反,去河邊找鬼。 笑死扒吁,一個(gè)胖子當(dāng)著我的面吹牛火鼻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播雕崩,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼魁索,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盼铁?” 一聲冷哼從身側(cè)響起粗蔚,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饶火,沒想到半個(gè)月后鹏控,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肤寝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年当辐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲤看。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缘揪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情找筝,我是刑警寧澤蹈垢,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站袖裕,受9級特大地震影響曹抬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜陆赋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一沐祷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧攒岛,春花似錦赖临、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至顺饮,卻和暖如春吵聪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背兼雄。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工吟逝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赦肋。 一個(gè)月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓块攒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親佃乘。 傳聞我的和親對象是個(gè)殘疾皇子囱井,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容