如何使用React创建电子商务网站?分步指南

2021年11月30日02:04:06 发表评论 1,371 次浏览

在本教程中,我们将了解如何使用 React 构建一个非常简单的电子商务 Web 应用程序。它不会是下一个 Shopify,但希望它可以作为一种有趣的方式来展示 React 非常适合构建动态且引人入胜的用户界面,本文是一个详细的React创建电商网站实例

如何使用React创建电子商务网站?我们将创建一个电商应用程序,该应用程序将演示一个基本的购物车管理系统,以及处理用户身份验证的简单方法。我们将使用 React Context 作为 Redux 或 MobX 等状态管理框架的替代方案,我们将使用 json-server 包创建一个假后端。

下面是我们将要构建的内容的屏幕截图:

如何使用React创建电子商务网站?分步指南
React创建电商网站实例

此应用程序的代码可在GitHub上找到。

React电商网站创建教程预先准备

本教程假设你具有 JavaScript 和 React 的基本知识。如果你是 React 的新手,你可能想查看我们的初学者指南。

要构建应用程序,你需要在 PC 上安装最新版本的 Node。如果不是这种情况,请转到 Node 主页并 为你的系统下载正确的二进制文件。或者,你可以考虑使用版本管理器来安装 Node。我们在此处提供了有关使用版本管理器的教程。

Node 与 npm 捆绑在一起,这是一个 JavaScript 包管理器,我们将通过它安装一些我们将使用的库。你可以在此处了解有关使用 npm 的更多信息。

你可以通过从命令行发出以下命令来检查两者是否已正确安装:

node -v
> 12.18.4

npm -v
> 6.14.8

完成后,让我们开始使用Create React App工具创建一个新的 React 项目。你可以全局安装它,也可以使用npx,如下所示:

npx create-react-app e-commerce

完成后,切换到新创建的目录:

cd e-commerce

在这个应用程序中,我们将使用React Router来处理路由。要安装此模块,请运行:

npm install react-router-dom

我们还需要json-serverjson-server-auth来创建我们的假后端来处理身份验证:

npm install json-server json-server-auth

我们需要axios来向我们的假后端发出 Ajax 请求。

npm install axios

我们需要jwt-decode以便我们可以解析后端将响应的 JWT:

npm install jwt-decode

最后,我们将使用Bulma CSS 框架来设计这个应用程序的样式。要安装它,请运行以下命令:

npm install bulma

入门

首先,我们需要将样式表添加到我们的应用程序中。为了实现这一点,我们将添加一个 import 语句以将该文件包含在index.js文件src夹中的文件中。这将在应用程序中的所有组件中应用样式表:

import "bulma/css/bulma.css";

上下文设置

如何使用React创建电子商务网站?如前所述,我们将在整个应用程序中使用React Context。这是 React 的一个相对较新的补充,并提供了一种通过组件树传递数据的方法,而无需在每个级别手动向下传递 props。

如果你想复习在 React 应用程序中使用上下文,请查看我们的教程“如何用 React Hooks 和 Context API 替换 Redux ”。

在通常需要上下文的复杂应用程序中,可以有多个上下文,每个上下文都有自己的数据和方法,这些数据和方法与需要数据和方法的组件集相关。例如,可以有一个ProductContext用于处理使用产品相关数据的组件,另一个ProfileContext用于处理与身份验证和用户数据相关的数据。然而,为了让事情尽可能简单,我们将只使用一个上下文实例。

为了创建上下文,我们在应用程序目录中创建一个Context.js文件和一个withContext.js文件src

cd src
touch Context.js withContext.js

然后将以下内容添加到Context.js

import React from "react";
const Context = React.createContext({});
export default Context;

这将创建上下文并将上下文数据初始化为空对象。接下来,我们需要创建一个组件包装器,我们将用它来包装使用上下文数据和方法的组件:

// src/withContext.js

import React from "react";
import Context from "./Context";

const withContext = WrappedComponent => {
  const WithHOC = props => {
    return (
      <Context.Consumer>
        {context => <WrappedComponent {...props} context={context} />}
      </Context.Consumer>
    );
  };

  return WithHOC;
};

export default withContext;

