- 文中的藍(lán)色字體是相關(guān)內(nèi)容的超鏈接,網(wǎng)址不另外列出映皆,請放心點(diǎn)擊组去。
- 本文內(nèi)容適合 Redux 和 React 新手,也歡迎 Redux 和 React 專家指導(dǎo)點(diǎn)評步淹。
摘要
閱讀本文并實(shí)際上手編碼運(yùn)行从隆,你將解決如下幾個(gè)疑問:
- Redux是如何運(yùn)作的诚撵?
- 如何規(guī)范化State?
- 如何創(chuàng)建一個(gè)簡單的React+Redux應(yīng)用(簡單路由)
工具
預(yù)備知識
- 熟悉 ES6 相關(guān)知識
- 了解 React 相關(guān)知識
Redux API
在學(xué)習(xí)前,如果你從未接觸過Redux或者對Redux不甚了解键闺,別擔(dān)心寿烟,通過API你可以了解詳盡的背景知識。API的內(nèi)容完善且豐富艾杏,故不在此贅述韧衣。本示例應(yīng)用基于Redux官方示例應(yīng)用之一的Shopping Cart ,我在此基礎(chǔ)上添加了一點(diǎn)點(diǎn)簡單的路由功能,在本文中购桑,我會(huì)針對這個(gè)示例項(xiàng)目結(jié)合Redux知識作詳盡的講解。官方示例應(yīng)用都很容易理解和上手而且富含Redux知識點(diǎn)氏淑。希望意圖學(xué)習(xí)Redux的讀者們照著所有的官方示例項(xiàng)目動(dòng)手實(shí)踐一番勃蜘,相信你很快就能將Redux運(yùn)用得得心應(yīng)手。
應(yīng)用效果展示
應(yīng)用首頁效果如下
點(diǎn)擊了Add to cart 按鈕后的效果如下
點(diǎn)擊了Checkout按鈕后假残,將跳轉(zhuǎn)到我們的結(jié)果頁面缭贡,如下所示
點(diǎn)擊Pay for it !按鈕,會(huì)進(jìn)行支付辉懒。
點(diǎn)擊back 按鈕后阳惹,又將跳轉(zhuǎn)回到我們的應(yīng)用首頁,如下所示眶俩。注意觀察莹汤,我們商品數(shù)量減少了!
現(xiàn)在開始實(shí)現(xiàn)它
好啦颠印,現(xiàn)在讓我們開始一步一步實(shí)現(xiàn)這個(gè)簡單的小應(yīng)用吧纲岭。
First 我們的需求是什么
首先,我們要知道自己要做什么线罕,根據(jù)上面的效果圖止潮,來看看我們有哪些工作內(nèi)容。
- 我們會(huì)有兩個(gè)頁面钞楼,一個(gè)展示產(chǎn)品信息和購物車信息的頁面喇闸,一個(gè)確認(rèn)付款的結(jié)果頁面,這兩個(gè)頁面可以相互跳轉(zhuǎn)询件。
- 展示產(chǎn)品信息和購物車信息的頁面(以下就簡稱它為主頁面)包含了兩部分的內(nèi)容燃乍。產(chǎn)品信息模塊包含了一個(gè)大標(biāo)題和一個(gè)產(chǎn)品的列表,每個(gè)條目展示了產(chǎn)品的名稱雳殊,價(jià)格和數(shù)量以及一個(gè)把它加入購物車的按鈕橘沥,點(diǎn)擊加入購物車的按鈕一下,相應(yīng)的產(chǎn)品就會(huì)減少一個(gè)夯秃,直到該產(chǎn)品的數(shù)量歸零座咆,該按鈕將不再能被繼續(xù)點(diǎn)擊痢艺。購物車頁面包含了我們添加到購物車的產(chǎn)品的信息,包括了每個(gè)產(chǎn)品的名稱介陶,價(jià)格和數(shù)量還有一個(gè)結(jié)算按鈕堤舒,點(diǎn)擊這個(gè)按鈕我們會(huì)跳轉(zhuǎn)到一個(gè)確認(rèn)付款的結(jié)果頁面(以下簡稱它為結(jié)果頁面)。
- 結(jié)果頁面哺呜。結(jié)果頁面分為確認(rèn)模塊和結(jié)果模塊兩部分舌缤,確認(rèn)模塊的內(nèi)容非常簡單,就只有簡單的兩行話和一個(gè)付款按鈕某残。最后一行會(huì)獲取到我們添加到購物車的商品的總價(jià)并展示出來国撵。點(diǎn)擊付款按鈕時(shí),會(huì)進(jìn)行支付玻墅。支付會(huì)展示支付結(jié)果模塊介牙,有成功和失敗兩種,如果成功的話澳厢,我們的購物車會(huì)被清空环础。點(diǎn)擊返回按鈕,返回主頁面剩拢。
這些就是我們的全部需求了线得。
看到需求,我們不著急先進(jìn)行編碼徐伐,不如先讓我們仔細(xì)思考下我們數(shù)據(jù)該怎么組織贯钩,頁面該怎么劃分,數(shù)據(jù)該怎么流動(dòng)呵晨。
思考一下
首先我們需要為我們的數(shù)據(jù)對象建立個(gè)模型魏保,在我們這個(gè)簡單的小應(yīng)用里只有一個(gè)簡單的數(shù)據(jù)對象,就是產(chǎn)品摸屠。一個(gè)產(chǎn)品對象應(yīng)該包含哪些東西呢谓罗?想必你已經(jīng)發(fā)現(xiàn)了,它至少要包含產(chǎn)品的名字季二,產(chǎn)品的價(jià)格檩咱,以及產(chǎn)品的庫存數(shù)量。這些是全部嗎胯舷?當(dāng)然不是刻蚯。如果你有一定的應(yīng)用開發(fā)經(jīng)驗(yàn)(或者你設(shè)計(jì)過數(shù)據(jù)庫表),你肯定知道我們通常需要一個(gè)標(biāo)識來區(qū)分這個(gè)產(chǎn)品對象模型的實(shí)體桑嘶。我們產(chǎn)品實(shí)體對象需要一個(gè)id以將它和其他產(chǎn)品實(shí)體對象區(qū)別開來炊汹。Id,名稱,價(jià)格逃顶,庫存數(shù)量讨便,這些是全部嗎充甚?哈哈哈哈,幾乎是全部了霸褒。我們這么來表示一個(gè)產(chǎn)品對象基本沒問題伴找。但是別忘了,我們還有一個(gè)購物車的功能废菱,我們需要把加入購物車的產(chǎn)品數(shù)量也記錄下來技矮,這樣我們就可以計(jì)算購物車內(nèi)商品的總價(jià)格了。在這個(gè)小項(xiàng)目里殊轴,為了盡可能的簡單衰倦,我們悄悄把用戶添加到購物車?yán)锏漠a(chǎn)品數(shù)量,也附加到產(chǎn)品對象模型上(注意旁理,在生產(chǎn)實(shí)踐上你也許不能這么做9⒈摇!韧拒!)。
現(xiàn)在一個(gè)產(chǎn)品對象模型就展現(xiàn)在我們面前了十性。
產(chǎn)品對象product [id,名稱叛溢,價(jià)格,庫存量劲适,買入量]
建立產(chǎn)品模型也是我們設(shè)計(jì)State的一部分工作內(nèi)容楷掉。
建立了模型,再讓我們來看看頁面該怎么劃分呢霞势?按照React組件化的思想烹植,我們將頁面劃分的適度小一點(diǎn)最好,這樣在頁面重新渲染的時(shí)候愕贡,需要渲染的部分也很少草雕。
如下圖,我們將產(chǎn)品信息組件分成了三塊固以,我用不同顏色的框?qū)⑺麄兛虺鰜砹恕?/p>
產(chǎn)品們是以一個(gè)列表的形式來展示的墩虹,每個(gè)列表?xiàng)l目又包含了該產(chǎn)品的信息塊和一個(gè)按鈕。產(chǎn)品的信息塊包含了產(chǎn)品的名稱憨琳、價(jià)格以及庫存數(shù)量诫钓。我們把這一大塊的內(nèi)容,盡可能得拆成小塊的內(nèi)容篙螟,然后再用小塊像搭建樂高積木一樣將大塊一點(diǎn)一點(diǎn)搭建出來菌湃。
購物車信息塊也是相似的劃分。不再次贅述了遍略。
接下來我們思考下數(shù)據(jù)的流動(dòng)惧所。我們先來看看哪些部分會(huì)產(chǎn)生變化骤坐,產(chǎn)品信息頁面的的庫存數(shù)量會(huì)變化,添加進(jìn)入購物車的按鈕會(huì)產(chǎn)生變化纯路,這個(gè)變化依賴于庫存數(shù)量的變化或油,庫存數(shù)量歸0了,按鈕就發(fā)生變化了驰唬。購物車信息會(huì)產(chǎn)生變化顶岸,會(huì)展示出被我們點(diǎn)擊過加入購物車按鈕的產(chǎn)品的名稱以及價(jià)格,以及購買的件數(shù)即(點(diǎn)擊按鈕的次數(shù))叫编。購物車內(nèi)的總價(jià)會(huì)變化辖佣,結(jié)果頁的總價(jià)也會(huì)產(chǎn)生變化,不過總價(jià)的變化依賴于加入購物車的產(chǎn)品數(shù)量。
根據(jù)頁面和數(shù)據(jù)流動(dòng)我們大致可以確定這個(gè)應(yīng)用的state
的大概樣子了。
state
- products[ ]
- carts[ ]
我們的state可能包含兩個(gè)數(shù)組一個(gè)是產(chǎn)品信息的數(shù)組填硕,一個(gè)是購物車信息的數(shù)組玛追,里面是我們的產(chǎn)品對象實(shí)體。
開發(fā)復(fù)雜的應(yīng)用時(shí)蹲盘,不可避免會(huì)有一些數(shù)據(jù)相互引用。建議你盡可能地把 state 范式化,不存在嵌套污淋。把所有數(shù)據(jù)放到一個(gè)對象里,每個(gè)數(shù)據(jù)以 ID 為主鍵余掖,不同實(shí)體或列表間通過 ID 相互引用數(shù)據(jù)寸爆。把應(yīng)用的 state 想像成數(shù)據(jù)庫。這種方法在 normalizr 文檔里有詳細(xì)闡述盐欺。例如赁豆,實(shí)際開發(fā)中,在 state 里同時(shí)存放
todosById: { id -> todo }
和todos: array<id>
是比較好的方式
讓我們以normalizr的方式讓我們的state更規(guī)范一點(diǎn)冗美。
新的state
- products
-- productIds[id1,id2,id3]
-- productsById{id1:{product},id2:{product},id3:{product}} - carts
--addedIds[id1,id2]
--addedById{id1:{product},id2:{product}}
--paid
相信這么一長串的分析也讓你對應(yīng)用可能會(huì)產(chǎn)生的交互動(dòng)作action
有所感悟魔种。
應(yīng)用至少會(huì)產(chǎn)生以下幾個(gè)動(dòng)作:
- 請求數(shù)據(jù)的動(dòng)作
- 將產(chǎn)品添加到購物車的動(dòng)作
- 點(diǎn)擊結(jié)算按鈕的動(dòng)作
- 點(diǎn)擊付款按鈕的動(dòng)作
- 點(diǎn)擊返回按鈕的動(dòng)作
充分的思考過后我們就可以開始實(shí)踐了!
讓我們首先從一些簡單的部分開始著手墩衙,比如把產(chǎn)品展示出來务嫡。
展示產(chǎn)品
首先我們讓制作點(diǎn)兒產(chǎn)品假數(shù)據(jù)。
我們在api文件夾下新建一個(gè)product.json
文件漆改,里面包含了一些我們用來模擬從后臺接收到的產(chǎn)品數(shù)據(jù)心铃。
[
{ "id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2 },
{ "id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10 },
{ "id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5 }
]
現(xiàn)在我們需要來獲取這些模擬數(shù)據(jù)了,我們在api文件夾下再新建一個(gè)shop.js
文件挫剑。
import _products from "./product.json";
const TIME_OUT = 2000;
/**
* cb 是個(gè)函數(shù)參數(shù),延遲多少ms后獲取產(chǎn)品信息
*/
export default {
getProduct: (cb, timeout) => setTimeout(cb(_products), timeout || TIME_OUT),
};
通過把模擬數(shù)據(jù)傳入函數(shù)里去扣,實(shí)現(xiàn)接收數(shù)據(jù)這一動(dòng)作。使用了setTimeout來模擬請求響應(yīng)之間的耗時(shí)。
接下來我們可以定義接收數(shù)據(jù)這一動(dòng)作類型了愉棱。
在constants文件夾下新建一個(gè)ActionTypes.js
文件來定義一些動(dòng)作類型常量唆铐。
export const RECEIVE_PRODUCTS = "RECEIVE_PRODUCTS";
定義好了常量類型,就需要去定義動(dòng)作(action
)了奔滑,動(dòng)作是一個(gè)改變state的信號艾岂,它包含了需要的數(shù)據(jù)。
我們在actions文件夾下新建一個(gè)index.js文件(我們的應(yīng)用比較簡單朋其,所以才使用一個(gè)文件)王浴,來存放所有的動(dòng)作事件。
import shop from "../api/shop";
import * as types from "../constants/ActionTypes";
/**
* 這是在接收到產(chǎn)品數(shù)據(jù)后發(fā)送的動(dòng)作
* @param {產(chǎn)品JSON} products 參數(shù)是json
*/
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS,
products
});
export const getAllProducts = () => dispatch => {
shop.getProduct(products => {
dispatch(receiveProducts(products));
});
};
在獲取到我們的模擬數(shù)據(jù)后我們發(fā)送一個(gè)帶有產(chǎn)品數(shù)據(jù)的action來告訴應(yīng)用我們要改變state了梅猿。但是action僅僅只是一個(gè)信號氓辣,它并不負(fù)責(zé)去更新state。我們通過編寫reducer純函數(shù)來實(shí)現(xiàn)更新保存著應(yīng)用所有state的單一的store內(nèi)的某一state袱蚓。即真正更新Store中的state的是我們r(jià)educer,它接受action作為參數(shù)之一钞啸,拿到action中的數(shù)據(jù)來更新state。
我們在reducers文件夾下新建一個(gè)products.js文件來保存所有有關(guān)處理關(guān)于產(chǎn)品state變更的reducer喇潘。reducer保證只要傳入?yún)?shù)相同体斩,返回計(jì)算得到的下一個(gè) state 就一定相同。
import { RECEIVE_PRODUCTS } from "../constants/ActionTypes";
import { combineReducers } from "redux";
/**
* 把產(chǎn)品數(shù)據(jù)以normalized化的形式組織颖低,即通過id對應(yīng)一個(gè)產(chǎn)品(1)
* 通過reduce的方式來實(shí)現(xiàn)
* @param {*} state
* @param {*} action
*/
const productsOrangedById = (state = {}, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product;
return obj;
}, {})
};
default:
return state;
}
};
//把產(chǎn)品數(shù)據(jù)以normalized化的形式組織硕勿,記錄所有的id值(2)
//map 把一個(gè)數(shù)組映射成一個(gè)新數(shù)組
//map() 原數(shù)組中的每個(gè)元素調(diào)用這個(gè)方法后返回值組成的新數(shù)組
const visibleIds = (state = [], action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return action.products.map(product => product.id);
default:
return state;
}
};
//組合成state
/**
* 結(jié)構(gòu)應(yīng)該是這樣的
* {
* visibleIds:[productId1,productId2],
* productsOrangedById:{
* productId1:{
* 屬性:值
* }
* productId2:{
* 屬性:值
* }
* }
* }
*/
export default combineReducers({
productsOrangedById,
visibleIds
});
//獲取state中的值的方式
/**
* 直接獲取state中的某個(gè)產(chǎn)品的方式
* */
export const getProduct = (state, id) => state.productsOrangedById[id];
/**
* 獲取產(chǎn)品數(shù)組的方式
* @param {*} state
*/
export const getVisibleProducts = state =>
state.visibleIds.map(id => getProduct(state, id));
我們定義了兩個(gè)reducer函數(shù)來處理action。注意我們使用了combineReducers生成了一個(gè)函數(shù)來調(diào)用這些reducer枫甲,對于Redux中的每一個(gè)action,每一個(gè)reducer都將被調(diào)用到扼褪,如果它有處理action定義的類型的邏輯想幻,它就會(huì)執(zhí)行這段邏輯,如果沒有话浇,它就返回一個(gè)默認(rèn)值脏毯,這個(gè)默認(rèn)值通常就是這個(gè)reducer接收的state。
通過使用reduce()函數(shù)和map()函數(shù)幔崖,我們實(shí)現(xiàn)了對state的normalizr化組織方式食店。
最后我們提供了兩個(gè)函數(shù)來獲取state中的某一產(chǎn)品和產(chǎn)品組。
我們在reducers文件夾下新建一個(gè)index.js文件用來將所有的子reducer組合起來(其實(shí)你現(xiàn)在不一定需要這么做赏寇,因?yàn)槲覀冎挥幸粋€(gè)子文件)吉嫩。
import { combineReducers } from "redux";
import products, * as fromProducts from "./products";
export default combineReducers({
products,
});
將reducer再整合一次,以后生成的store結(jié)構(gòu)樹也因此多了一層嗅定。
編寫完了reducer自娩,我們就可以來編寫頁面展示的組件了。首先從外層大的List組件開始渠退。
在components文件夾下新建Product.js文件忙迁。
import React from "react";
import PropTypes from "prop-types";
//產(chǎn)品
const Product = ({ title, price, quantity }) => (
<div>
{title} - ${price}
{quantity ? ` x ${quantity}` : null}
</div>
);
Product.propTypes = {
price: PropTypes.number,
quantity: PropTypes.number,
title: PropTypes.string
};
export default Product;
我們的產(chǎn)品組件需要展示名稱脐彩、價(jià)格和數(shù)量,因此它接收這三項(xiàng)參數(shù)姊扔。
在components文件夾下新建ProductItem.js文件惠奸,構(gòu)建產(chǎn)品條目組件。
import React from "react";
import PropTypes from "prop-types";
import Product from "./Product";
const ProductItem = ({ product}) => (
<div style={{ marginBottom: 20 }}>
<Product
title={product.title}
price={product.price}
quantity={product.inventory}
/>
</div>
);
ProductItem.propTypes = {
product: PropTypes.shape({
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
}).isRequired,
};
export default ProductItem;
它接收產(chǎn)品對象作為參數(shù)恰梢,并且將產(chǎn)品內(nèi)的具體信息傳遞給產(chǎn)品組件佛南。
在components文件夾下新建ProductsList.js文件,構(gòu)建產(chǎn)品列表組件删豺。
import React from "react";
import PropTypes from "prop-types";
//傳入了一個(gè)對象
const ProductsList = ({ title, children }) => (
<div>
<h3>{title}</h3>
<div>{children}</div>
</div>
);
ProductsList.propTypes = {
children: PropTypes.node,
title: PropTypes.string.isRequired
};
export default ProductsList;
它將接收我們要展示的標(biāo)題和一些產(chǎn)品條目子組件共虑。
我們將組件劃分為兩類,一類是視圖類展示組件呀页,一類的狀態(tài)管理類容器組建妈拌,有點(diǎn)類似于MVC的感覺。視圖類組件不需要管理狀態(tài)蓬蝶,其中很多都是React函數(shù)式組件尘分。在完成了展示組件后,我們需要一個(gè)容器組件丸氛,獲取狀態(tài)變化并傳遞給視圖組件培愁。
在containers文件夾下新建ProductsContainer.js文件,創(chuàng)建產(chǎn)品信息容器組件缓窜。
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { getVisibleProducts } from "../reducers/products";
import ProductItem from "../components/ProductItem";
import ProductsList from "../components/ProductsList";
const ProductsContainer = ({ products }) => (
<ProductsList title={"產(chǎn)品"}>
{products.map(product => (
<ProductItem
key={product.id}
product={product}
/>
))}
</ProductsList>
);
ProductsContainer.propTypes = {
products: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
})
).isRequired
};
//容器組件需要和狀態(tài)映射起來
const mapStateToProps = state => ({
products: getVisibleProducts(state.products)
});
export default connect(
mapStateToProps
)(ProductsContainer);
我們將容器組件和state映射起來定续,這樣一旦state變化,容器組件就能感知到禾锤,對視圖展示進(jìn)行重新渲染私股。
當(dāng)然還需要一個(gè)根容器將我們?nèi)康钠渌萜鞫佳b起來。
在containers文件下新建一個(gè)App.js文件
import React from 'react'
import ProductsContainer from './ProductsContainer'
const App = () => (
<div>
<h2>Shopping Cart Example</h2>
<hr/>
<ProductsContainer />
<hr/>
</div>
)
export default App
App組件是最大的組件恩掷,決定了我們應(yīng)用的主體展示結(jié)構(gòu)倡鲸。
現(xiàn)在修改項(xiàng)目根路徑下的index.js文件,讓我們創(chuàng)建集中管理state的store,獲取產(chǎn)品數(shù)據(jù)黄娘,并通過Provider將state注入到容器組件中峭状。值得注意的是我們獲取產(chǎn)品的動(dòng)作是一個(gè)異步操作,因此需要引入redux-thunk中間件來解決異步問題逼争。
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import reducer from './reducers'
import { getAllProducts } from './actions'
import App from './containers/App'
const middleware = [ thunk ];
const store = createStore(
reducer,
applyMiddleware(...middleware)
)
store.dispatch(getAllProducts())
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
現(xiàn)在我們就可以看到產(chǎn)品的展示信息了优床。效果如下圖所示:
添加到購物車按鈕
現(xiàn)在我們僅僅是把產(chǎn)品信息展示出來了,還沒有加上添加到購物車的按鈕∈慕梗現(xiàn)在就讓我們來添加上這一按鈕吧羔巢。
點(diǎn)擊這一按鈕,會(huì)觸發(fā)一個(gè)將商品添加到購物車的動(dòng)作。那么我們先來定義一下這個(gè)動(dòng)作類別吧竿秆!
在constants/ActionTypes.js文件下添加新定義的動(dòng)作類型
export const ADD_TO_CART = "ADD_TO_CART";
緊接著启摄,我們來定義動(dòng)作。在actions/index.js文件里做一些修改,現(xiàn)在它變成這樣了幽钢。
import shop from "../api/shop";
import * as types from "../constants/ActionTypes";
import { push } from "react-router-redux";
/**
* 這是在接收到產(chǎn)品數(shù)據(jù)后發(fā)送的動(dòng)作
* @param {產(chǎn)品JSON} products 參數(shù)是json
*/
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS,
products
});
export const getAllProducts = () => dispatch => {
shop.getProduct(products => {
dispatch(receiveProducts(products));
});
};
const addToCartUnsafe = productId => ({
type: types.ADD_TO_CART,
productId
});
export const addToCart = productId => (dispatch, getState) => {
if (getState().products.productsOrangedById[productId].inventory > 0) {
dispatch(addToCartUnsafe(productId));
}
};
通過addToCart函數(shù)我們可以對產(chǎn)品的庫存量做一個(gè)判斷歉备,只有庫存量大于0才可以向我們的reducer發(fā)送action來修改state。修改我們的reducers/products.js文件定義處理新action的reducer匪燕。
import { RECEIVE_PRODUCTS, ADD_TO_CART } from "../constants/ActionTypes";
import { combineReducers } from "redux";
const reduceProducts = (state, action) => {
switch (action.type) {
case ADD_TO_CART:
return {
...state,
inventory: state.inventory - 1
};
default:
return state;
}
};
/**
* 把產(chǎn)品數(shù)據(jù)以normalized化的形式組織蕾羊,即通過id對應(yīng)一個(gè)產(chǎn)品(1)
* 通過reduce的方式來實(shí)現(xiàn)
* @param {*} state
* @param {*} action
*/
const productsOrangedById = (state = {}, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product;
return obj;
}, {})
};
//通過id更新state里的產(chǎn)品數(shù)量
default:
const { productId } = action;
if (productId) {
return {
...state,
[productId]: reduceProducts(state[productId], action)
};
}
//action里沒有產(chǎn)品id則直接返回
return state;
}
};
//把產(chǎn)品數(shù)據(jù)以normalized化的形式組織,記錄所有的id值(2)
//map 把一個(gè)數(shù)組映射成一個(gè)新數(shù)組
//map() 原數(shù)組中的每個(gè)元素調(diào)用這個(gè)方法后返回值組成的新數(shù)組
const visibleIds = (state = [], action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
console.log("bbbbbb");
return action.products.map(product => product.id);
default:
return state;
}
};
//組合成state
/**
* 結(jié)構(gòu)應(yīng)該是這樣的
* {
* visibleIds:[productId1,productId2],
* productsOrangedById:{
* productId1:{
* 屬性:值
* }
* productId2:{
* 屬性:值
* }
* }
* }
*/
export default combineReducers({
productsOrangedById,
visibleIds
});
//獲取state中的值的方式
/**
* 直接獲取state中的某個(gè)產(chǎn)品的方式
* */
export const getProduct = (state, id) => state.productsOrangedById[id];
/**
* 獲取產(chǎn)品數(shù)組的方式
* @param {*} state
*/
export const getVisibleProducts = state =>
state.visibleIds.map(id => getProduct(state, id));
修改了action和reducer帽驯,我們接下來可以修改組件了龟再。修改components/ProductItems.js文件添加一個(gè)按鈕。
import React from "react";
import PropTypes from "prop-types";
import Product from "./Product";
const ProductItem = ({ product, onAddToCartClick }) => (
<div style={{ marginBottom: 20 }}>
<Product
title={product.title}
price={product.price}
quantity={product.inventory}
/>
<button
onClick={onAddToCartClick}
disabled={product.inventory > 0 ? "" : "disabled"}
>
{product.inventory > 0 ? "Add to cart" : "Sold Out"}
</button>
</div>
);
ProductItem.propTypes = {
product: PropTypes.shape({
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
}).isRequired,
onAddToCartClick: PropTypes.func.isRequired
};
export default ProductItem;
ProductItem中的按鈕點(diǎn)擊事件函數(shù)依賴于容器組件傳遞進(jìn)來尼变,接下來我們修改一下容器組件利凑。把點(diǎn)擊事件傳遞進(jìn)來。
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { addToCart } from "../actions";
import { getVisibleProducts } from "../reducers/products";
import ProductItem from "../components/ProductItem";
import ProductsList from "../components/ProductsList";
const ProductsContainer = ({ products, addToCart }) => (
<ProductsList title={"產(chǎn)品"}>
{products.map(product => (
<ProductItem
key={product.id}
product={product}
onAddToCartClick={() => addToCart(product.id)}
/>
))}
</ProductsList>
);
ProductsContainer.propTypes = {
products: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
inventory: PropTypes.number.isRequired
})
).isRequired
};
//容器組件需要和狀態(tài)映射起來
const mapStateToProps = state => ({
products: getVisibleProducts(state.products)
});
export default connect(
mapStateToProps,
{ addToCart }
)(ProductsContainer);
通過connect()函數(shù)將addToCart動(dòng)作作為組件的props的一部分綁定到組件上嫌术。好啦哀澈,這樣我們就實(shí)現(xiàn)了這個(gè)按鈕。讓我們來看看效果度气。
如果庫存為0了按鈕就會(huì)被禁用割按,點(diǎn)擊按鈕一次展示的產(chǎn)品的數(shù)量也會(huì)減少一個(gè)。
展示購物車信息
產(chǎn)品模塊的展示基本就完成啦磷籍,現(xiàn)在讓我們來實(shí)現(xiàn)一下購物車模塊吧适荣!
首先還是先考慮下購物車模塊有哪些會(huì)更新state的動(dòng)作。
- 產(chǎn)品添加到購物車這個(gè)動(dòng)作顯然也會(huì)觸發(fā)購物車信息的更新
- 總價(jià)會(huì)變化院领,不過這依賴于上面的這個(gè)動(dòng)作
首先我們在actions/index.js內(nèi)添加一下結(jié)算按鈕的點(diǎn)擊動(dòng)作事件束凑,我們意圖做路由跳轉(zhuǎn),但是目前我們還不跳轉(zhuǎn)栅盲。于是先寫一個(gè)空的函數(shù)體。
export const checkout = () => dispatch => {
};
添加到購物車這個(gè)動(dòng)作在之前我們已經(jīng)定義過了废恋,不必重復(fù)定義了谈秫。我們直接開始設(shè)計(jì)接收這個(gè)action來變更state的reducer。在reducers文件夾下鱼鼓,新建cart.js文件拟烫。
import {
ADD_TO_CART
} from "../constants/ActionTypes";
const initialState = {
addedIds: [],
quantityById: {},
};
const addedIds = (state = initialState.addedIds, action) => {
switch (action.type) {
case ADD_TO_CART:
if (state.indexOf(action.productId) !== -1) {
return state;
}
return [...state, action.productId];
default:
return state;
}
};
const quantityById = (state = initialState.quantityById, action) => {
switch (action.type) {
case ADD_TO_CART:
const { productId } = action;
return {
...state,
[productId]: (state[productId] || 0) + 1
};
default:
return state;
}
};
export const getQuantity = (state, productId) =>
state.quantityById[productId] || 0;
export const getAddedIds = state => state.addedIds;
const cart = (state = initialState, action) => {
switch (action.type) {
default:
return {
addedIds: addedIds(state.addedIds, action),
quantityById: quantityById(state.quantityById, action)
};
}
};
export default cart;
同樣的,購物車模塊的state也以normalized化的形式進(jìn)行組織迄本。分成了id和實(shí)體集兩部分 id對應(yīng)了實(shí)體集合硕淑。
我們需要把它和產(chǎn)品模塊的state整合一下。修改我們r(jià)educers/index.js文件。順便提供一些獲取state內(nèi)容的方法置媳。
import { combineReducers } from "redux";
import cart, * as fromCart from "./cart";
import products, * as fromProducts from "./products";
export default combineReducers({
cart,
products,
});
const getAddedIds = state => fromCart.getAddedIds(state.cart);
const getQuantity = (state, id) => fromCart.getQuantity(state.cart, id);
const getProduct = (state, id) => fromProducts.getProduct(state.products, id);
export const getTotal = state =>
getAddedIds(state)
.reduce(
(total, id) =>
total + getProduct(state, id).price * getQuantity(state, id),
0
)
.toFixed(2);
export const getCartProducts = state =>
getAddedIds(state).map(id => ({
...getProduct(state, id),
quantity: getQuantity(state, id)
}));
接下來我們開始編寫購物車模塊的視圖組件于樟。
在components文件夾下新建Cart.js文件,得益于購物車模塊內(nèi)有一部分組件和產(chǎn)品模塊內(nèi)相似拇囊,因此我們可以復(fù)用一下迂曲,這也是盡量把組件做小的原因之一,可以便于以后復(fù)用寥袭。
import React from "react";
import PropTypes from "prop-types";
import Product from "./Product";
const Cart = ({ products, total, onCheckoutClicked }) => {
const hasProducts = products.length > 0;
const nodes = hasProducts ? (
products.map(product => (
<Product
title={product.title}
price={product.price}
quantity={product.quantity}
key={product.id}
/>
))
) : (
<em>Please add some products to cart.</em>
);
return (
<div>
<h3>Your Cart</h3>
<div>{nodes}</div>
<p>Total:${total}</p>
<button
onClick={onCheckoutClicked}
disabled={hasProducts ? "" : "disabled"}
>
Checkout
</button>
</div>
);
};
Cart.PropTypes = {
products: PropTypes.array,
total: PropTypes.string,
onCheckoutClicked: PropTypes.func
};
export default Cart;
編寫完視圖組件路捧,就可以開始編寫容器組件了。在Containers文件夾下新建CartContainer.js文件
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { checkout } from "../actions";
import { getTotal, getCartProducts } from "../reducers";
import Cart from "../components/Cart";
const CartContainer = ({ products, total, checkout }) => (
<Cart products={products} total={total} onCheckoutClicked={checkout} />
);
CartContainer.PropTypes = {
products: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
quantity: PropTypes.number.isRequired
})
).isRequired,
total: PropTypes.string,
checkout: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
products: getCartProducts(state),
total: getTotal(state)
});
export default connect(
mapStateToProps,
{ checkout }
)(CartContainer);
最后在整個(gè)大的App容器內(nèi)加上我們的購物車容器传黄。修改Containers/App.js
import React from "react";
import ProductsContainer from "./ProductsContainer";
import CartContainer from "./CartContainer";
const App = () => (
<div>
<h2>Shopping Cart Example</h2>
<hr />
<ProductsContainer />
<hr />
<CartContainer />
</div>
);
export default App;
我們的購物車模塊也就完成了杰扫。
效果如下:
結(jié)果頁
雖然實(shí)現(xiàn)了購物車模塊的展示,但是還沒有實(shí)現(xiàn)點(diǎn)擊checkout按鈕的功能膘掰。不過不著急章姓,我們先來開發(fā)結(jié)果頁好了,最后再把它們聯(lián)系起來炭序。
思考一下結(jié)果頁會(huì)有哪些更新state的動(dòng)作呢啤覆?
- 會(huì)有一個(gè)支付按鈕,點(diǎn)擊支付會(huì)把我們的購物車狀態(tài)置為已支付惭聂,如果支付成功則還會(huì)清空我們的購物車內(nèi)容窗声。
-還會(huì)有一個(gè)返回按鈕,返回時(shí)會(huì)重置我們的購物車支付狀態(tài)辜纲。
依據(jù)這些動(dòng)作笨觅,我們來定義下動(dòng)作類型。
//支付
export const CHECKOUT_SUCCESS = "CHECKOUT_SUCCESS";
export const CHECKOUT_FAILURE = "CHECKOUT_FAILURE";
//清空支付狀態(tài)
export const CLEAR_PAYMENT_STATUS = "CLEAR_PAYMENT_STATUS";
點(diǎn)擊付款按鈕耕腾,我們要模擬一個(gè)發(fā)起請求到接收響應(yīng)的過程见剩,我們定義一個(gè)延時(shí)函數(shù)來模擬一下,修改下api/shop.js文件扫俺。在返回對象里添加一個(gè)buyProducts的函數(shù)苍苞。
import _products from "./product.json";
const TIME_OUT = 2000;
/**
* cb 是個(gè)函數(shù)參數(shù),延遲多少ms后獲取產(chǎn)品信息
*/
export default {
getProduct: (cb, timeout) => setTimeout(cb(_products), timeout || TIME_OUT),
buyProducts: (payload, cb, timeout) =>
setTimeout(() => cb(), timeout || TIME_OUT)
};
現(xiàn)在開始編寫我們的動(dòng)作。修改actions/index.js文件添加幾個(gè)函數(shù)狼纬。
export const payForSomething = products => (dispatch, getState) => {
const { cart } = getState();
shop.buyProducts(products, () => {
const payResult = Math.random() > 0.495;
if (payResult) {
dispatch({
type: types.CHECKOUT_SUCCESS,
cart
});
} else {
dispatch({
type: types.CHECKOUT_FAILURE,
cart
});
}
});
};
//回到主頁清空付款狀態(tài)
export const backToHomePage = () => (dispatch, getState) => {
const { cart } = getState();
dispatch({
type: types.CLEAR_PAYMENT_STATUS,
cart
});
};
定義了一個(gè)隨機(jī)事件來模擬隨機(jī)支付成功和支付失敗羹呵。
接下來定義處理state的reducer,修改reduces/cart.js,并且為cart添加一個(gè)支付狀態(tài)疗琉。
export const getPaymentStatus = state => state.paid;
const cart = (state = initialState, action) => {
switch (action.type) {
case CHECKOUT_SUCCESS:
return {
...initialState,
paid:true
};
case CHECKOUT_FAILURE:
return {
...action.cart,
paid:true
};
case CLEAR_PAYMENT_STATUS:
return{
...action.cart,
paid:false
}
default:
return {
addedIds: addedIds(state.addedIds, action),
quantityById: quantityById(state.quantityById, action)
};
}
};
再修改reducers/index.js文件添加一個(gè)獲取支付狀態(tài)的函數(shù)冈欢。
export const getPaymentStatus = state => fromCart.getPaymentStatus(state.cart);
現(xiàn)在開始編寫結(jié)果頁的視圖組件。結(jié)果頁有確認(rèn)模塊和支付結(jié)果模塊兩個(gè)部分組成盈简。先來編寫確認(rèn)模塊凑耻。在components文件夾下新建ConfirmPage.js文件太示。
import React from "react";
import PropTypes from "prop-types";
const ConfirmPage = ({ total, payClick }) => (
<div>
<h3>Please confirm</h3>
<em>You will spend ${total} for this.</em>
<hr />
<button onClick={payClick}>Pay for it !</button>
</div>
);
ConfirmPage.propTypes = {
total: PropTypes.string,
payClick: PropTypes.func
};
export default ConfirmPage;
接下來編寫支付結(jié)果模塊。在components文件夾下新建ResultPage.js文件香浩。
import React from "react";
import PropTypes from "prop-types";
import * as constants from "../constants/Constants";
const ResultPage = ({ paymentResult, backClick }) => (
<div>
<h3>Payment Result</h3>
<p>
{paymentResult === constants.PAID_SUCCESS
? "Congratulations!"
: "Ops! Payment fail"}
</p>
<hr />
<button onClick={backClick}>back</button>
</div>
);
ResultPage.propTypes = {
paymentResult: PropTypes.bool,
backClick: PropTypes.func
};
export default ResultPage;
最后定義一個(gè)容器組件类缤,來把結(jié)果頁的所有部分整合起來。在Containers文件夾下新建ResultContainer.js文件弃衍。
import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import ConfirmPage from "../components/ConfirmPage";
import ResultPage from "../components/ResultPage";
import { getTotal, getPaymentStatus, getCartProducts } from "../reducers";
import { backToHomePage, payForSomething } from "../actions";
import * as constants from "../constants/Constants";
const ResultContainer = ({
products,
total,
paid,
payForSomething,
backToHomePage
}) => {
const hasProducts = products.length > 0;
return (
<div>
{paid !== constants.PAID ? (
<ConfirmPage total={total} payClick={payForSomething} />
) : null}
{paid === constants.PAID ? (
<ResultPage paymentResult={!hasProducts} backClick={backToHomePage} />
) : null}
</div>
);
};
ResultContainer.propTypes = {
products: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
quantity: PropTypes.number.isRequired
})
),
total: PropTypes.string,
paid: PropTypes.bool,
payForSomething: PropTypes.func,
backToHomePage: PropTypes.func
};
const mapStateToProps = state => ({
products: getCartProducts(state),
total: getTotal(state),
paid: getPaymentStatus(state)
});
export default connect(
mapStateToProps,
{ backToHomePage, payForSomething }
)(ResultContainer);
至此呀非,我們的結(jié)果頁面也開發(fā)完成了,接下來我們就要開始制作路由镜盯,讓頁面聯(lián)系起來岸裙。
路由跳轉(zhuǎn)
在進(jìn)行路由開發(fā)前,我們需要對React-Router有一點(diǎn)點(diǎn)了解速缆。請參閱React-Router的官方文檔以了解一些背景知識降允。
點(diǎn)擊主頁面上的checkout按鈕可以跳轉(zhuǎn)到我們的結(jié)果頁。
點(diǎn)擊結(jié)果頁上的back按鈕可以返回主頁面艺糜。
首先需要引入路由組件 我們這兒使用browserHistory,使得路由更像一個(gè)App應(yīng)用程序的路由剧董。修改根目錄下的index.js文件。
import React from "react";
import ReactDOM from "react-dom";
import thunk from "redux-thunk";
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducers";
import { getAllProducts } from "./actions";
import Root from "./containers/Root";
import createBrowserHistory from "history/createBrowserHistory";
import { syncHistoryWithStore, routerMiddleware } from "react-router-redux";
import "./styles.css";
let browserHistory = createBrowserHistory();
const browserHistoryMiddleware = routerMiddleware(browserHistory);
const middleware = [thunk, browserHistoryMiddleware];
const store = createStore(reducer, applyMiddleware(...middleware));
const history = syncHistoryWithStore(browserHistory, store);
store.dispatch(getAllProducts());
console.log(store.getState());
const rootElement = document.getElementById("root");
ReactDOM.render(<Root store={store} history={history} />, rootElement);
在這里我們新建了一個(gè)全新的容器Root將原來的App容器再包裹了一層破停。在containers文件夾下新建Root.js文件翅楼。
import React from "react";
import PropTypes from "prop-types";
import { Provider } from "react-redux";
import { Router, Route } from "react-router";
import App from "./App";
import ResultContainer from "./ResultContainer";
const Root = ({ store, history }) => (
<Provider store={store}>
<Router history={history}>
<Route exact path="/" component={App} />
<Route path="/result" component={ResultContainer} />
</Router>
</Provider>
);
Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default Root;
這里使用Route的exact屬性,使得對根路由的匹配更精準(zhǔn)真慢。
引入browserHistory后我們還要在state中加入routerReducer來處理路由狀態(tài)變更毅臊。
修改reducers/index.js文件。添加以下內(nèi)容黑界。
import { routerReducer } from "react-router-redux";
export default combineReducers({
cart,
products,
routing: routerReducer
});
現(xiàn)在已經(jīng)引入了路由組件了管嬉,離實(shí)現(xiàn)路由跳轉(zhuǎn)只剩一步之遙了。
我們修改actions/index.js為某些動(dòng)作添加路由跳轉(zhuǎn)動(dòng)作事件朗鸠。修改如下的兩個(gè)函數(shù)蚯撩。
//跳轉(zhuǎn)到結(jié)果頁。
export const checkout = () => dispatch => {
dispatch(push("/result"));
};
//回到主頁清空付款狀態(tài)
export const backToHomePage = () => (dispatch, getState) => {
const { cart } = getState();
dispatch({
type: types.CLEAR_PAYMENT_STATUS,
cart
});
dispatch(push("/"));
};
好了烛占,至此我們就實(shí)現(xiàn)了整個(gè)應(yīng)用了胎挎。
寫在最后
這個(gè)小小的示例項(xiàng)目非常簡單,但是包含了日常前端開發(fā)會(huì)涉及的數(shù)個(gè)方面忆家。本文旨在幫助剛剛接觸React和Redux的新手迅速上手React和Redux進(jìn)行開發(fā)實(shí)踐犹菇,故只分享了一些開發(fā)過程中的常用方法,沒有涉及到一些高級用法弦赖。如果你想了解更多,請參閱官方的API使用說明浦辨,里面有詳盡的使用示例和原理講解蹬竖。
稍后我可能會(huì)將源碼上傳到GitHub沼沈,屆時(shí)會(huì)將鏈接貼在下方,請耐心等待币厕。如果你有任何疑問和建議列另,歡迎評論和私信我。Happy Coding! :)