八、电子商务应用

网上购物是大多数零售商都采用的方式,但用户正慢慢地从网站迁移到移动应用。这就是为什么电子商务将重点放在响应迅速的网站上,这些网站可以通过台式计算机或移动浏览器无缝访问。除此之外,用户还要求更高的质量标准,即使是响应速度最快的网站也无法始终满足这一要求。加载时间、滞后的动画、非本机组件或缺少本机功能可能会影响用户体验,从而导致低转换率。

在 React Native 中构建我们的电子商务应用可以减少所需的开发工作,因为可以重用一些已经为 web 设计的 web 组件(使用 React.js)。此外,我们还可以减少上市时间和开发成本,使 React Native 成为愿意在线销售其产品或服务的中小型企业非常有吸引力的工具。

在本章中,我们将重点介绍如何为 iOS 和 Android 构建一个 100%重用代码的书店。尽管专注于书店,但只要替换产品列表,相同的代码库就可以被重用来销售任何类型的产品

为了使我们免于为此应用构建 API,我们将模拟假 API 服务背后的所有数据。我们将用于此应用的状态管理库是 Redux 及其中间件redux-thunk,用于处理异步调用。

Asynchronous calls and redux-thunk were already explained in Chapter 4, Image Sharing App. It may be useful to review its usage in that chapter to reinforce the main concepts before moving into the Actions sections in this chapter.

导航将由react-navigation处理,因为它是迄今为止 React Native 开发的最完整和性能最好的导航库。最后,我们将使用一些非常有用的库,特别是用于电子商务应用,例如处理信用卡输入的react-native-credit-card-input

在构建该应用时,我们将强调几个质量方面,以确保该应用在本章结束时已准备就绪。例如,我们将对属性和代码 linting 广泛使用类型验证。

概述

与我们在前几章中所做的一样,我们不会在应用的外观和感觉上投入太多精力,而是将重点放在这个应用的功能和代码质量上。尽管如此,我们将以一种允许任何开发人员在稍后阶段轻松设置样式的方式构建它。考虑到这一点,让我们来看看应用将是什么样子,一旦完成。

让我们从显示所有书籍的主屏幕开始:

在 Android 中,我们将添加抽屉导航模式,而不是选项卡式模式,因为 Android 用户更习惯于此:

从左边缘向右滑动屏幕可打开抽屉:

现在,让我们看看当用户从主屏幕(可用书籍列表)点击其中一本书时会发生什么:

此屏幕的 Android 版本将类似,因为只有两个本机组件将采用不同的样式,具体取决于应用的执行平台:

只有登录的用户才能从我们的应用购买书籍。这意味着我们需要在某一点弹出一个登录/注册屏幕,然后点击购买!按钮似乎是一个合适的时机:

在这种情况下,由于每个平台上本机按钮的样式不同,Android 版本看起来与 iOS 不同:

出于测试目的,我们使用以下凭据在此应用中创建了一个测试帐户:

  • 电子邮件:test@test.com
  • 密码:test

如果用户仍然没有帐户,她可以单击或注册按钮创建一个帐户:

此表格将包括以下验证:

  • 电子邮件和重复电子邮件字段值匹配
  • 所有字段都已输入

如果这些验证失败,我们将在此屏幕底部显示一条错误消息:

注册后,用户将自动登录,并可以通过查看购物车继续她的购买旅程:

同样,Android 版本将在该屏幕的外观上显示细微差异:

通过单击此屏幕上的“继续购买”按钮,用户将返回主屏幕,在主屏幕上显示所有可用的书籍,以便她继续向购物车添加项目。

如果她决定确认购买,应用将显示一个付款屏幕,用户可以在其中输入她的信用卡详细信息:

只有在正确输入所有数据后,“立即付款”按钮才会激活:

出于测试目的,开发商可以使用以下信用卡数据:

  • 卡号:4111 1111 1111 1111
  • 到期日期:将来的任何日期
  • CVC/CVV:123

付款完成后,用户将收到一份购买确认书,详细说明将发送至其地址的所有项目:

此屏幕将完成购买过程。在此阶段,用户可以单击“继续购物”按钮返回可用产品列表。

通过选项卡式/抽屉式导航还有两次行程。第一个是到“我的个人资料”部分查看她的帐户详细信息或注销:

如果用户仍然没有登录,应用将在此屏幕上显示登录/注册表单。

通过 Sales(销售)选项卡/菜单项访问最后一次旅程:

通过按 Add to cart,用户将直接被发送到购买旅程,在那里她可以向购物车添加更多项目,或者通过输入登录名(如果不存在)和付款详细信息直接确认购买。

最后,每次我们需要从后端 API 接收数据时,我们都会显示一个微调器,让用户知道后台正在进行一些活动:

因为我们将模拟所有的 API 调用,所以我们需要给它们的响应添加一个小的延迟,以便看到微调器,这样当我们为真实的 API 请求替换模拟调用时,开发人员可以获得与用户类似的体验。

设置文件夹结构

此应用将使用 Redux 作为其状态管理库,它将定义我们将在本章中使用的文件夹结构。让我们首先通过 React Native 的 CLI 初始化项目:

react-native init --version="0.48.3" ecommerce 

正如我们在前面使用 Redux 的章节中所看到的,我们需要文件夹结构来适应不同的模块类型:reducersactionscomponentsscreensapi调用。我们将在以下文件夹结构中执行此操作:

除了 React Native 的 CLI 创建的文件夹结构外,我们还添加了以下文件夹和文件:

  • src/components:这将保存可重用的可视组件。
  • src/reducers:这将存储减速器,减速器通过检测触发了哪些动作来修改应用的状态。
  • src/screens:这将存储通过 Redux 将它们连接到应用状态的所有不同视觉容器。
  • src/api.js:到本章结束时,我们将在该文件中模拟所有必需的 API 调用。如果我们想连接到一个真正的 API,我们只需要更改这个文件,向适当的端点发出 HTTP 请求。
  • src/main.js:这是应用的入口点,将设置导航组件并初始化应用状态所在的存储。

src/components文件夹将包含以下文件:

src/reducers将在我们的应用中包含三个不同的数据域:用户、支付和产品:

最后,screens文件夹将为用户能够在应用中看到的每个屏幕存储一个文件:

现在让我们来看看我们将要为这个应用安装所有必需的库的

/*** package.json ***/

{
  "name": "ecommerce",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest",
    "ios": "react-native run-ios",
    "android": "react-native run-android"
  },
  "dependencies": {
 "native-base": "^2.3.1",
 "prop-types": "^15.5.10",
    "react": "16.0.0-alpha.12",
    "react-native": "0.48.3",
 "react-native-credit-card-input": "^0.3.3",
 "react-navigation": "^1.0.0-beta.11",
 "react-redux": "^5.0.6",
 "redux": "^3.7.2",
 "redux-thunk": "^2.2.0"
  },
  "devDependencies": {
 "babel-eslint": "^7.2.3",
    "babel-jest": "20.0.3",
    "babel-plugin-lodash": "^3.2.11",
    "babel-plugin-module-resolver": "^2.7.1",
    "babel-plugin-transform-builtin-extend": "^1.1.2",
    "babel-plugin-transform-react-jsx-source": "^6.22.0",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.6.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react-native": "2.0.0",
    "babel-preset-stage-0": "^6.24.1",
 "eslint-config-airbnb": "^15.1.0",
    "eslint-config-prettier": "^2.3.0",
    "eslint-config-rallycoding": "^3.2.0",
    "eslint-import-resolver-babel-module": "^3.0.0",
    "eslint-import-resolver-webpack": "^0.8.3",
    "eslint-plugin-flowtype": "^2.35.0",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-jsx-a11y": "^5.1.1",
    "eslint-plugin-prettier": "^2.1.2",
    "eslint-plugin-react": "^7.2.0",
    "eslint-plugin-react-native": "^3.0.1",
    "jest": "20.0.4",
 "prettier": "^1.5.3",
    "prettier-package-json": "^1.4.0",
    "react-test-renderer": "16.0.0-alpha.12"
  },
  "jest": {
    "preset": "react-native"
  }
}