这可能看起来有点复杂,但本质上它所做的就是创建一个高阶组件,它将我们的上下文附加到包装组件的 props 上。

稍微分解一下,我们可以看到该withContext函数将一个 React 组件作为其参数。然后它返回一个函数,该函数将组件的 props 作为参数。在返回的函数中,我们将组件包装在我们的上下文中,然后将上下文作为 prop: 分配给它context={context}。该{...props}位确保组件保留首先传递给它的任何道具。

所有这一切意味着我们可以在整个应用程序中遵循这种模式:

import React from "react";
import withContext from "../withContext";

const Cart = props => {
  // We can now access Context as props.context
};

export default withContext(Cart);

搭建应用程序的脚手架

React创建电商网站实例:现在,让我们为应用程序的基本导航正常运行所需的组件创建一个骨架版本。这是AddProductsCartLoginProductList,和我们将它们放置在components该目录内src的目录:

mkdir components
cd components
touch AddProduct.js Cart.js Login.js ProductList.js

另外AddProduct.js

import React from "react";

export default function AddProduct() {
  return <>AddProduct</>
}

另外Cart.js

import React from "react";

export default function Cart() {
  return <>Cart</>
}

另外Login.js

import React from "react";

export default function Login() {
  return <>Login</>
}

最后,ProductList.js补充一下:

import React from "react";

export default function ProductList() {
  return <>ProductList</>
}

接下来,我们需要设置App.js文件。在这里,我们将处理应用程序的导航以及定义其数据和方法来管理它。

首先,让我们设置导航。更改App.js如下:

import React, { Component } from "react";
import { Switch, Route, Link, BrowserRouter as Router } from "react-router-dom";

import AddProduct from './components/AddProduct';
import Cart from './components/Cart';
import Login from './components/Login';
import ProductList from './components/ProductList';

import Context from "./Context";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      cart: {},
      products: []
    };
    this.routerRef = React.createRef();
  }

  render() {
    return (
      <Context.Provider
        value={{
          ...this.state,
          removeFromCart: this.removeFromCart,
          addToCart: this.addToCart,
          login: this.login,
          addProduct: this.addProduct,
          clearCart: this.clearCart,
          checkout: this.checkout
        }}
      >
        <Router ref={this.routerRef}>
        <div className="App">
          <nav
            className="navbar container"
            role="navigation"
            aria-label="main navigation"
          >
            <div className="navbar-brand">
              <b className="navbar-item is-size-4 ">ecommerce</b>
              <label
                role="button"
                class="navbar-burger burger"
                aria-label="menu"
                aria-expanded="false"
                data-target="navbarBasicExample"
                onClick={e => {
                  e.preventDefault();
                  this.setState({ showMenu: !this.state.showMenu });
                }}
              >
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
              </label>
            </div>
              <div className={`navbar-menu ${
                  this.state.showMenu ? "is-active" : ""
                }`}>
                <Link to="/products" className="navbar-item">
                  Products
                </Link>
                {this.state.user && this.state.user.accessLevel < 1 && (
                  <Link to="/add-product" className="navbar-item">
                    Add Product
                  </Link>
                )}
                <Link to="/cart" className="navbar-item">
                  Cart
                  <span
                    className="tag is-primary"
                    style={{ marginLeft: "5px" }}
                  >
                    { Object.keys(this.state.cart).length }
                  </span>
                </Link>
                {!this.state.user ? (
                  <Link to="/login" className="navbar-item">
                    Login
                  </Link>
                ) : (
                  <Link to="/" onClick={this.logout} className="navbar-item">
                    Logout
                  </Link>
                )}
              </div>
            </nav>
            <Switch>
              <Route exact path="/" component={ProductList} />
              <Route exact path="/login" component={Login} />
              <Route exact path="/cart" component={Cart} />
              <Route exact path="/add-product" component={AddProduct} />
              <Route exact path="/products" component={ProductList} />
            </Switch>
          </div>
        </Router>
      </Context.Provider>
    );
  }
}

我们的App组件将负责初始化应用程序数据,还将定义操作这些数据的方法。首先,我们使用Context.Provider组件定义上下文数据和方法。数据和方法作为属性传递valueProvider组件,以替换上下文创建时给定的对象。(请注意,该值可以是任何数据类型。)我们传递状态值和一些方法,我们将很快定义这些方法。

