從零開始搭建第一個(gè)React+Redux應(yīng)用

  • 文中的藍(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ù)備知識

  1. 熟悉 ES6 相關(guān)知識
  2. 了解 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é)果頁面缭贡,如下所示


支付確認(rèn)

點(diǎn)擊Pay for it !按鈕,會(huì)進(jìn)行支付辉懒。


支付成功

支付失敗

點(diǎn)擊back 按鈕后阳惹,又將跳轉(zhuǎn)回到我們的應(yīng)用首頁,如下所示眶俩。注意觀察莹汤,我們商品數(shù)量減少了!


Wow!購物車被清空了

現(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)品信息組件拆分

產(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ù)。


項(xiàng)目目錄結(jié)構(gòu)

項(xiàng)目依賴

我們在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} - &#36;{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)品的展示信息了优床。效果如下圖所示:


產(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:&#36;{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 &#36;{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! :)

?著作權(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)容