我们将为我们的应用使用以下额外库:

  • native-base:这是针对样式化组件的。
  • prop-types:用于组件内部的属性验证。
  • react-native-credit-card-input:供用户输入信用卡详细信息。
  • react-redux:此和 Redux 用于状态管理。
  • redux-thunk:用于将 Redux 连接到异步调用。

除了所有这些依赖项之外,我们还将添加一些其他dev依赖项,这将帮助我们的开发人员以一种非常舒适和自信的方式编写代码:

  • babel-eslint:这是用来删除我们的 ES6 代码。
  • eslint-config-airbnb:这是我们将使用的一组编码样式。
  • prettier:这是我们将用于支持 ES6 和 JSX 的代码格式化程序。

有了这个package.json之后,我们可以通过运行以下命令来安装所有这些依赖项:

npm install

在开始编写代码之前,让我们配置 linting 规则和文本编辑器,以充分利用我们将在本章中使用的代码格式化工具。

Linting 和代码格式化

编写干净、无 bug 的代码是一项挑战。我们可能会面临很多陷阱,例如缩进、导入/导出未命中、标记未关闭等等。必须手动克服所有这些困难是一项艰巨的工作,这会分散我们对主要目的的注意力:编写函数代码。幸运的是,有一些非常有用的工具可以帮助我们完成这项任务。