接下来,我们构建我们的应用程序导航。为了实现这一点,我们需要用一个Router组件包装我们的应用程序,它可以是BrowserRouter(就像我们的例子一样)或HashRouter. 接下来,我们使用SwitchRoute组件定义应用程序的路由。我们还创建了应用程序的导航菜单,每个链接都使用LinkReact Router 模块中提供的组件。我们还routerRefRouter组件添加了一个引用 ,以使我们能够从App组件内部访问路由器。

要对此进行测试,请前往项目根目录(例如,/files/jim/Desktop/e-commerce)并使用npm start. 启动后,你的默认浏览器应该会打开,你应该会看到我们应用程序的框架。请务必单击并确保所有导航正常工作。

React电商网站创建教程:创建一个假后端

在下一步中,我们将设置一个假后端来存储我们的产品并处理用户身份验证。如前所述,为此我们将使用 json-server 创建一个虚假的 REST API,并使用 json-server-auth向我们的应用程序添加一个简单的基于 JWT 的身份验证流程。

json-server 的工作方式是它从文件系统中读取一个 JSON 文件,并使用它来创建一个具有相应端点的内存数据库以与之交互。现在让我们创建 JSON 文件。在你的项目路径中,创建一个新backend文件夹并在该文件夹中创建一个新db.json文件:

mkdir backend
cd backend
touch db.json

打开db.json并添加以下内容:

{
  "users": [
    {
      "email": "regular@example.com",
      "password": "$2a$10$2myKMolZJoH.q.cyXClQXufY1Mc7ETKdSaQQCC6Fgtbe0DCXRBELG",
      "id": 1
    },
    {
      "email": "admin@example.com",
      "password": "$2a$10$w8qB40MdYkMs3dgGGf0Pu.xxVOOzWdZ5/Nrkleo3Gqc88PF/OQhOG",
      "id": 2
    }
  ],
  "products": [
    {
      "id": "hdmdu0t80yjkfqselfc",
      "name": "shoes",
      "stock": 10,
      "price": 399.99,
      "shortDesc": "Nulla facilisi. Curabitur at lacus ac velit ornare lobortis.",
      "description": "Cras sagittis. Praesent nec nisl a purus blandit viverra. Ut leo. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Fusce a quam."
    },
    {
      "id": "3dc7fiyzlfmkfqseqam",
      "name": "bags",
      "stock": 20,
      "price": 299.99,
      "shortDesc": "Nulla facilisi. Curabitur at lacus ac velit ornare lobortis.",
      "description": "Cras sagittis. Praesent nec nisl a purus blandit viverra. Ut leo. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Fusce a quam."
    },
    {
      "id": "aoe8wvdxvrkfqsew67",
      "name": "shirts",
      "stock": 15,
      "price": 149.99,
      "shortDesc": "Nulla facilisi. Curabitur at lacus ac velit ornare lobortis.",
      "description": "Cras sagittis. Praesent nec nisl a purus blandit viverra. Ut leo. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Fusce a quam."
    },
    {
      "id": "bmfrurdkswtkfqsf15j",
      "name": "shorts",
      "stock": 5,
      "price": 109.99,
      "shortDesc": "Nulla facilisi. Curabitur at lacus ac velit ornare lobortis.",
      "description": "Cras sagittis. Praesent nec nisl a purus blandit viverra. Ut leo. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Fusce a quam."
    }
  ]
}

我们在这里创建两个资源 -usersproducts. 查看users资源,你会注意到每个用户都有一个 ID、一个电子邮件地址和一个密码。密码显示为一堆字母和数字,因为它是使用bcryptjs加密的。不要在应用程序的任何地方以纯文本形式存储密码,这一点很重要。

也就是说,每个密码的纯文本版本只是“密码”——没有引号。

现在通过从项目的根目录发出以下命令来启动服务器:

./node_modules/.bin/json-server-auth ./backend/db.json --port 3001

这将在 .json 上启动 json-server http://localhost:3001。感谢 json-server-auth 中间件,该users资源还将为我们提供一个/login端点,我们可以使用它来模拟登录到应用程序。

让我们使用https://hoppscotch.io试试看。在新窗口中打开该链接,然后将方法更改为POST并将 URL更改为http://localhost:3001/login。接下来,确保将原始输入开关设置为打开并输入以下内容作为原始请求正文

{
  "email": "regular@example.com",
  "password": "password"
}

单击发送,你应该会收到如下所示的响应(在页面下方):

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJlZ3VsYXJAZXhhbXBsZS5jb20iLCJpYXQiOjE2MDE1Mzk3NzEsImV4cCI6MTYwMTU0MzM3MSwic3ViIjoiMSJ9.RAFUYXxG2Z8W8zv5-4OHun8CmCKqi7IYqYAc4R7STBM"
}

那是一个 JSON Web Token,有效期为一个小时。在具有适当后端的普通应用程序中,你将其保存在客户端中,然后在你请求受保护资源时将其发送到服务器。服务器将验证它收到的令牌,如果一切都检查完毕,它将用你请求的数据进行响应。

这一点值得重复。你需要验证对服务器上受保护资源的任何请求。这是因为在客户端中运行的代码可能会被逆向工程和篡改。

这是Hoppscotch上已完成请求的链接。你只需要按发送

如果你想了解有关在 Node.js 中使用 JSON Web 令牌的更多信息,请参阅我们的教程。

在 React App 中实现身份验证

如何使用React创建电子商务网站?在本节中,我们将需要应用程序中的 axios 和 jwt_decode 包。将导入添加到App.js文件的顶部:

import axios from 'axios';
import jwt_decode from 'jwt-decode';

如果你查看类的顶部,你会看到我们已经声明了一个用户处于状态。这最初设置为空。

接下来,我们需要通过将用户设置为组件安装来确保在应用程序启动时加载用户,如下所示。将此方法添加到App组件中,该组件将本地存储中的最后一个用户会话加载到状态(如果存在):

componentDidMount() {
  let user = localStorage.getItem("user");
  user = user ? JSON.parse(user) : null;
  this.setState({ user });
}

接下来,我们定义附加到上下文的loginlogout方法:

login = async (email, password) => {
  const res = await axios.post(
    'http://localhost:3001/login',
    { email, password },
  ).catch((res) => {
    return { status: 401, message: 'Unauthorized' }
  })

  if(res.status === 200) {
    const { email } = jwt_decode(res.data.accessToken)
    const user = {
      email,
      token: res.data.accessToken,
      accessLevel: email === 'admin@example.com' ? 0 : 1
    }

    this.setState({ user });
    localStorage.setItem("user", JSON.stringify(user));
    return true;
  } else {
    return false;
  }
}

logout = e => {
  e.preventDefault();
  this.setState({ user: null });
  localStorage.removeItem("user");
};

login方法向我们的/login端点发出一个 Ajax 请求,将用户在登录表单中输入的任何内容传递给它(我们将在一分钟内完成)。如果端点的响应具有 200 状态代码,我们可以假设用户的凭据是正确的。然后我们解码服务器响应中发送的令牌以获取用户的电子邮件,然后保存电子邮件、令牌和用户访问级别的状态。如果一切顺利,该方法返回true,否则返回false。我们可以在我们的Login组件中使用这个值来决定显示什么。

请注意,此处对访问级别的检查是一项非常肤浅的检查,对于已登录的普通用户来说,将自己设为管理员并不困难。但是,假设在发送响应之前在服务器上验证了对受保护资源的请求,用户将只能看到一个额外的按钮。服务器验证将确保他们无法获取任何受保护的数据。

如果你想实现更强大的解决方案,你可以在用户登录时或应用程序加载时发出第二个请求以获取当前用户的权限。不幸的是,这超出了本教程的范围。

logout方法从状态和本地存储中清除用户。

创建登录组件

React创建电商网站实例:接下来,我们可以处理Login组件。该组件使用上下文数据。为了能够访问这些数据和方法,它必须使用withContext我们之前创建的方法进行包装。

src/Login.js像这样改变:

import React, { Component } from "react";
import { Redirect } from "react-router-dom";
import withContext from "../withContext";