在本章中,我们将使用 ESLint(工具来确保我们的代码是干净的 https://eslint.org/ 和更漂亮的(https://github.com/prettier/prettier

ESLint 将负责识别和报告在 ES6/JavaScript 代码中发现的模式,目标是使代码更加一致并避免 bug。例如,ESLint 将标记未声明变量的任何使用,在编写代码时公开错误,而不是等到编译。

另一方面,Prettier 在整个代码库中强制执行一致的代码样式,因为它忽略了原始样式,将其解析掉,并使用考虑到最大行长度的自己的规则重新打印,必要时包装代码。

我们还可以使用 ESLint 直接在浏览器中强制使用更漂亮的代码样式。我们的第一步将是配置 ESLint 以适应我们希望在项目中强制执行的格式和 linting 规则。在这个应用的情况下,我们将遵循 Airbnb 和 Prettier 的规则,因为我们已经在这个项目中作为开发人员的依赖项安装了它们。

为了确保 ESLint 将使用这些规则,我们将创建一个.eslintrc文件,其中包含我们在 linting 时要设置的所有选项:

/*** .eslintrc ***/

{
  "extends": ["airbnb", "prettier", "prettier/react", "prettier/flowtype"],
  "globals": {
    "queryTree": false
  },
 "plugins": ["react", "react-native", "flowtype", "prettier"],
  "env": { "es6": true, "jest": true },
  "parser": "babel-eslint",
  "rules": {
    "prettier/prettier": [
 "error",
 {
 "trailingComma": "all",
 "singleQuote": true,
 "bracketSpacing": true,
 "tabWidth": 2
 }
 ],

    ...

}

在这本书中,我们将不深入探讨如何配置 ESLint,因为他们的文档非常广泛,并且解释得很好。对于这个项目,我们只需要在配置文件中设置相应的插件(reactreact-nativeflowtypeprettier时扩展 Airbnb 和 Prettier 的规则

为短绒设置规则是一个品味问题,如果没有太多的经验,最好从一组预先构建的规则(如 Airbnb 规则)开始,然后一次修改一个规则。

最后,我们需要配置代码编辑器来显示这些规则,标记它们,并在理想情况下在保存时修复它们。Visual Studio 代码在将这些 linting/代码格式规则集成为其 ESLint 插件(方面做得非常好 https://github.com/Microsoft/vscode-eslint 为我们做所有的工作。强烈建议启用eslint.autoFixOnSave选项,以确保编辑器在保存我们正在处理的文件后修复所有代码格式问题。

现在我们已经准备好了 linting 工具,让我们开始编写应用的代码库。

索引和主文件

iOS 和 Android 平台将使用src/main.js作为入口点共享相同的代码库。因此,我们将index.ios.jsindex.android.js更改为导入main.js并将该组件作为根来初始化应用:

/*** index.ios.js and index.android.js ***/ 

import { AppRegistry } from 'react-native';
import App from './src/main';

AppRegistry.registerComponent('ecommerce', () => App);

这与本书中所有共享代码库的应用使用的结构相同。我们的main.js文件现在应该初始化导航组件,并设置用于保存应用状态的存储:

/*** src/main.js ***/

import React from 'react';
import {
  DrawerNavigator,
  TabNavigator,
  StackNavigator,
} from 'react-navigation';
import { Platform } from 'react-native';

import { Provider } from 'react-redux';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import paymentsReducer from './reducers/payments';
import productsReducer from './reducers/products';
import userReducer from './reducers/user';

import ProductList from './screens/ProductList';
import ProductDetail from './screens/ProductDetail';
import MyCart from './screens/MyCart';
import MyProfile from './screens/MyProfile';
import Payment from './screens/Payment';
import PaymentConfirmation from './screens/PaymentConfirmation';
import Sales from './screens/Sales';

const ProductsNavigator = StackNavigator({
 ProductList: { screen: ProductList },
 ProductDetail: { screen: ProductDetail },
});

const PurchaseNavigator = StackNavigator({
 MyCart: { screen: MyCart },
 Payment: { screen: Payment },
 PaymentConfirmation: { screen: PaymentConfirmation },
});

let Navigator;
if (Platform.OS === 'ios') {
 Navigator = TabNavigator(
 {
 Home: { screen: ProductsNavigator },
 MyCart: { screen: PurchaseNavigator },
 MyProfile: { screen: MyProfile },
 Sales: { screen: Sales },
 },
 {
 tabBarOptions: {
 inactiveTintColor: '#aaa',
 activeTintColor: '#000',
 showLabel: true,
 },
 },
 );
} else {
 Navigator = DrawerNavigator({
 Home: { screen: ProductsNavigator },
 MyCart: { screen: MyCart },
 MyProfile: { screen: MyProfile },
 Sales: { screen: Sales },
 });
}

const store = createStore(
 combineReducers({ paymentsReducer, productsReducer, userReducer }),
 applyMiddleware(thunk),
);

export default () => (
  <Provider store={store}>
 <Navigator />
  </Provider>
);

我们的主要导航器(Navigator)将是 iOS 上的标签导航器和 Android 上的抽屉导航器。此导航器将是应用的根目录,并将使用两个嵌套的堆叠导航器(ProductsNavigatorPurchaseNavigator,它们将覆盖以下行程:

  • ProductsNavigator:产品清单|产品明细
  • PurchaseNavigator:我的购物车|付款|付款确认

每个旅程中的每一步都是应用中的一个特定屏幕

Login and registration are not steps in those journeys since they will be treated as pop-up screens displaying only if they are needed.

本文件的最后一步是负责设置 Redux,应用所有的还原器和中间件(在我们的例子中只有redux-thunk),这将适用于本项目:

const store = createStore(
  combineReducers({ paymentsReducer, productsReducer, userReducer }),
  applyMiddleware(thunk),
);

创建store后,我们将其传递给应用根目录中的提供商,以确保所有屏幕共享状态。在进入每个单独的屏幕之前,让我们创建简化程序和操作,以便在构建屏幕时可以使用它们。

还原剂

在前几章中,我们按照 Redux 文档中记录的标准方式拆分了特定于 Redux 的代码(还原器、动作和动作创建者)。为了便于将来的维护,我们将为该应用使用不同的方法:Redux Ducks(https://github.com/erikras/ducks-modular-redux )。

Redux Ducks 是一个在使用 Redux 时将还原程序、操作类型和操作捆绑在一起的建议。不是为简化程序和操作创建单独的文件夹,而是根据它们处理的功能类型将它们放在文件中,从而减少了在实现新功能时要处理的文件数量。

让我们从products减速器开始:

/*** src/reducers/products.js ***/

import { get } from '../api';

// Actions
const FETCH = 'products/FETCH';
const FETCH_SUCCESS = 'products/FETCH_SUCCESS';
const FETCH_ERROR = 'products/FETCH_ERROR';
const ADD_TO_CART = 'products/ADD_TO_CART';
const REMOVE_FROM_CART = 'products/REMOVE_FROM_CART';
const RESET_CART = 'products/RESET_CART';

// Reducer
const initialState = {
  loading: false,
  cart: [],
  products: [],
};
export default function reducer(state = initialState, action = {}) {
  let product;
  let i;
  switch (action.type) {
 case FETCH:
      return { ...state, loading: true };
 case FETCH_SUCCESS:
      return {
        ...state,
        products: action.payload.products,
        loading: false,
        error: null,
      };
 case FETCH_ERROR:
      return { ...state, error: action.payload.error, loading: false };
 case ADD_TO_CART:
      product = state.cart.find(p => p.id === 
                action.payload.product.id);
      if (product) {
        product.quantity += 1;
        return {
          ...state,
          cart: state.cart.slice(),
        };
      }
      product = action.payload.product;
      product.quantity = 1;
      return {
        ...state,
        cart: state.cart.slice().concat([action.payload.product]),
      };
 case REMOVE_FROM_CART:
      i = state.cart.findIndex(p => p.id === 
          action.payload.product.id);
      if (state.cart[i].quantity === 1) {
        state.cart.splice(i, 1);
      } else {
        state.cart[i].quantity -= 1;
      }
      return {
        ...state,
        cart: state.cart.slice(),
      };
 case RESET_CART:
      return {
        ...state,
        cart: [],
      };
 default:
      return state;
  }
}

// Action Creators
export function addProductToCart(product) {
  return { type: ADD_TO_CART, payload: { product } };
}

export function removeProductFromCart(product) {
  return { type: REMOVE_FROM_CART, payload: { product } };
}

export function fetchProducts() {
  return dispatch => {
    dispatch({ type: FETCH });
    get('/products')
      .then(products =>
        dispatch({ type: FETCH_SUCCESS, payload: { products } }),
      )
      .catch(error => dispatch({ type: FETCH_ERROR, payload: { error } }));
  };
}

export function resetCart() {
  return { type: RESET_CART };
}

此文件处理应用中与产品相关的所有业务逻辑。让我们回顾一下每个动作创建者,以及它在由 reducer 处理时如何修改状态:

  • addProductToCart():这将发送ADD_TO_CART动作,该动作将由减速器拾取。如果提供的产品已存在于该州的购物车中,则会增加一项数量。否则,它会将产品插入购物车并将其数量设置为 1。
  • removeProductFromCart():此动作与前一动作相反。如果此产品已存在于存储在该状态的购物车中,则会减少此产品的数量。如果此产品的数量为一,则减速机将从购物车中取出该产品。
  • fetchProducts():这是一个异步操作,因此将返回一个函数供redux-thunk拾取。它会向/products端点的 API 发出GET请求(由api.json文件中的get()函数实现)。它还将处理来自该端点的响应,在请求成功完成时发送FETCH_SUCCESS操作,或者在请求出错时发送FETCH_ERROR操作。
  • resetCart():这将发送一个RESET_CART动作,减速器将使用该动作清除状态中的所有小车详细信息。

由于我们遵循 Redux Ducks 的建议,所有这些操作都放在同一个文件中,因此很容易确定操作的作用以及它们在应用状态下造成的影响。

现在我们来看下一个减速机:user减速机:

/*** src/reducers/user.js ***/

import { post } from '../api';

// Actions
const LOGIN = 'user/LOGIN';
const LOGIN_SUCCESS = 'user/LOGIN_SUCCESS';
const LOGIN_ERROR = 'user/LOGIN_ERROR';
const REGISTER = 'user/REGISTER';
const REGISTER_SUCCESS = 'user/REGISTER_SUCCESS';
const REGISTER_ERROR = 'user/REGISTER_ERROR';
const LOGOUT = 'user/LOGOUT';

// Reducer
export default function reducer(state = {}, action = {}) {
  switch (action.type) {
 case LOGIN:
 case REGISTER:
      return { ...state, user: null, loading: true, error: null };
 case LOGIN_SUCCESS:
 case REGISTER_SUCCESS:
      return {
        ...state,
        user: action.payload.user,
        loading: false,
        error: null,
      };
 case LOGIN_ERROR:
 case REGISTER_ERROR:
      return {
        ...state,
        user: null,
        loading: false,
        error: action.payload.error,
      };
 case LOGOUT:
      return {
        ...state,
        user: null,
      };
    default:
      return state;
  }
}

// Action Creators
export function login({ email, password }) {
  return dispatch => {
    dispatch({ type: LOGIN });
    post('/login', { email, password })
      .then(user => dispatch({ type: LOGIN_SUCCESS, 
       payload: { user } }))
      .catch(error => dispatch({ type: LOGIN_ERROR,
       payload: { error } }));
  };
}

export function register({
  email,
  repeatEmail,
  name,
  password,
  address,
  postcode,
  city,
}) {
  if (
    !email ||
    !repeatEmail ||
    !name ||
    !password ||
    !name ||
    !address ||
    !postcode ||
    !city
  ) {
    return {
      type: REGISTER_ERROR,
      payload: { error: 'All fields are mandatory' },
    };
  }
  if (email !== repeatEmail) {
    return {
      type: REGISTER_ERROR,
      payload: { error: "Email fields don't match" },
    };
  }
  return dispatch => {
    dispatch({ type: REGISTER });
    post('/register', {
      email,
      name,
      password,
      address,
      postcode,
      city,
    })
      .then(user => dispatch({ type: REGISTER_SUCCESS, payload: 
                    { user } }))
      .catch(error => dispatch({ type: REGISTER_ERROR, payload: 
                    { error } }));
  };
}

export function logout() {
  return { type: LOGOUT };
}

此 reducer 中的动作创建者非常简单:

  • login():这需要emailpassword发送LOGIN动作,然后向/login端点发出POST请求以验证凭证。如果 API 调用成功,操作创建者将发送一个LOGIN_SUCCESS操作,将用户登录。如果请求失败,它将发送一个LOGIN_ERROR操作,以便用户知道发生了什么。
  • register():类似login()动作创建者;它将发送一个REGISTER操作,然后发送一个REGISTER_SUCCESSREGISTER_ERROR,具体取决于 API 调用的返回方式。如果注册成功,用户数据将存储在应用的状态中,表示用户已登录。
  • logout():发送LOGOUT动作,使减速器在应用状态下清除user对象。

最后一个减缩器处理付款数据:

/*** src/reducers/payments.js ***/

import { post } from '../api';

// Actions
const PAY = 'products/PAY';
const PAY_SUCCESS = 'products/PAY_SUCCESS';
const PAY_ERROR = 'products/PAY_ERROR';
const RESET_PAYMENT = 'products/RESET_PAYMENT';

// Reducer
export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    case PAY:
      return { ...state, loading: true, paymentConfirmed: false, 
               error: null };
    case PAY_SUCCESS:
      return {
        ...state,
        paymentConfirmed: true,
        loading: false,
        error: null,
      };
    case PAY_ERROR:
      return {
        ...state,
        loading: false,
        paymentConfirmed: false,
        error: action.payload.error,
      };
    case RESET_PAYMENT:
      return { loading: false, paymentConfirmed: false, error: null };
    default:
      return state;
  }
}

// Action Creators
export function pay(user, cart, card) {
  return dispatch => {
    dispatch({ type: PAY });
    post('/pay', { user, cart, card })
      .then(() => dispatch({ type: PAY_SUCCESS }))
      .catch(error => dispatch({ type: PAY_ERROR, 
             payload: { error } }));
  };
}

export function resetPayment() {
  return { type: RESET_PAYMENT };
}

此 reducer 中只有两个动作创建者:

  • pay():这需要用户、购物车和信用卡,并调用 API 中的/pay端点进行支付。如果支付成功,则触发PAY_SUCCESS动作,否则触发PAY_ERROR动作通知用户。
  • resetPayment():触发RESET_PAYMENT动作,清除任何支付数据。

我们已经看到这些动作创建者以多种方式与 API 联系。现在让我们创建一些 API 方法,以便操作创建者可以与应用的后端交互。

美国石油学会

我们将使用的 API 服务将使用两个 HTTP 方法(GETPOST)以及四个端点(/products/login/register/pay)。出于测试和开发的原因,我们将对该服务进行模拟,但在稍后阶段,将开放实现以方便插入外部端点:

/*** src/api.js ***/

export const get = uri =>
  new Promise(resolve => {
    let response;

    switch (uri) {
      case '/products':
        response = [
          {
            id: 1,
            name: 'Mastering Docker - Second Edition',
            author: 'James Cameron',
            img:
              'https://d1ldz4te4covpm.cloudfront.net/sites/default
              /files/imagecache/ppv4_main_book_cover
              /B06565_MockupCover_0.png',
            price: 39.58,
          },

         ...

        ];
        break;
      default:
        return null;
    }

    setTimeout(() => resolve(response), 1000);
    return null;
  });

export const post = (uri, data) =>
  new Promise((resolve, reject) => {
    let response;

    switch (uri) {
      case '/login':
        if (data.email === 'test@test.com' && data.password === 'test')  
        {
          response = {
            email: 'test@test.com',
            name: 'Test Testson',
            address: '123 test street',
            postcode: '2761XZ',
            city: 'Testington',
          };
        } else {
          setTimeout(() => reject('Unauthorised'), 1000);
          return null;
        }
        break;
      case '/pay':
        if (data.card.cvc === '123') {
          response = true;
        } else {
          setTimeout(() => reject('Payment not authorised'), 1000);
          return null;
        }
        break;
      case '/register':
        response = data;
        break;
      default:
        return null;
    }

    setTimeout(() => resolve(response), 1000);
    return null;
  });

export const put = () => {};

所有调用都封装在一个具有 1 秒延迟的setTimeout()函数中,以模拟网络活动,从而可以测试指标。仅当凭据为test@test.com/test时,服务才会成功响应。另一方面,pay()服务仅在 CVC/CVV 代码为123时返回成功响应。register 调用只是将提供的数据作为成功注册的用户数据返回

This setTimeout() trick is used to mock asynchronous calls up as they would happen with a real backend. It is an useful way to develop front-end solutions before the backend or testing environments are ready.

现在让我们转到应用中的屏幕。

产品列表

我们的主屏幕显示可购买的产品列表:

/*** src/screens/ProductList.js ***/

import React from 'react';
import { ScrollView, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {
  Spinner,
  Icon,
  List,
  ListItem,
  Thumbnail,
  Body,
  Text,
} from 'native-base';
import * as ProductActions from '../reducers/products';

class ProductList extends React.Component {
  static navigationOptions = {
    drawerLabel: 'Home',
    tabBarIcon: () => <Icon name="home" />,
  };

  componentWillMount() {
 this.props.fetchProducts();
  }

 onProductPress(product) {
 this.props.navigation.navigate('ProductDetail', { product });
 }

  render() {
    return (
      <ScrollView>
 {this.props.loading && <Spinner />}
        <List>
          {this.props.products.map(p => (
            <ListItem key={p.id}>
              <Thumbnail square height={80} source={{ uri: p.img }} />
              <Body>
                <TouchableOpacity onPress={() => 
                 this.onProductPress(p)}>
                  <Text>{p.name}</Text>
                  <Text note>${p.price}</Text>
                </TouchableOpacity>
              </Body>
            </ListItem>
          ))}
        </List>
      </ScrollView>
    );
  }
}

ProductList.propTypes = {
 fetchProducts: PropTypes.func.isRequired,
 products: PropTypes.array.isRequired,
 loading: PropTypes.bool.isRequired,
 navigation: PropTypes.any.isRequired,
};

function mapStateToProps(state) {
 return {
 products: state.productsReducer.products || [],
 loading: state.productsReducer.loading,
 };
}

function mapStateActionsToProps(dispatch) {
 return bindActionCreators(ProductActions, dispatch);
}

export default connect(mapStateToProps, mapStateActionsToProps)(ProductList);

安装此屏幕后,它将通过调用this.props.fetchProducts();检索可用产品的最新列表。这将触发屏幕中的重新渲染,因此所有可用书籍都将显示在屏幕上。为此,我们依靠 Redux 更新状态(通过 product reducer)并通过调用connect方法将新状态注入该屏幕,我们需要将mapStateToPropsmapStateActionsToProps函数传递给该方法

mapStateToProps将负责从state中提取产品列表,mapStateActionsToProps将每个动作连接到dispatch()功能,该功能将这些动作连接到 Redux 状态,将每个触发动作应用到所有减速器。在此屏幕中,我们只对与产品相关的操作感兴趣,因此我们将仅通过bindActionCreatorsRedux 函数将ProductActionsdispatch函数绑定在一起。

render方法中,我们使用map功能将检索到的产品列表翻译成几个<ListItem/>组件,这些组件将显示在<List/>中。在此列表上方,我们将在等待网络请求完成时显示<Spinner/>{this.props.loading && <Spinner />}

我们还通过prop-types库添加了属性验证:

ProductList.propTypes = {
  fetchProducts: PropTypes.func.isRequired,
  products: PropTypes.array.isRequired,
  loading: PropTypes.bool.isRequired,
  navigation: PropTypes.any.isRequired,
};

这意味着,每当该组件接收到输入错误的道具,或者实际上没有接收到所需道具时,我们都会收到警告。在这种情况下,我们希望收到:

  • 一个名为fetchProducts的函数,它将向 API 请求可用产品的列表。它将由 Redux 通过本屏幕上定义的mapStateActionsToProps提供。
  • 包含可用产品列表的products数组。这将由 Redux 通过前面提到的mapStateToProps功能注入。
  • 用于标记网络活动的加载布尔值(也由 Redux 通过mapStateToProps提供)。
  • react-navigation自动提供的导航对象。我们将其标记为类型any,因为它是一个外部对象,可能会在我们无法控制的情况下更改其类型。

所有这些都可以在我们组件的道具内使用(this.props

关于这个容器,最后要注意的是我们将如何处理用户操作。在此屏幕中,只有一个操作:用户单击产品项以查看其详细信息:

onProductPress(product) {
    this.props.navigation.navigate('ProductDetail', { product });
}

当用户点击特定产品时,此屏幕将调用navigation道具中的navigate功能,移动到下一屏幕ProductDetail。我们将使用navigation选项直接传递所选产品,而不是通过操作将其保存在状态中,以简化我们的存储。

产品详细信息

此屏幕将向用户显示所选产品的所有详细信息,并允许用户将所选产品添加到购物车中:

/*** src/screens/ProductDetail.js ***/

import React from 'react';
import { Image, ScrollView } from 'react-native';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Icon, Button, Text } from 'native-base';
import * as ProductsActions from '../reducers/products';

class ProductDetail extends React.Component {
  static navigationOptions = {
    drawerLabel: 'Home',
    tabBarIcon: () => <Icon name="home" />,
  };

  onBuyPress(product) {
 this.props.addProductToCart(product);
 this.props.navigation.goBack();
 setTimeout(() => this.props.navigation.navigate('MyCart',
                     { product }), 0);
 }

  render() {
    const { navigation } = this.props;
    const { state } = navigation;
    const { params } = state;
    const { product } = params;
    return (
      <ScrollView>
        <Image
 style={{
 height: 200,
 width: 160,
 alignSelf: 'center',
 marginTop: 20,
 }}
 source={{ uri: product.img }}
 />
 <Text
 style={{
 alignSelf: 'center',
 marginTop: 20,
 fontSize: 30,
 fontWeight: 'bold',
 }}
 >
 ${product.price}
        </Text>
        <Text
          style={{
            alignSelf: 'center',
            margin: 20,
          }}
        >
          Lorem ipsum dolor sit amet, consectetur 
          adipiscing elit. Nullam nec
          eros quis magna vehicula blandit at nec velit. 
          Mauris porta risus non
          lectus ultricies lacinia. Phasellus molestie metus ac 
          metus dapibus,
          nec maximus arcu interdum. In hac habitasse platea dictumst.
          Suspendisse fermentum iaculis ex, faucibus semper turpis 
          vestibulum quis.
        </Text>
        <Button
 block
 style={{ margin: 20 }}
 onPress={() => this.onBuyPress(product)}
 >
 <Text>Buy!</Text>
 </Button>
      </ScrollView>
    );
  }
}

ProductDetail.propTypes = {
  navigation: PropTypes.any.isRequired,
  addProductToCart: PropTypes.func.isRequired,
};

ProductDetail.navigationOptions = props => {
  const { navigation } = props;
  const { state } = navigation;
  const { params } = state;
  return {
    tabBarIcon: () => <Icon name="home" />,
    headerTitle: params.product.name,
  };
};

function mapStateToProps(state) {
 return {
 user: state.userReducer.user,
 };
}
function mapStateActionsToProps(dispatch) {
 return bindActionCreators(ProductsActions, dispatch);
}

export default connect(mapStateToProps, mapStateActionsToProps)(ProductDetail);

ProductDetail要求 Redux 提供state中存储的用户详细信息。这是通过调用connect方法,传递一个mapStateToProps函数,该函数将从指定的state中提取用户,并将其返回到屏幕中作为prop注入。它还需要 Redux 的操作:addProductToCart。当用户表示希望购买所选产品时,此操作仅将其存储在商店中。

此屏幕中的render()方法显示<ScrollView />包装书籍图像、价格、描述(我们现在将显示一个假的lorem ipsum描述),以及一个Buy!按钮,该按钮将连接到 Redux 提供的addProductToCart动作:

onBuyPress(product) {
    this.props.addProductToCart(product);
    this.props.navigation.goBack();
    setTimeout(() => this.props.navigation.navigate('MyCart', 
                     { product }), 0);
}

onBuyPress()方法调用上述操作,然后执行一个小的导航技巧。它通过调用navigation对象上的goBack()方法将ProductDetail屏幕从导航堆栈中移除,因为用户将产品添加到购物车后不再需要它。完成此操作后,onBuyPress()方法将立即调用要移动的navigation对象上的navigate方法,并在MyCart屏幕中显示用户购物车的状态。我们在这里使用setTimeout是为了确保等待上一次调用(this.props.navigation.goBack();完成所有导航任务,并且该对象再次准备好供我们使用。等待0秒应该足够了,因为我们只想等待调用堆栈被清除。

让我们来看看现在的屏幕看起来是什么样子。

我的购物车

此屏幕期望 Redux 注入存储在状态中的购物车,因此它可以呈现购物车中的所有项目,供用户在确认购买之前查看:

/*** src/screens/MyCart.js ***/

import React from 'react';
import { ScrollView, View } from 'react-native';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
  ListItem,
  Text,
  Icon,
  Button,
  Badge,
  Header,
  Title,
} from 'native-base';

import * as ProductActions from '../reducers/products';

class MyCart extends React.Component {
  static navigationOptions = {
    drawerLabel: 'My Cart',
    tabBarIcon: () => <Icon name="cart" />,
  };

  onTrashPress(product) {
 this.props.removeProductFromCart(product);
 }

  render() {
    return (
      <View>
        <ScrollView>
          {this.props.cart.map((p, i) => (
            <ListItem key={i} style={{ justifyContent: 
                              'space-between' }}>
              <Badge primary>
                <Text>{p.quantity}</Text>
              </Badge>
              <Text> {p.name}</Text>
              <Button
 icon
 danger
 small
 transparent
 onPress={() => this.onTrashPress(p)}
 >
 <Icon name="trash" />
 </Button>
            </ListItem>
          ))}
          {this.props.cart.length > 0 && (
            <View>
              <Text style={{ alignSelf: 'flex-end', margin: 10 }}>
                Total: ${this.props.cart.reduce(
                  (sum, p) => sum + p.price * p.quantity,
                  0,
                )}
              </Text>
              <View style={{ flexDirection: 'row', 
               justifyContent: 'center' }}>
 <Button
 style={{ margin: 10 }}
 onPress={() =>  
                  this.props.navigation.navigate('Home')}
 >
 <Text>Keep buying</Text>
 </Button>
 <Button
 style={{ margin: 10 }}
 onPress={() => 
                  this.props.navigation.navigate('Payment')}
 >
 <Text>Confirm purchase</Text>
 </Button>
              </View>
            </View>
          )}
          {this.props.cart.length == 0 && (
            <Text style={{ alignSelf: 'center', margin: 30 }}>
              There are no products in the cart
            </Text>
          )}
        </ScrollView>
      </View>
    );
  }
}

MyCart.propTypes = {
  cart: PropTypes.array.isRequired,
  navigation: PropTypes.object.isRequired,
  removeProductFromCart: PropTypes.func.isRequired,
};

function mapStateToProps(state) {
  return {
    user: state.userReducer.user,
    cart: state.productsReducer.cart || [],
    loading: state.userReducer.loading,
    error: state.userReducer.error,
    paying: state.paymentsReducer.loading,
  };
}
function mapStateActionsToProps(dispatch) {
  return bindActionCreators(ProductActions, dispatch);
}

export default connect(mapStateToProps, mapStateActionsToProps)(MyCart);

除了购物车本身,正如我们在propTypes定义中所看到的,这个屏幕需要ProductActionsremoveProductFromCart动作,当用户准备确认购买时,需要提供navigation对象导航到Payment屏幕。

总之,用户可以从这里执行三个操作:

  • 通过单击每个产品行上的垃圾箱图标从购物车中删除项目(调用this.onTrashPress()
  • 导航到Payment屏幕完成她的购买(调用this.props.navigation.navigate('Payment')
  • 导航到主屏幕以继续购买产品(调用this.props.navigation.navigate('Home')

让我们通过查看Payment屏幕继续购买过程。

付款

我们将使用react-native-credit-card-input库捕获用户的信用卡详细信息。要使此屏幕正常工作,我们将从 Redux 请求购物车、用户和几个重要操作:

/*** src/screens/Payment.js ***/

import React from 'react';
import { View } from 'react-native';

import { CreditCardInput } from 'react-native-credit-card-input';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Icon, Button, Text, Spinner, Title } from 'native-base';
import PropTypes from 'prop-types';
import * as PaymentsActions from '../reducers/payments';
import * as UserActions from '../reducers/user';
import LoginOrRegister from '../components/LoginOrRegister';

class Payment extends React.Component {
  static navigationOptions = {
    drawerLabel: 'MyCart',
    tabBarIcon: () => <Icon name="cart" />,
  };
  state = {
 validCardDetails: false,
 cardDetails: null,
 };
  onCardInputChange(creditCardForm) {
 this.setState({
 validCardDetails: creditCardForm.valid,
 cardDetails: creditCardForm.values,
 });
 }

  componentWillReceiveProps(newProps) {
 if (this.props.paying && newProps.paymentConfirmed) {
 this.props.navigation.navigate('PaymentConfirmation');
 }
 }

  render() {
    return (
      <View
        style={{
          flex: 1,
          alignSelf: 'stretch',
          paddingTop: 10,
        }}
      >
        {this.props.cart.length > 0 &&
 !this.props.user && (
 <LoginOrRegister
 login={this.props.login}
 register={this.props.register}
 logout={this.props.logout}
 loading={this.props.loading}
 error={this.props.error}
 />
 )}
        {this.props.cart.length > 0 &&
 this.props.user && (
          <View>
            <Title style={{ margin: 10 }}>
              Paying: $
              {this.props.cart.reduce(
                (sum, p) => sum + p.price * p.quantity,
                0,
              )}
            </Title>
            <CreditCardInput onChange=
            {this.onCardInputChange.bind(this)} />
            <Button
 block
 style={{ margin: 20 }}
 onPress={() =>
 this.props.pay(
 this.props.user,
 this.props.cart,
 this.state.cardDetails,
 )}
 disabled={!this.state.validCardDetails}
 >
 <Text>Pay now</Text>
 </Button>
            {this.props.paying && <Spinner />}
          </View>
        )}
        {this.props.cart.length > 0 &&
        this.props.error && (
          <Text
 style={{
 alignSelf: 'center',
 color: 'red',
 position: 'absolute',
 bottom: 10,
 }}
 >
 {this.props.error}
 </Text>
        )}
        {this.props.cart.length === 0 && (
 <Text style={{ alignSelf: 'center', margin: 30 }}>
 There are no products in the cart
 </Text>
 )}
      </View>
    );
  }
}

Payment.propTypes = {
 user: PropTypes.object,
 cart: PropTypes.array,
 login: PropTypes.func.isRequired,
 register: PropTypes.func.isRequired,
 logout: PropTypes.func.isRequired,
 pay: PropTypes.func.isRequired,
 loading: PropTypes.bool,
 paying: PropTypes.bool,
 error: PropTypes.string,
 paymentConfirmed: PropTypes.bool,
 navigation: PropTypes.object.isRequired,
};

function mapStateToProps(state) {
  return {
    user: state.userReducer.user,
    cart: state.productsReducer.cart,
    loading: state.userReducer.loading,
    paying: state.paymentsReducer.loading,
    paymentConfirmed: state.paymentsReducer.paymentConfirmed,
    error: state.paymentsReducer.error || state.userReducer.error,
  };
}
function mapStateActionsToProps(dispatch) {
  return bindActionCreators(
    Object.assign({}, PaymentsActions, UserActions),
    dispatch,
  );
}

export default connect(mapStateToProps, mapStateActionsToProps)(Payment);

这是一个复杂的组件。让我们看一下道具验证,了解它的签名:

Payment.propTypes = {
  user: PropTypes.object,
  cart: PropTypes.array,
  login: PropTypes.func.isRequired,
  register: PropTypes.func.isRequired,
  logout: PropTypes.func.isRequired,
  pay: PropTypes.func.isRequired,
  loading: PropTypes.bool,
  paying: PropTypes.bool,
  error: PropTypes.string,
  paymentConfirmed: PropTypes.bool,
  navigation: PropTypes.object.isRequired,
};

要使组件正常工作,需要传递以下道具:

  • user:我们需要用户检查是否登录。如果她不是,我们将显示登录/注册组件,而不是信用卡输入。
  • cart:我们需要它来计算并显示要记入信用卡的总额
  • login:如果用户决定从此屏幕登录,将调用此操作。
  • register:如果用户决定从此屏幕注册,将调用此操作。
  • logout:此动作是<LoginOrRegister />组件工作所必需的,因此需要 Redux 提供,以便注入子<LoginOrRegister />组件。
  • pay:当用户输入有效的信用卡详细信息并按下“立即付款”按钮时,将触发此操作。
  • loading:这是子<LoginOrRegister />组件正常工作的标志。
  • paying:此标志用于确认付款时显示微调器。
  • error:这是对上次尝试支付或登录/注册时发生的错误的描述。
  • paymentConfirmed:此标志将告知组件何时/是否正确支付。
  • navigation:用于导航到其他屏幕的navigation对象。

此组件也有自己的状态:

state = {
    validCardDetails: false,
    cardDetails: null,
};

此状态下的两个属性将由<CreditCardInput />(主组件表单react-native-credit-card-input提供),并将保存用户的信用卡详细信息及其有效性。

为了检测付款确认的时间,我们将使用 React 方法componentWillReceiveProps

componentWillReceiveProps(newProps) {
    if (this.props.paying && newProps.paymentConfirmed) {
      this.props.navigation.navigate('PaymentConfirmation');
    }
}

此方法仅检测道具paymentConfirmed何时从false变为true,以便导航到PaymentConfirmation屏幕。

付款确认书

一个简单的屏幕显示刚刚确认的购买摘要:

/*** src/screens/PaymentConfirmation ***/

import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { NavigationActions } from 'react-navigation';
import { Icon, Title, Text, ListItem, Badge, Button } from 'native-base';

import * as UserActions from '../reducers/user';
import * as ProductActions from '../reducers/products';
import * as PaymentsActions from '../reducers/payments';

class PaymentConfirmation extends React.Component {
  static navigationOptions = {
    drawerLabel: 'MyCart',
    tabBarIcon: () => <Icon name="cart" />,
  };

  componentWillMount() {
 this.setState({ cart: this.props.cart }, () => {
 this.props.resetCart();
 this.props.resetPayment();
 });
 }

 continueShopping() {
 const resetAction = NavigationActions.reset({
 index: 0,
 actions: [NavigationActions.navigate({ routeName: 'MyCart' })],
 });
 this.props.navigation.dispatch(resetAction);
 }

  render() {
    return (
      <View>
        <Title style={{ marginTop: 20 }}>Your purchase is complete!
        </Title>
        <Text style={{ margin: 20 }}>
          Thank you for buying with us. We sent you an email with the
          confirmation details and an invoice. 
          Here you can find a summary of
          your purchase:{' '}
        </Text>
        {this.state.cart.map((p, i) => (
          <ListItem key={i} style={{ justifyContent: 
          'space-between' }}>
            <Badge primary>
              <Text>{p.quantity}</Text>
            </Badge>
            <Text> {p.name}</Text>
            <Text> {p.price * p.quantity}</Text>
          </ListItem>
        ))}
        <Text style={{ alignSelf: 'flex-end', margin: 10 }}>
          Total: ${this.state.cart.reduce(
            (sum, p) => sum + p.price * p.quantity,
            0,
          )}
        </Text>
        <Button
          block
          style={{ margin: 20 }}
          onPress={this.continueShopping.bind(this)}
        >
          <Text>Continue Shopping</Text>
        </Button>
      </View>
    );
  }
}

PaymentConfirmation.propTypes = {
  cart: PropTypes.array.isRequired,
  resetCart: PropTypes.func.isRequired,
  resetPayment: PropTypes.func.isRequired,
};

function mapStateToProps(state) {
  return {
    cart: state.productsReducer.cart || [],
  };
}
function mapStateActionsToProps(dispatch) {
  return bindActionCreators(
    Object.assign({}, PaymentsActions, ProductActions, UserActions),
    dispatch,
  );
}

export default connect(mapStateToProps, mapStateActionsToProps)(
  PaymentConfirmation,
);

此屏幕所做的第一件事是将与购物车相关的应用状态保存在自己组件的状态中:

componentWillMount() {
    this.setState({ cart: this.props.cart }, () => {
      this.props.resetCart();
      this.props.resetPayment();
    });
}

这是必要的,因为我们希望在显示此屏幕后立即重置购物车和付款详细信息,因为在以后的任何场合都不需要它。这是通过调用 Redux 提供的resetCart()resetPayment()操作来完成的。

render方法只是将购物车中的项目(现在以组件状态保存)映射到视图列表中,以便用户可以查看其订单。在这些视图的底部,我们将显示一个标记为继续购物的按钮,该按钮将通过调用continueShopping方法将用户返回到ProductList屏幕。除了导航到ProductList屏幕外,我们还需要重置导航,以便用户下次想要购买某些物品时可以从头开始购买旅程。这是通过创建重置导航操作并调用this.props.navigation.dispatch(resetAction);来实现的。

The method continueShopping calls NavigationActions.reset to clear the navigation stack and go back to the home screen. This method is usually called at the end of a user journey.

这个屏幕完成了购买过程,现在让我们关注应用的另一部分:用户配置文件。

我的个人资料

正如我们之前所看到的,只有登录的用户才能完成购买,因此我们需要一种让用户登录、注销、注册和查看其帐户详细信息的方法。这将通过MyProfile屏幕和<LonginOrRegister />组件实现:

/*** src/screens/MyProfile.js ***/

import React from 'react';
import { View, Button as LinkButton } from 'react-native';
import PropTypes from 'prop-types';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {
  Icon,
  Header,
  Title,
  Label,
  Input,
  Item,
  Form,
  Content,
} from 'native-base';

import * as UserActions from '../reducers/user';
import LoginOrRegister from '../components/LoginOrRegister';

class MyProfile extends React.Component {
  static navigationOptions = {
    drawerLabel: 'My Profile',
    tabBarIcon: () => <Icon name="person" />,
  };

  render() {
    return (
      <View
        style={{
          flex: 1,
          alignSelf: 'stretch',
        }}
      >
        <Header>
          <Title style={{ paddingTop: 10 }}>My Profile</Title>
        </Header>
        {!this.props.user && (
 <LoginOrRegister
 login={this.props.login}
 register={this.props.register}
 logout={this.props.logout}
 loading={this.props.loading}
 error={this.props.error}
 />
 )}
        {this.props.user && (
          <Content>
            <Form>
              <Item>
                <Item fixedLabel>
                  <Label>Name</Label>
                  <Input disabled placeholder={this.props.user.name} />
                </Item>
              </Item>
              <Item disabled>
                <Item fixedLabel>
                  <Label>Email</Label>
                  <Input disabled placeholder={this.props.user.email} 
                  />
                </Item>
              </Item>
              <Item disabled>
                <Item fixedLabel>
                  <Label>Address</Label>
                  <Input disabled placeholder={this.props.user.address} 
                  />
                </Item>
              </Item>
              <Item disabled>
                <Item fixedLabel&gt;
                  <Label>Postcode</Label>
                  <Input disabled placeholder=
                    {this.props.user.postcode} />
                </Item>
              </Item>
              <Item disabled>
                <Item fixedLabel>
                  <Label>City</Label>
                  <Input disabled placeholder={this.props.user.city} />
                </Item>
              </Item>
            </Form>
            <LinkButton title={'Logout'} onPress={() => 
              this.props.logout()} />
          </Content>
        )}
      </View>
    );
  }
}

MyProfile.propTypes = {
 user: PropTypes.any,
 login: PropTypes.func.isRequired,
 register: PropTypes.func.isRequired,
 logout: PropTypes.func.isRequired,
 loading: PropTypes.bool,
 error: PropTypes.string,
};

function mapStateToProps(state) {
  return {
    user: state.userReducer.user || null,
    loading: state.userReducer.loading,
    error: state.userReducer.error,
  };
}
function mapStateActionsToProps(dispatch) {
  return bindActionCreators(UserActions, dispatch);
}

export default connect(mapStateToProps, mapStateActionsToProps)(MyProfile);

此屏幕接收来自应用状态的用户和一系列操作(loginregisterlogout),这些操作将输入<LoginOrRegister />组件以启用登录和注册。因此,大部分逻辑将被推迟到<LoginOrRegister />组件,留下MyProfile屏幕的任务是列出用户的帐户详细信息并显示一个用于注销用户的按钮。

让我们回顾一下<LoginOrRegister />组件的功能和方式。

逻辑寄存器

实际上,该组件由两个子组件组成:<Login /><Register /><LoginOrRegister />的唯一任务是保存应该显示哪个组件(<Login /><Register />的状态,并相应地显示出来。

/*** src/components/LoginOrRegister.js ***/

import React from 'react';
import { View } from 'react-native';
import PropTypes from 'prop-types';

import Login from './Login';
import Register from './Register';

export default class LoginOrRegister extends React.Component {
  state = {
 display: 'login',
 };

  render() {
    return (
      <View
        style={{
          flex: 1,
          justifyContent: 'center',
          alignSelf: 'stretch',
        }}
      >
        {this.state.display === 'login' && (
 <Login
 login={this.props.login}
 changeToRegister={() => this.setState({ display: 
            'register' })}
 loading={this.props.loading}
 error={this.props.error}
 />
 )}
 {this.state.display === 'register' && (
 <Register
 register={this.props.register}
 changeToLogin={() => this.setState({ display: 'login' })}
 loading={this.props.loading}
 error={this.props.error}
 />
 )}
      </View>
    );
  }
}

LoginOrRegister.propTypes = {
  error: PropTypes.string,
  login: PropTypes.func.isRequired,
  register: PropTypes.func.isRequired,
  loading: PropTypes.bool,
};

此组件中的状态可由其子组件更改,因为它将函数传递给每个子组件:

changeToRegister={() => this.setState({ display: 'register' })}

...

changeToLogin={() => this.setState({ display: 'login' })}

现在让我们来看看<Login /><Register />组件将如何使用这些道具来更新其父母的状态,从一个视图切换到另一个视图。

登录

默认情况下,登录视图将显示在父组件上。它的任务是捕获登录信息,一旦用户按下Login按钮,就调用login操作:

/*** src/components/Login.js ***/

import React from 'react';
import { View, Button as LinkButton } from 'react-native';
import { Form, Item, Input, Content, Button, Text, Spinner } from 'native-base';
import PropTypes from 'prop-types';

class Login extends React.Component {
  state = { email: null, password: null };

  render() {
    return (
      <View style={{ flex: 1 }}>
        <Content>
          <Form>
            <Item>
              <Input
 placeholder="e-mail"
 keyboardType={'email-address'}
 autoCapitalize={'none'}
 onChangeText={email => this.setState({ email })}
 />
            </Item>
            <Item last>
              <Input
 placeholder="password"
 secureTextEntry
 onChangeText={password => this.setState({ password })}
 />
            </Item>
            <Button
 block
 disabled={this.props.loading}
 style={{ margin: 20 }}
 onPress={() =>
 this.props.login({
 email: this.state.email,
 password: this.state.password,
 })}
 >
 <Text>Login</Text>
 </Button>
          </Form>

          <LinkButton
 title={'or Register'}
 onPress={() => this.props.changeToRegister()}
 />
 {this.props.loading && <Spinner />}
        </Content>
        {this.props.error && (
          <Text
            style={{
              alignSelf: 'center',
              color: 'red',
              position: 'absolute',
              bottom: 10,
            }}
          >
            {this.props.error}
          </Text>
        )}
      </View>
    );
  }
}

Login.propTypes = {
  error: PropTypes.string,
  loading: PropTypes.bool,
  login: PropTypes.func.isRequired,
  changeToRegister: PropTypes.func.isRequired,
};

export default Login;

两个输入捕获电子邮件和密码,并在更改输入时将其保存到组件状态。一旦用户输入完她的凭证,她将按下Login按钮并触发登录操作,从组件的状态传递电子邮件和密码

还有一个标记为or Register<LinkButton />,它将调用(按下时)其父<LoginOrRegister />传递的this.props.changeToRegister()函数。

登记

与登录表单类似,<Register />组件是一个输入字段列表,将其更改保存到组件状态,直到用户有足够的信心按下Register按钮:

import React from 'react';
import { View, Button as LinkButton } from 'react-native';
import { Form, Item, Input, Content, Button, Text, Spinner } from 'native-base';
import PropTypes from 'prop-types';

class Register extends React.Component {
  state = {
 email: null,
 repeatEmail: null,
 name: null,
 password: null,
 address: null,
 postcode: null,
 city: null,
 };

  render() {
    return (
      <View style={{ flex: 1 }}>
        <Content>
          <Form>
            <Item>
              <Input
                placeholder="e-mail"
                keyboardType={'email-address'}
                autoCapitalize={'none'}
                onChangeText={email => this.setState({ email })}
              />
            </Item>
            <Item>
              <Input
                placeholder="repeat e-mail"
                autoCapitalize={'none'}
                keyboardType={'email-address'}
                onChangeText={repeatEmail => this.setState({ 
                                             repeatEmail })}
              />
            </Item>
            <Item>
              <Input
                placeholder="name"
                onChangeText={name => this.setState({ name })}
              />
            </Item>
            <Item>
              <Input
                placeholder="password"
                secureTextEntry
                onChangeText={password => this.setState({ password })}
              />
            </Item>
            <Item>
              <Input
                placeholder="address"
                onChangeText={address => this.setState({ address })}
              />
            </Item>
            <Item>
              <Input
                placeholder="postcode"
                onChangeText={postcode => this.setState({ postcode })}
              />
            </Item>
            <Item>
              <Input
                placeholder="city"
                onChangeText={city => this.setState({ city })}
              />
            </Item>
            <Button
 block
 style={{ margin: 20 }}
 onPress={() =>
 this.props.register({
 email: this.state.email,
 repeatEmail: this.state.repeatEmail,
 name: this.state.name,
 password: this.state.password,
 address: this.state.address,
 postcode: this.state.postcode,
 city: this.state.city,
 })}
 >
 <Text>Register</Text>
 </Button>
          </Form>
          <LinkButton
 title={'or Login'}
 onPress={() => this.props.changeToLogin()}
 />
 {this.props.loading && <Spinner />}
        </Content>
        {this.props.error && (
          <Text
            style={{
              alignSelf: 'center',
              color: 'red',
              position: 'absolute',
              bottom: 10,
            }}
          >
            {this.props.error}
          </Text>
        )}
      </View>
    );
  }
}

Register.propTypes = {
  register: PropTypes.func.isRequired,
  changeToLogin: PropTypes.func.isRequired,
  error: PropTypes.string,
  loading: PropTypes.bool,
};

export default Register;

在这种情况下,当按下按钮切换到登录视图时,视图底部的<LinkButton />将调用this.props.changeToLogin()

销售额

我们添加了最后一个屏幕来演示如何通过重用屏幕和组件将不同的旅程链接在一起。在这种情况下,我们将创建一个降价产品列表,可以直接添加到购物车中进行快速购买:

/*** src/screens/Sales.js ***/

import React from 'react';
import { ScrollView, Image } from 'react-native';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';

import {
  Icon,
  Card,
  CardItem,
  Left,
  Body,
  Text,
  Button,
  Right,
  Title,
} from 'native-base';
import * as ProductActions from '../reducers/products';

class Sales extends React.Component {
  static navigationOptions = {
    drawerLabel: 'Sales',
    tabBarIcon: () => <Icon name="home" />,
  };

  onBuyPress(product) {
 this.props.addProductToCart(product);
 setTimeout(() => this.props.navigation.navigate
    ('MyCart', { product }), 0);
 }

  render() {
    return (
      <ScrollView style={{ padding: 20 }}>
        {this.props.products.filter(p => p.discount).map(product => (
          <Card key={product.id}>
            <CardItem>
              <Left>
                <Body>
                  <Text>{product.name}</Text>
                  <Text note>{product.author}</Text>
                </Body>
              </Left>
            </CardItem>
            <CardItem cardBody>
              <Image
                source={{ uri: product.img }}
                style={{ height: 200, width: null, flex: 1 }}
              />
            </CardItem>
            <CardItem>
              <Left>
                <Title>${product.price}</Title>
              </Left>
              <Body>
                <Button transparent onPress={() => 
                 this.onBuyPress(product)}>
 <Text>Add to cart</Text>
 </Button>
              </Body>
              <Right>
                <Text style={{ color: 'red' }}>
                 {product.discount} off!</Text>
              </Right>
            </CardItem>
          </Card>
        ))}
      </ScrollView>
    );
  }
}

Sales.propTypes = {
 products: PropTypes.array.isRequired,
 addProductToCart: PropTypes.func.isRequired,
 navigation: PropTypes.any.isRequired,
};

function mapStateToProps(state) {
  return {
    products: state.productsReducer.products || [],
  };
}
function mapStateActionsToProps(dispatch) {
  return bindActionCreators(ProductActions, dispatch);
}

export default connect(mapStateToProps, mapStateActionsToProps)(Sales);

我们将使用已存储在 Redux 状态的可用产品的完整列表,过滤(通过降价)并映射到一个吸引人的列表项,该列表项准备通过触发onBuyPress()方法添加到购物车中,该方法反过来触发addProductToCart()

onBuyPress(product) {
    this.props.addProductToCart(product);
    setTimeout(() => this.props.navigation.navigate('MyCart',
                                                    { product }), 0);
}

除了触发此 Redux 操作外,onBuyPress()还会导航到MyCart屏幕,但在清除调用堆栈后会这样做,以确保产品已正确添加到购物车中。

在此阶段,购买过程将再次启动,允许用户登录(如果尚未登录)、支付项目费用并确认购买。

总结

在本章中,我们开发了大多数电子商务应用中的几种常见功能,如用户登录和注册、从 API 检索数据、购买旅程和支付。

我们将所有屏幕与通过 Redux 管理的通用应用状态绑定在一起,这使得该应用具有可扩展性和易于维护性。

考虑到可维护性,我们为所有组件和屏幕添加了属性验证。此外,我们使用 ESLint 强制执行标准代码格式化和 linting,这样应用就可以让各种团队成员调整和开发舒适的新功能或维护当前功能。

最后,我们还添加了 API 模拟,让开发人员在构建移动应用时无需后端即可在本地工作。