class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: ""
    };
  }

  handleChange = e => this.setState({ [e.target.name]: e.target.value, error: "" });

  login = (e) => {
    e.preventDefault();

    const { username, password } = this.state;
    if (!username || !password) {
      return this.setState({ error: "Fill all fields!" });
    }
    this.props.context.login(username, password)
      .then((loggedIn) => {
        if (!loggedIn) {
          this.setState({ error: "Invalid Credentails" });
        }
      })
  };

  render() {
    return !this.props.context.user ? (
      <>
        <div className="hero is-primary ">
          <div className="hero-body container">
            <h4 className="title">Login</h4>
          </div>
        </div>
        <br />
        <br />
        <form onSubmit={this.login}>
          <div className="columns is-mobile is-centered">
            <div className="column is-one-third">
              <div className="field">
                <label className="label">Email: </label>
                <input
                  className="input"
                  type="email"
                  name="username"
                  onChange={this.handleChange}
                />
              </div>
              <div className="field">
                <label className="label">Password: </label>
                <input
                  className="input"
                  type="password"
                  name="password"
                  onChange={this.handleChange}
                />
              </div>
              {this.state.error && (
                <div className="has-text-danger">{this.state.error}</div>
              )}
              <div className="field is-clearfix">
                <button
                  className="button is-primary is-outlined is-pulled-right"
                >
                  Submit
                </button>
              </div>
            </div>
          </div>
        </form>
      </>
    ) : (
      <Redirect to="/products" />
    );
  }
}

export default withContext(Login);

此组件呈现具有两个输入的表单以收集用户登录凭据。提交时,组件调用login通过上下文传递的方法。如果用户已经登录,此模块还确保重定向到产品页面。

如果你现在转到http://localhost:3000/login,你应该能够使用上述名称/密码组合登录。

如何使用React创建电子商务网站?分步指南

React电商网站创建教程:创建产品视图

现在我们需要从后端获取一些产品以显示在我们的应用程序中。我们可以再次在组件中的组件挂载上执行此操作App,就像我们对登录用户所做的那样:

async componentDidMount() {
  let user = localStorage.getItem("user");
  const products = await axios.get('http://localhost:3001/products');
  user = user ? JSON.parse(user) : null;
  this.setState({ user,  products: products.data });
}

在上面的代码片段中,我们将componentDidMount生命周期钩子标记为async,这意味着我们可以向我们的/products端点发出请求,然后等待数据返回,然后再将其粘贴到状态。

接下来,我们可以创建产品页面,该页面也将作为应用程序登陆页面。此页面将使用两个组件。第一个是ProductList.js,它将显示页面正文,另一个是ProductItem.js列表中每个产品的组件。

修改Productlist组件,如下图:

import React from "react";
import ProductItem from "./ProductItem";
import withContext from "../withContext";

const ProductList = props => {
  const { products } = props.context;

  return (
    <>
      <div className="hero is-primary">
        <div className="hero-body container">
          <h4 className="title">Our Products</h4>
        </div>
      </div>
      <br />
      <div className="container">
        <div className="column columns is-multiline">
          {products && products.length ? (
            products.map((product, index) => (
              <ProductItem
                product={product}
                key={index}
                addToCart={props.context.addToCart}
              />
            ))
          ) : (
            <div className="column">
              <span className="title has-text-grey-light">
                No products found!
              </span>
            </div>
          )}
        </div>
      </div>
    </>
  );
};

export default withContext(ProductList);

由于列表依赖于数据的上下文,因此我们也用withContext函数包装它。该组件使用ProductItem我们尚未创建的组件呈现产品。它还addToCart从上下文(我们还没有定义)传递一个方法到ProductItem. 这消除了直接在ProductItem组件中使用上下文的需要。

现在让我们创建ProductItem组件:

cd src/components
touch ProductItem.js

并添加以下内容:

import React from "react";

const ProductItem = props => {
  const { product } = props;
  return (
    <div className=" column is-half">
      <div className="box">
        <div className="media">
          <div className="media-left">
            <figure className="image is-64x64">
              <img
                src="https://bulma.io/images/placeholders/128x128.png" alt="如何使用React创建电子商务网站?分步指南"
                alt={product.shortDesc}
              />
            </figure>
          </div>
          <div className="media-content">
            <b style={{ textTransform: "capitalize" }}>
              {product.name}{" "}
              <span className="tag is-primary">${product.price}</span>
            </b>
            <div>{product.shortDesc}</div>
            {product.stock > 0 ? (
              <small>{product.stock + " Available"}</small>
            ) : (
              <small className="has-text-danger">Out Of Stock</small>
            )}
            <div className="is-clearfix">
              <button
                className="button is-small is-outlined is-primary   is-pulled-right"
                onClick={() =>
                  props.addToCart({
                    id: product.name,
                    product,
                    amount: 1
                  })
                }
              >
                Add to Cart
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default ProductItem;

此元素在卡片上显示产品,还提供了一个操作按钮来将产品添加到用户的购物车中。

如何使用React创建电子商务网站?分步指南

添加产品

如何使用React创建电子商务网站?现在我们在商店中展示了一些东西,让我们为管理员用户创建一个界面来添加新产品。首先,让我们定义添加产品的方法。我们将在App组件中执行此操作,如下所示:

addProduct = (product, callback) => {
  let products = this.state.products.slice();
  products.push(product);
  this.setState({ products }, () => callback && callback());
};

此方法接收product对象并将其附加到产品数组,然后将其保存到应用程序状态。它还接收一个回调函数以在成功添加产品时执行。

现在我们可以继续填写AddProduct组件:

import React, { Component } from "react";
import withContext from "../withContext";
import { Redirect } from "react-router-dom";
import axios from 'axios';

const initState = {
  name: "",
  price: "",
  stock: "",
  shortDesc: "",
  description: ""
};

class AddProduct extends Component {
  constructor(props) {
    super(props);
    this.state = initState;
  }

  save = async (e) => {
    e.preventDefault();
    const { name, price, stock, shortDesc, description } = this.state;

    if (name && price) {
      const id = Math.random().toString(36).substring(2) + Date.now().toString(36);

      await axios.post(
        'http://localhost:3001/products',
        { id, name, price, stock, shortDesc, description },
      )

      this.props.context.addProduct(
        {
          name,
          price,
          shortDesc,
          description,
          stock: stock || 0
        },
        () => this.setState(initState)
      );
      this.setState(
        { flash: { status: 'is-success', msg: 'Product created successfully' }}
      );

    } else {
      this.setState(
        { flash: { status: 'is-danger', msg: 'Please enter name and price' }}
      );
    }
  };

  handleChange = e => this.setState({ [e.target.name]: e.target.value, error: "" });

  render() {
    const { name, price, stock, shortDesc, description } = this.state;
    const { user } = this.props.context;

    return !(user && user.accessLevel < 1) ? (
      <Redirect to="/" />
    ) : (
      <>
        <div className="hero is-primary ">
          <div className="hero-body container">
            <h4 className="title">Add Product</h4>
          </div>
        </div>
        <br />
        <br />
        <form onSubmit={this.save}>
          <div className="columns is-mobile is-centered">
            <div className="column is-one-third">
              <div className="field">
                <label className="label">Product Name: </label>
                <input
                  className="input"
                  type="text"
                  name="name"
                  value={name}
                  onChange={this.handleChange}
                  required
                />
              </div>
              <div className="field">
                <label className="label">Price: </label>
                <input
                  className="input"
                  type="number"
                  name="price"
                  value={price}
                  onChange={this.handleChange}
                  required
                />
              </div>
              <div className="field">
                <label className="label">Available in Stock: </label>
                <input
                  className="input"
                  type="number"
                  name="stock"
                  value={stock}
                  onChange={this.handleChange}
                />
              </div>
              <div className="field">
                <label className="label">Short Description: </label>
                <input
                  className="input"
                  type="text"
                  name="shortDesc"
                  value={shortDesc}
                  onChange={this.handleChange}
                />
              </div>
              <div className="field">
                <label className="label">Description: </label>
                <textarea
                  className="textarea"
                  type="text"
                  rows="2"
                  style={{ resize: "none" }}
                  name="description"
                  value={description}
                  onChange={this.handleChange}
                />
              </div>
              {this.state.flash && (
                <div className={`notification ${this.state.flash.status}`}>
                  {this.state.flash.msg}
                </div>
              )}
              <div className="field is-clearfix">
                <button
                  className="button is-primary is-outlined is-pulled-right"
                  type="submit"
                  onClick={this.save}
                >
                  Submit
                </button>
              </div>
            </div>
          </div>
        </form>
      </>
    );
  }
}

export default withContext(AddProduct);

这个组件做了很多事情。它检查是否有当前用户存储在上下文中,以及该用户是否accessLevel小于 1(即,如果他们是管理员)。如果是这样,它会呈现表单以添加新产品。如果没有,它会重定向到应用程序的主页。

再次请注意,客户端可以轻松绕过此检查。在实际应用中,你需要对服务器执行额外检查,以确保允许用户创建新产品。

假设表单已呈现,有几个字段供用户填写(其中nameprice是强制性的)。无论用户输入什么,都会在组件的状态中进行跟踪。当表单被提交时,组件的save方法被调用,它向我们的后端发出一个 Ajax 请求以创建一个新产品。我们还创建了一个唯一的 ID(这是 json-server 所期望的)并将其传递出去。此代码来自Stack Overflow 上的一个线程。

最后,我们调用addProduct通过上下文接收到的方法,将新创建的产品添加到我们的全局状态并重置表单。假设所有这些都成功了,我们flash在 state 中设置了一个属性,然后它会更新界面以通知用户产品已创建。

如果缺少nameprice字段,我们设置flash属性以通知用户这一点。

如何使用React创建电子商务网站?分步指南

花点时间检查一下你的进度。以管理员身份登录(电子邮件:admin@example.com,密码:password)并确保你在导航中看到“添加产品”按钮。导航到此页面,然后使用表单创建几个新产品。最后,返回主页并确保新产品显示在产品列表中。

添加购物车管理

现在我们可以添加和显示产品,最后要做的是实现我们的购物车管理。我们已经在 中将我们的购物车初始化为一个空对象App.js,但我们还需要确保在组件加载时从本地存储加载现有购物车。

更新componentDidMount方法App.js如下:

async componentDidMount() {
  let user = localStorage.getItem("user");
  let cart = localStorage.getItem("cart");

  const products = await axios.get('http://localhost:3001/products');
  user = user ? JSON.parse(user) : null;
  cart = cart? JSON.parse(cart) : {};

  this.setState({ user,  products: products.data, cart });
}

接下来,我们需要定义购物车功能(也在 中App.js)。首先,我们将创建addToCart方法:

addToCart = cartItem => {
  let cart = this.state.cart;
  if (cart[cartItem.id]) {
    cart[cartItem.id].amount += cartItem.amount;
  } else {
    cart[cartItem.id] = cartItem;
  }
  if (cart[cartItem.id].amount > cart[cartItem.id].product.stock) {
    cart[cartItem.id].amount = cart[cartItem.id].product.stock;
  }
  localStorage.setItem("cart", JSON.stringify(cart));
  this.setState({ cart });
};

此方法使用项目 ID 作为购物车对象的键来附加项目。我们正在为购物车使用一个对象而不是一个数组来实现轻松的数据检索。此方法检查购物车对象以查看是否存在具有该键的项目。如果是,则增加数量;否则它会创建一个新条目。第二个if语句确保用户不能添加比实际可用的更多的项目。然后该方法将购物车保存到状态,状态通过上下文传递到应用程序的其他部分。最后,该方法将更新后的购物车保存到本地存储以进行持久化。

接下来,我们将定义removeFromCart从用户购物车中删除特定产品和从用户购物车clearCart中删除所有产品的方法:

removeFromCart = cartItemId => {
  let cart = this.state.cart;
  delete cart[cartItemId];
  localStorage.setItem("cart", JSON.stringify(cart));
  this.setState({ cart });
};

clearCart = () => {
  let cart = {};
  localStorage.removeItem("cart");
  this.setState({ cart });
};

removeCart方法使用提供的产品密钥删除产品。然后它相应地更新应用程序状态和本地存储。该clearCart方法将购物车重置为空对象状态并删除本地存储中的购物车条目。

如何使用React创建电子商务网站?分步指南

React创建电商网站实例:现在,我们可以继续制作购物车用户界面。与产品列表类似,我们使用两个元素实现此目的:第一个,Cart.js呈现页面布局,以及使用第二个组件的购物车项目列表CartItem.js

// ./src/components/Cart.js

import React from "react";
import withContext from "../withContext";
import CartItem from "./CartItem";

const Cart = props => {
  const { cart } = props.context;
  const cartKeys = Object.keys(cart || {});
  return (
    <>
      <div className="hero is-primary">
        <div className="hero-body container">
          <h4 className="title">My Cart</h4>
        </div>
      </div>
      <br />
      <div className="container">
        {cartKeys.length ? (
          <div className="column columns is-multiline">
            {cartKeys.map(key => (
              <CartItem
                cartKey={key}
                key={key}
                cartItem={cart[key]}
                removeFromCart={props.context.removeFromCart}
              />
            ))}
            <div className="column is-12 is-clearfix">
              <br />
              <div className="is-pulled-right">
                <button
                  onClick={props.context.clearCart}
                  className="button is-warning "
                >
                  Clear cart
                </button>{" "}
                <button
                  className="button is-success"
                  onClick={props.context.checkout}
                >
                  Checkout
                </button>
              </div>
            </div>
          </div>
        ) : (
          <div className="column">
            <div className="title has-text-grey-light">No item in cart!</div>
          </div>
        )}
      </div>
    </>
  );
};

export default withContext(Cart);

Cart组件还将一个方法从上下文传递到CartItem. 该Cart组件循环遍历上下文购物车对象值的数组,并CartItem为每个值返回一个。它还提供了一个按钮来清除用户购物车。

接下来是CartItem组件,它与组件非常相似,ProductItem但有一些细微的变化:

让我们先创建组件:

cd src/components
touch CartItem.js

然后添加以下内容:

import React from "react";

const CartItem = props => {
  const { cartItem, cartKey } = props;

  const { product, amount } = cartItem;
  return (
    <div className=" column is-half">
      <div className="box">
        <div className="media">
          <div className="media-left">
            <figure className="image is-64x64">
              <img
                src="https://bulma.io/images/placeholders/128x128.png" alt="如何使用React创建电子商务网站?分步指南"
                alt={product.shortDesc}
              />
            </figure>
          </div>
          <div className="media-content">
            <b style={{ textTransform: "capitalize" }}>
              {product.name}{" "}
              <span className="tag is-primary">${product.price}</span>
            </b>
            <div>{product.shortDesc}</div>
            <small>{`${amount} in cart`}</small>
          </div>
          <div
            className="media-right"
            onClick={() => props.removeFromCart(cartKey)}
          >
            <span className="delete is-large"></span>
          </div>
        </div>
      </div>
    </div>
  );
};

export default CartItem;

此组件显示产品信息和所选项目的数量。它还提供了一个从购物车中移除产品的按钮。

最后,我们需要在App组件中添加 checkout 方法:

checkout = () => {
  if (!this.state.user) {
    this.routerRef.current.history.push("/login");
    return;
  }

  const cart = this.state.cart;

  const products = this.state.products.map(p => {
    if (cart[p.name]) {
      p.stock = p.stock - cart[p.name].amount;

      axios.put(
        `http://localhost:3001/products/${p.id}`,
        { ...p },
      )
    }
    return p;
  });

  this.setState({ products });
  this.clearCart();
};

此方法在继续之前检查用户是否已登录。如果用户没有登录,它会使用我们之前附加到Router组件的路由器引用将用户重定向到登录页面。

通常,在常规电子商务网站中,这是计费流程发生的地方,但对于我们的应用程序,我们将假设用户已付款,因此从可用项目列表中删除他们购买的项目。我们还将使用 axios 更新后端的库存水平。

有了这个,我们已经成功地完成了我们的基本购物车。

如何使用React创建电子商务网站?分步指南

React电商网站创建教程结论

如何使用React创建电子商务网站?在本教程中,我们使用 React 搭建了一个基本购物车的界面。我们使用上下文在多个组件和 json-server 之间移动数据和方法来持久化数据。我们还使用 json-server auth 来实现基本的身份验证流程。

此应用程序绝不是成品,可以在许多方面进行改进。例如,下一步是为数据库添加适当的后端并在服务器上执行身份验证检查。你还可以让管理员用户能够编辑和删除产品。

我希望你喜欢这个教程。请不要忘记此应用程序的代码可在GitHub上找到。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: