七、使用 GraphQL 创建购物车系统

在上一章中,我们使用 Vue 3 和 Express 构建了一个旅游预订系统。这是我们从零开始构建前端使用的自己的后端的第一个项目。拥有自己的后端可以让我们做很多其他无法完成的事情,例如,我们可以在自己创建的数据库中保存我们喜欢的数据。此外,我们还添加了自己的身份验证系统来验证管理员用户。在管理员前端,我们使用beforeEnter路由保护来保护我们的路由,它在管理员用户登录之前检查身份验证令牌。

在本章中,我们将了解以下主题:

  • 介绍 GraphQL应用编程接口API
  • 使用 Express 创建 GraphQLAPI
  • 创建管理前端
  • 创建客户前端

技术要求

本章项目代码见https://github.com/PacktPublishing/-Vue.js-3-By-Example/tree/master/Chapter07

引入 GraphQL API

在上一章中,我们使用 Express 创建了一个后端。端点接受 JSON 数据作为输入,并返回 JSON 数据作为响应。但是,它可以接受任何 JSON 数据,这是后端可能不期望的。此外,如果没有前端,就没有简单的方法来测试我们的 API 端点。这是我们可以用 GraphQLAPI 解决的问题。GraphQL是一种特殊的查询语言,使得客户端和服务器之间的通信更加容易。GraphQLAPI 具有内置的数据结构验证。每个属性都有一个数据类型,可以是简单类型,也可以是复杂类型,由许多具有简单数据类型的属性组成。

我们还可以使用 GraphiQL 测试 GraphQLAPI,GraphiQL 是一个允许我们轻松地发出自己的 GraphQLAPI 请求的网页。由于每个请求都有数据类型验证,因此根据 GraphQLAPI 模式的定义,它可以提供自动完成功能。该模式为我们提供了查询和突变使用的所有数据类型定义。查询是允许我们使用 GraphQL API 查询数据的请求,而突变是允许我们以某种方式更改数据的 GraphQL 请求。

我们使用模式字符串显式定义查询和突变。查询和转换将输入类型作为输入数据的数据类型,并使用指定的输出数据类型返回数据。因此,我们永远不会对发出 GraphQL 请求所需发送的数据的结构有任何疑问,也永远不会猜测请求将返回什么样的数据。

GraphQL API 请求除了具有特殊的结构外,大多只是常规的超文本传输协议HTTP请求)。默认情况下,所有请求都会到达/graphql端点,我们在 JSON 请求中以query属性的字符串值发送查询或突变。变量值随variable参数一起发送。

查询和突变被命名,所有查询和突变都被发送到代码中具有相同名称的解析器函数,而不是路由处理程序。然后,这些函数接受模式指定的参数,之后我们可以获取请求数据,并在解析器函数代码中对其执行我们想要的操作。

使用 Vue 3 应用,我们可以使用专门的 GraphQL API 客户端来简化 GraphQL API 请求的创建。我们所要做的就是为查询和突变传递一个字符串,以及查询和突变所伴随的变量。

在本章中,我们将使用 Vue 3 创建一个带有管理员前端和客户前端的购物车系统。然后,我们将使用 Express 和express-graphql库创建一个后端,该库接受 GraphQLAPI 请求并将数据存储在 SQLite 数据库中。

建立购物车系统项目

为了创建假期预订项目,我们必须为前端、管理前端和后端创建子项目。要创建前端和管理前端项目,我们将使用 Vue CLI。要创建后端项目,我们将使用express-generator全局包。

为了设置本章的项目,我们执行以下步骤:

  1. 首先,我们创建一个文件夹来存放所有项目,并将其命名为shopping-cart
  2. 然后在主文件夹中创建admin-frontendfrontendbackend文件夹。
  3. 接下来,我们进入admin-frontend文件夹并运行npx vue create将 Vue 项目的脚手架代码添加到admin-frontend文件夹中。
  4. 如果要求我们在当前文件夹中创建项目,我们选择Y,然后当要求我们选择项目的 Vue 版本时,我们选择Vue 3。同样,我们对[T2]文件夹以相同的方式运行 Vue CLI。
  5. To create the Express project, we run the Express application generator app. To run it, we go into the backend folder and then run npx express-generator.

    此命令将把项目所需的所有文件添加到backend文件夹中。如果出现错误,请尝试以管理员身份运行express-generator包。

现在我们已经完成了项目的设置,我们可以开始编写代码了。接下来,我们将从创建 GraphQL 后端开始。

使用 Express 创建 GraphQL API

为了启动购物车系统项目,我们首先使用 Express 创建一个 GraphQL API。我们从后端开始,因为两个前端都需要它。首先,我们必须添加一些库,这些库是操作 SQLite 数据库和向我们的应用添加身份验证所必需的。此外,我们需要库在我们的应用中启用跨来源资源共享CORS

CORS 是一种让我们从浏览器向承载前端的不同域中的端点发出请求的方法。

为了让我们的 Express 应用接受 GraphQL 请求,我们使用graphqlexpress-graphql库。要安装这两个,我们运行以下命令:

npm i cors jsonwebtoken sqlite3 express-graphql graphql

安装包之后,我们就可以开始编写代码了。

使用分解器功能

首先,我们研究解析器函数。要添加它们,我们首先将一个resolvers文件夹添加到backend文件夹中。然后,我们可以使用解析器进行身份验证。在resolvers文件夹中,我们创建一个auth.js文件,并编写以下代码:

const jwt = require('jsonwebtoken');
module.exports = {
  login: ({ user: { username, password } }) => {
    if (username === 'admin' && password === 'password') {
      return { token: jwt.sign({ username }, 'secret') }
    }
    throw new Error('authentication failed');
  }
}

login方法是一个解析器函数。它使用user object属性和usernamepassword属性,我们使用它们来检查凭证。我们检查用户名是否为'admin',密码是否为'password'。如果凭据正确,那么我们将发出令牌。否则,我们抛出一个错误,/graphql端点会将其作为错误响应返回。

为顺序逻辑添加解析程序

接下来,我们为顺序逻辑添加解析器。在resolvers文件夹中,我们添加了orders.js文件。然后,我们使用解析器函数来获取订单数据。订单数据包含有关订单本身以及客户购买的商品的信息。要添加解析器,我们编写以下代码:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  getOrders: () => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve, reject) => {
      db.serialize(() => {
        db.all(`
          SELECT *,
            orders.name AS purchaser_name,
            shop_items.name AS shop_item_name
          FROM orders
          INNER JOIN order_shop_items ON orders.order_id = 
            order_shop_items.order_id
          INNER JOIN shop_items ON 
           order_shop_items.shop_item_id = shop_items.
             shop_item_id
        `, [], (err, rows = []) => {
          ...
        });
      })
      db.close();
    })
  },
  ...
}

我们使用sqlite3.Database构造函数打开数据库,其中包含数据库的路径。然后,我们返回一个承诺,该承诺将查询所有订单以及客户购买的物品。订单在orders表中。商店库存物品存储在shop_items表中,我们有order_shop_items表来链接订单和购买的物品。

我们使用db.all方法进行select查询以获取所有数据,并使用inner join连接所有相关表以获取其他表中的相关数据。在回调中,我们编写以下代码来循环行以创建order对象:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  getOrders: () => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve, reject) => {
      db.serialize(() => {
        db.all(`
          ...
        `, [], (err, rows = []) => {
          if (err) {
            reject(err)
...
          const orderArr = Object.values(orders)
          for (const order of orderArr) {
            order.ordered_items = rows
              .filter(({ order_id }) => order_id === 
                order.order_id)
              .map(({ shop_item_id, shop_item_name: name, 
                price, description }) => ({
                shop_item_id, name, price, description
              }))
          }
          resolve(orderArr)
        });
      })
      db.close();
    })
  },
  ...
}

这允许我们删除行中的重复订单条目。键是order_id值,该值是订单数据本身。然后,我们使用Object.values方法获得所有订单值。我们将返回的数组分配给orderArr变量。然后,我们循环通过orderArr数组,使用filter方法从原始行的数组中获取所有订购的店铺商品,通过order_id查找商品。我们调用map从行中提取订单的车间物料数据。

我们调用数据上的resolve,将其作为/graphql端点的响应返回。在回调的前几行中,当err为 truthy 时,我们调用reject,以便我们可以将错误返回给用户(如果有)。

最后,完成后,我们调用db.close()关闭数据库。最后我们可以这样做,因为我们使用db.serializeserialize回调中一系列地运行所有语句,所以可以顺序运行结构化查询语言SQL代码。

添加订单

我们添加了一个解析器函数来添加订单。为此,我们编写以下代码:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  ...
  addOrder: ({ order: { name, address, phone, ordered_items:
    orderedItems } }) => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve) => {
      db.serialize(() => {
        const orderStmt = db.prepare(`
          INSERT INTO orders (
            name,
            address,
            phone
...
                  shop_item_id: shopItemId
                } = orderItem
                orderShopItemStmt.run(orderId, shopItemId)
              }
              orderShopItemStmt.finalize()
            })
            resolve({ status: 'success' })
            db.close();
          });
      })
    })
  },
  ...
}

我们获得订单的请求有效负载,以及在参数中分解的变量。我们以相同的方式打开数据库,并使用相同的承诺代码和db.serialize调用启动,但在其中,我们使用db.prepare方法创建了一个准备好的语句。我们发出一个INSERT语句将数据添加到订单条目中。

然后,我们使用要插入的变量值调用run,以运行 SQL 语句。准备好的语句很好,因为我们传递给db.run的所有变量值都经过了消毒,以防止 SQL 注入攻击。然后,我们调用finalize来提交事务。

接下来,我们通过db.all调用和SELECT语句获取刚刚插入orders表的行的 ID 值。在db.all方法的回调中,我们获取返回的数据,并从返回的数据中解构orderId

然后,我们创建另一个准备好的语句,将购买的商店商品的数据插入order_shop_items表中。我们只需插入order_idshop_item_id即可将订单与购买的商店商品联系起来。

我们循环通过orderedItems数组并调用run来添加条目,然后调用finalize来完成所有数据库事务。

最后,我们调用resolve向客户端返回成功响应。

为了完成这个文件,我们添加了removeOrder解析器,以便从数据库中删除订单。为此,我们编写以下代码:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  ...
  removeOrder: ({ orderId }) => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve) => {
      db.serialize(() => {
        const delOrderShopItemsStmt = db.prepare("DELETE FROM 
          order_shop_items WHERE order_id = (?)");
        delOrderShopItemsStmt.run(orderId)
        delOrderShopItemsStmt.finalize();
        const delOrderStmt = db.prepare("DELETE FROM orders 
          WHERE order_id = (?)");
        delOrderStmt.run(orderId)
        delOrderStmt.finalize();
        resolve({ status: 'success' })
      })
      db.close();
    })
  },
}

我们调用db.serializedb.prepare的方式与之前相同。唯一的区别是我们发布了DELETE语句,删除了order_shop_itemsorders表中给定order_id的所有内容。我们需要先从order_shop_items表中删除项目,因为订单仍在那里被引用。

一旦我们除去了orders表之外的订单的所有引用,我们就可以删除orders表中的订单本身。

获取商店物品

我们在resolvers文件夹中创建一个shopItems.js文件,用于保存解析器函数,以获取和设置商店项目。首先,我们从一个解析器函数开始,以获取所有商店商品。为此,我们编写以下代码:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  getShopItems: () => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve, reject) => {
      db.serialize(() => {
        db.all("SELECT * FROM shop_items", [], (err, rows = 
          []) => {
          if (err) {
            reject(err)
          }
          resolve(rows)
        });
      })
      db.close();
    })
  },
  ...
}

我们称之为db.serializedb.all,就像我们之前称之为一样。我们只需通过查询获取所有的shop_items条目,然后调用resolve将选择的数据作为响应返回给客户端。

添加分解器函数以添加车间项目

我们现在将添加一个解析器函数来添加一个车间项目。为此,我们编写以下代码:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  ...
  addShopItem: ({ shopItem: { name, description, image_url: 
    imageUrl, price } }) => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve) => {
      db.serialize(() => {
        const stmt = db.prepare(`
          INSERT INTO shop_items (
            name,
            description,
            image_url,
            price
          ) VALUES (?, ?, ?, ?)
        `
        );
        stmt.run(name, description, imageUrl, price)
        stmt.finalize();
        resolve({ status: 'success' })
      })
      db.close();
    })
  },
  ...
}

我们发出一个INSERT语句来插入一个条目,其中的值是从参数中解构出来的。

最后,我们通过编写下面的代码来添加removeShopItem解析器,让我们通过 ID 从shop_items表中删除一个条目:

const sqlite3 = require('sqlite3').verbose();
module.exports = {
  ...
  removeShopItem: ({ shopItemId }) => {
    const db = new sqlite3.Database('./db.sqlite');
    return new Promise((resolve) => {
      db.serialize(() => {
        const stmt = db.prepare("DELETE FROM shop_items WHERE 
          shop_item_id = (?)");
        stmt.run(shopItemId)
        stmt.finalize();
        resolve({ status: 'success' })
      })
      db.close();
    })
  },
}

将解析器映射到查询和突变

我们需要将解析器映射到查询和突变,以便在发出 GraphQLAPI 请求时调用它们。为此,我们转到app.js文件并添加一些内容。我们还将添加一些中间件,以便能够为某些请求启用跨域通信和令牌检查。为此,我们首先编写以下代码:

const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
const cors = require('cors')
const shopItemResolvers = require('./resolvers/shopItems')
const orderResolvers = require('./resolvers/orders')
const authResolvers = require('./resolvers/auth')
const jwt = require('jsonwebtoken');

我们使用require功能导入所需的所有内容。我们可以用前面的代码块替换文件顶部的所有内容。我们导入解析器、CORS 中间件、GraphQL 库项和jsonwebtoken模块。

接下来,我们通过调用buildSchema函数为 GraphQLAPI 创建模式。为此,我们编写以下代码:

...
const schema = buildSchema(`
  type Response {
    status: String
  }
  ...
  input Order {
    order_id: Int
    name: String
    address: String
    phone: String
    ordered_items: [ShopItem]
  }
  ...
  type Query {
    getShopItems: [ShopItemOutput],
    getOrders: [OrderOutput]
  }
  type Mutation {
    addShopItem(shopItem: ShopItem): Response
    removeShopItem(shopItemId: Int): Response
    addOrder(order: Order): Response
    removeOrder(orderId: Int): Response
    login(user: User): Token
  }
`);
...

完整的模式定义见https://github.com/PacktPublishing/-Vue.js-3-By-Example/blob/master/Chapter07/backend/app.js

我们有type关键字来定义响应的数据类型,我们有ResponseToken类型来用作响应。express-graphql库将根据数据类型中指定的内容检查响应的结构,因此返回Response类型数据的任何查询或变异都应该具有status string属性。这是可选的,因为字符串后面没有感叹号。

input关键字让我们定义一个input类型。input类型用于指定请求有效负载的数据结构。它们的定义方式与具有属性列表的output类型相同,其数据类型位于冒号之后。

我们可以将一种数据类型嵌套在另一种数据类型中,就像我们在OrderOutput类型中使用ordered_items属性所做的那样。我们指定它保存一个具有ShopItemOutput数据类型的对象数组。同样,我们在Order数据类型中为ordered_items属性指定了类似的数据类型。方括号表示数据类型是数组。

QueryMutation是特殊的数据类型,允许我们在冒号之前添加解析器名称,在冒号之后添加输出的数据类型。Query类型指定查询,Mutation类型指定突变。

接下来,我们通过编写以下代码来指定添加了所有解析程序的root对象:

const root = {
  ...shopItemResolvers,
  ...orderResolvers,
  ...authResolvers
}

我们只需将导入的所有解析器放入root对象,然后将所有条目分散到root对象中,将它们合并为一个对象。

然后,我们添加authMiddleware来为一些 GraphQL 请求添加身份验证检查。为此,我们编写以下代码:

const authMiddleware = (req, res, next) => {
  const { query } = req.body
  const token = req.get('authorization')
  const requiresAuth = query.includes('removeOrder') ||
    query.includes('removeShopItem') ||
    query.includes('addShopItem')
  if (requiresAuth) {
    try {
      jwt.verify(token, 'secret');
      next()
      return
    } catch (error) {
      res.status(401).json({})
      return
    }
  }
  next();
}

我们从 JSON 请求负载中获取query属性,以检查 GraphQL 请求正在调用哪个查询或变异。然后,我们使用req.get方法得到authorization报头。接下来,我们定义一个requiresAuth布尔变量来检查客户端是否发出调用受限查询或突变的请求。

如果是true,我们会调用jwt.verify来验证令牌的秘密。如果有效,则调用next继续到/graphql端点。否则,我们将返回一个401响应。如果querymutation属性不需要身份验证,那么我们只需调用next即可进入/graphql端点。

添加中间件

接下来,我们添加启用跨域通信所需的所有中间件,并添加/graphql端点以接受 GraphQL 请求。为此,我们编写以下代码:

...
const app = express();
app.use(cors())
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(authMiddleware)
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));
...

我们编写以下代码行以实现跨域通信:

app.use(cors())

下面的代码行允许我们接受 JSON 请求,我们也需要它来接受 GraphQL 请求:

app.use(express.json());

以下代码行将身份验证检查添加到受限 GraphQL 查询:

app.use(authMiddleware)

必须在以下代码块之前添加前一行代码:

app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));

这样,身份验证检查在 GraphQL 请求发出之前完成。最后,下面的代码块添加了一个/graphql端点,让我们接受 GraphQL 请求:

app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));

grapgqlHTTP函数在我们传入一组选项后返回一个中间件。我们为 GraphQLAPI 设置模式。rootValue属性有一个包含所有解析程序的对象。解析程序名称应与QueryMutation类型中指定的名称匹配。graphiql属性设置为true,这样我们可以在浏览器中转到/graphql页面时使用可用的 GraphiQL web 应用。

为了测试经过身份验证的端点,我们可以使用 Chrome 和 Firefox 提供的ModHeader扩展将带有令牌的身份验证头添加到请求头中。然后,我们可以轻松地测试经过身份验证的 GraphQL 请求。

笔记

可从下载扩展名 https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj?hl=en 用于 Chromium 浏览器和https://addons.mozilla.org/en-CA/firefox/addon/modheader-firefox/?utm_source=addons.mozilla.org &utm_ 媒体=推荐&utm_ 内容=搜索对于 Firefox。

下面的屏幕截图显示了 GraphiQL 接口的外观。我们还有ModHeader扩展,可以在屏幕右上角添加进行身份验证请求所需的标题:

Figure 7.1 – GraphiQL with ModHeader extension

图 7.1–带 ModHeader 扩展的 GraphiQL

接下来,我们通过编写以下代码创建一个db.sql脚本,让我们创建我们需要使用的数据库:

DROP TABLE IF EXISTS order_shop_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS shop_items;
CREATE TABLE shop_items (
  shop_item_id INTEGER NOT NULL PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT NOT NULL,
  price NUMBER NOT NULL,
  image_url TEXT NOT NULL
);
CREATE TABLE orders (
  order_id INTEGER NOT NULL PRIMARY KEY,
  name TEXT NOT NULL,
  address TEXT NOT NULL,
  phone TEXT NOT NULL
);
CREATE TABLE order_shop_items (
  order_id INTEGER NOT NULL,
  shop_item_id INTEGER NOT NULL,
  FOREIGN KEY (order_id) REFERENCES orders(order_id)
  FOREIGN KEY (shop_item_id) REFERENCES 
   shop_items(shop_item_id)
);

我们创建解析程序脚本中使用的表。TEXT让我们将文本存储在一列中;INTEGER让我们存储整数;FOREIGN KEY指定引用表中指定的列和REFERENCES之后的列的外键;NOT NULL需要设置一列;DROP TABLE IF EXISTS删除一个表(如果存在);CREATE TABLE创建一个表;PRIMARY KEY指定主键列。

创建 SQLite 数据库

为了创建和操作一个 SQLite 数据库,我们使用DB 浏览器进行 SQLiteDB4S程序,我们可以从下载该程序 https://sqlitebrowser.org/ 。该程序适用于 Windows、Mac 和 Linux。然后点击新建数据库并将db.sqlite数据库保存在backend文件夹中,以便后端可以访问该数据库。然后,在执行 SQL选项卡中,我们粘贴脚本以将表添加到数据库中。要将数据库的更改写入磁盘,必须保存这些更改。要执行此操作,请单击文件菜单,然后单击写入更改。我们也可以按Ctrl+S组合键保存更改。

最后,在package.json中,我们通过编写以下代码来更改start脚本:

{
  ...
  "scripts": {
    "start": "nodemon ./bin/www"
  },
  ...
}

我们切换nodemon,这样当我们更改代码并保存时应用将重新启动。我们运行npm I –g nodemon在全球范围内安装nodemon

现在我们已经完成了后端,我们可以转到前端,这样我们就有了一个完整的购物车系统。

创建管理前端

现在我们已经完成了后端应用,我们可以继续在前端工作了。因为我们之前已经在admin-frontend文件夹中为管理员前端创建了 Vue 3 项目,所以我们只需安装所需的软件包,然后再编写代码。我们需要graphql-requestGraphQL 包和 GraphQL 客户端库,以及 VeeValidate、Vue 路由、Axios 和 Yup 包。

要安装它们,我们在admin-frontend文件夹中运行以下命令:

npm i vee-validate@next vue-router@4 yup graphql graphql-request

安装包之后,我们可以开始编写代码。

使用部件

首先,我们开始部件的工作。在components文件夹中,我们通过写入以下代码将TopBar组件添加到components/TopBar.vue文件中,以保存路由链接和注销按钮:

<template>
  <p>
    <router-link to="/orders">Orders</router-link>
    <router-link to="/shop-items">Shop Items</router-link>
    <button @click="logOut">Log Out</button>
  </p>
</template>
<script>
export default {
  name: "TopBar",
  methods: {
    logOut() {
      localStorage.clear();
      this.$router.push("/");
    },
  },
};
</script>
<style scoped>
a {
  margin-right: 5px;
}
</style>

我们添加了 Vue 路由router-link组件,让管理员用户点击它们进入不同的页面。

点击时注销按钮运行logOut方法,用localStorage.clear清除本地存储,并用this.$router.push重定向回登录页面。/路径将映射到登录页面,稍后我们将看到。

接下来,在src/plugins文件夹中,我们添加router.js文件。为此,我们编写以下代码:

import { createRouter, createWebHashHistory } from 'vue-router'
import Login from '@/views/Login'
import Orders from '@/views/Orders'
import ShopItems from '@/views/ShopItems'
const beforeEnter = (to, from, next) => {
  try {
    const token = localStorage.getItem('token')
    if (to.fullPath !== '/' && !token) {
      return next({ fullPath: '/' })
    }
    return next()
  } catch (error) {
    return next({ fullPath: '/' })
  }
}
const routes = [
  { path: '/', component: Login },
  { path: '/orders', component: Orders, beforeEnter },
  { path: '/shop-items', component: ShopItems, beforeEnter },
]
const router = createRouter({
  history: createWebHashHistory(),
  routes,
})
export default router

我们添加了路由保护,检查认证令牌是否存储在本地存储器中。如果它已经被存储,并且我们将进入一个经过身份验证的路由,那么我们将通过调用next而不带任何参数进入页面。否则,我们通过调用next,将fullPath属性设置为'/'的对象重定向回登录页面。如果有任何错误,我们也会返回登录页面。

接下来,我们有一个带有路由映射的routes数组。我们在页面中点击链接到上的【URL】链接到上的上的【路由】上。我们将beforeEnter路由保护添加到需要身份验证的路由中。**

然后调用createRouter创建router对象,调用createWebHashHistory使用哈希模式。在哈希模式下,主机名和 URL 的其余部分将由一个#符号分隔。我们还将routes数组添加到传递到createRouter的对象中,以添加路由映射。

然后,我们导出router对象,以便稍后将其添加到我们的应用中。

接下来,我们创建登录页面组件。为此,我们创建views文件夹,向其中添加Login.vue文件,然后编写以下代码:

<template>
  <h1>Admin Login</h1>
  <Form :validationSchema="schema" @submit="submitForm">
    <div>
      <label for="name">Username</label>
      <br />
      <Field name="username" type="text" 
        placeholder="Username" />
      <ErrorMessage name="username" />
    </div>
    <br />
    <div>
      <label for="password">Password</label>
      <br />
      <Field name="password" placeholder="Password" 
        type="password" />
      <ErrorMessage name="password" />
    </div>
    <input type="submit" />
  </Form>
</template>

我们将带有validationSchema属性集的Form组件添加到yup模式中。我们监听submit事件,该事件在所有字段都有效时发出,然后单击提交按钮。submitForm方法将包含我们输入的表单字段值,Field组件允许我们创建表单字段。

ErrorMessage显示带有表单字段的错误消息。如果FieldErrorMessagename属性值匹配,则会自动显示给定名称字段的任何表单验证。placeholder属性允许我们添加表单占位符,type属性设置form输入类型。

接下来,我们添加组件的脚本部分。为此,我们编写以下代码:

<script>
import { GraphQLClient, gql } from "graphql-request";
import * as yup from "yup";
import { Form, Field, ErrorMessage } from "vee-validate";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL, {
  headers: {
    authorization: "",
  },
});
const schema = yup.object({
  name: yup.string().required(),
  password: yup.string().required(),
});
...
</script>

我们使用GraphQLClient构造函数创建 GraphQL 客户机对象。这需要 GraphQL 端点 URL 和我们可以传入的各种选项。我们将使用它在需要身份验证的组件中传递所需的请求头。

schema变量保存yup验证模式,该模式包含namepassword字段。这两个字段都是字符串,它们都是必需的,如方法调用所示。属性名称必须与FieldErrorMessage组件的name属性值相匹配,以便为字段触发验证。

添加登录逻辑并发出第一个 GraphQL 请求

接下来,我们通过编写以下代码来添加登录逻辑:

<script>
...
export default {
  name: "Login",
  components: {
    Form,
    Field,
    ErrorMessage,
  },
  data() {
    return {
      schema,
    };
  },
...
        } = await graphQLClient.request(mutation, variables);
        localStorage.setItem("token", token);
        this.$router.push('/orders')
      } catch (error) {
        alert("Login failed");
      }
    },
  },
};
</script>

我们注册从 VeeValidate 软件包导入的FormFieldErrorMessage组件。我们有data方法,它返回一个带有模式的对象,以便我们可以在模板中使用它。最后,我们使用submitForm方法,从Field组件中获取usernamepassword值,并发出登录变异 GraphQL 请求。

我们将$username$password值传递到括号中,以将它们传递到我们的突变中。这些值将从variablesvariables对象中获得,我们将其传递到graphQLClient.request方法中。如果请求成功,我们将从请求中取回令牌。一旦我们拿到代币,我们就把它放在localStorage.setItem中放入本地存储。

gql标记是一个函数,允许我们将字符串转换为可以发送到服务器的查询 JSON 对象。

如果登录请求失败,我们将显示警报。下面的屏幕截图显示了登录屏幕:

Figure 7.2 – Admin login screen

图 7.2–管理员登录屏幕

创建订单页面

接下来,我们通过创建一个views/Orders.vue文件来创建一个订单页面。为此,我们更新以下代码:

<template>
  <TopBar />
  <h1>Orders</h1>
  <div v-for="order of orders" :key="order.order_id">
    <h2>Order ID: {{ order.order_id }}</h2>
    <p>Name: {{ order.name }}</p>
    <p>Address: {{ order.address }}</p>
    <p>Phone: {{ order.phone }}</p>
    <div>
      <h3>Ordered Items</h3>
      <div
        v-for="orderedItems of order.ordered_items"
        :key="orderedItems.shop_item_id"
      >
        <h4>Name: {{ orderedItems.name }}</h4>
        <p>Description: {{ orderedItems.description }}</p>
        <p>Price: ${{ orderedItems.price }}</p>
      </div>
    </div>
    <p>
      <b>Total: ${{ calcTotal(order.ordered_items) }}</b>
    </p>
    <button type="button" @click="deleteOrder(order)">Delete 
      Order</button>
  </div>
</template>

我们添加TopBar并使用v-for循环订单以呈现条目。我们还循环通过ordered_items。我们使用calcTotal方法显示订购项目的总价。我们还有删除订单按钮,点击时调用deleteOrder方法。必须指定key道具,以便 Vue 3 能够识别项目。

接下来,我们通过编写以下代码,使用 GraphQL 客户端创建一个脚本:

<script>
import { GraphQLClient, gql } from "graphql-request";
import TopBar from '@/components/TopBar'
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL, {
  headers: {
    authorization: localStorage.getItem("token"),
  },
});
...
</script>

这与登录页面不同,因为我们将授权头设置为从本地存储获取的令牌。接下来,我们通过编写以下代码来创建 component 对象:

<script>
...
export default {
  name: "Orders",
  components: {
    TopBar
...
        {
          getOrders {
            order_id
            name
            address
            phone
            ordered_items {
              shop_item_id
              name
              description
              image_url
              price
            }
          }
        }
      `;
...
      await graphQLClient.request(mutation, variables);
      await this.getOrders();
    },
  },
};
</script>

我们使用components属性注册TopBar组件。我们有data方法返回一个具有orders反应性属性的对象。在beforeMount钩子中,我们调用getOrders方法来获取组件安装时的订单。calcTotal方法通过使用map从所有orderedItems对象中获取价格,然后调用reduce将所有价格相加,从而计算所有订购项目的总价。

getOrders方法发出 GraphQL 查询请求以获取所有订单。我们指定要在请求中获取的字段。我们为我们也要获取的嵌套对象指定字段,因此我们对[T1]也这样做。仅返回指定的字段。

然后,我们通过查询调用graphQlClient.request来发出查询请求,并将返回的数据分配给ordersreactive 属性。

deleteOrder方法获取order对象并向服务器发出removeOrder变异请求。orderId在变量中,因此正确的顺序将被删除。删除最新订单后,我们再次致电getOrders获取。

以下屏幕截图显示管理员看到的订单页面:

Figure 7.3 – Orders page: admin view

图 7.3–订单页面:管理视图

现在我们已经添加了 orders 页面,接下来我们将添加一个页面,让管理员添加和删除他们想在商店出售的物品。

添加和删除要出售的店铺项目

接下来,我们在中添加一个店铺项目页面,让我们添加和删除店铺项目。为此,我们从模板开始。我们通过编写以下代码来呈现商店项目:

<template>
  <TopBar />
  <h1>Shop Items</h1>
  <button @click="showDialog = true">Add Item to Shop</button>
  <div v-for="shopItem of shopItems" 
    :key="shopItem.shop_item_id">
    <h2>{{ shopItem.name }}</h2>
    <p>Description: {{ shopItem.description }}</p>
    <p>Price: ${{ shopItem.price }}</p>
    <img :src="shopItem.image_url" :alt="shopItem.name" />
    <br />
    <button type="button" @click="deleteItem(shopItem)">
      Delete Item from Shop
    </button>
  </div>
  ...
</template>

我们像以前一样添加TopBar组件,并像处理订单一样呈现shopItems

接下来,我们添加一个带有 HTML 对话框元素的对话框,以便添加商店项目。为此,我们编写以下代码:

<template>
  ...
  <dialog :open="showDialog" class="center">
    <h2>Add Item to Shop</h2>
    <Form :validationSchema="schema" @submit="submitForm">
      <div>
...
        <Field name="imageUrl" type="text" placeholder=" Image 
          URL" />
        <ErrorMessage name="imageUrl" />
      </div>
      <br />
      <div>
        <label for="price">Price</label>
        <br />
        <Field name="price" type="text" placeholder="Price" />
        <ErrorMessage name="price" />
      </div>
      <br />
      <input type="submit" />
      <button @click="showDialog = false" type="button">
        Cancel</button>
    </Form>
  </dialog>
</template>

我们将open属性设置为控制对话框打开的时间,并将类设置为center,以便我们可以应用样式使对话框居中,稍后将其显示在页面其余部分的上方。

在对话框中,我们以与登录页面相同的方式创建表单。唯一的区别是表单中的字段。在表单底部,我们有一个取消按钮,用于将showDialog反应属性设置为false以关闭对话框,因为它被设置为open属性的值。

接下来,我们使用 GraphQL 客户机和表单验证模式创建脚本(与之前一样),如下所示:

<script>
import { GraphQLClient, gql } from "graphql-request";
import * as yup from "yup";
import TopBar from "@/components/TopBar";
import { Form, Field, ErrorMessage } from "vee-validate";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL, {
  headers: {
    authorization: localStorage.getItem("token"),
  },
});
const schema = yup.object({
  name: yup.string().required(),
  description: yup.string().required(),
  imageUrl: yup.string().required(),
  price: yup.number().required().min(0),
});
...
</script>

然后,我们通过写入以下代码来添加component options对象:

<script>
... 
export default {
  name: "ShopItems",
  components: {
    Form,
    Field,
    ErrorMessage,
    TopBar,
  },
  data() {
    return {
      shopItems: [],
      showDialog: false,
      schema,
    };
  },
  beforeMount() {
    this.getShopItems();
  },
  ...
};
</script>

我们注册组分并创建data方法来返回我们使用的反应性属性。beforeMount钩子调用getShopItems方法从 API 中获取商店项目。

接下来,我们通过编写以下代码来添加getShopItems方法:

<script>
... 
export default {
  ...
  methods: {
    async getShopItems() {
      const query = gql`
        {
          getShopItems {
            shop_item_id
            name
            description
            image_url
            price
          }
        }
      `;
      const { getShopItems: data } = await 
        graphQLClient.request(query);
      this.shopItems = data;
    },
    ...
  },
};
</script>

我们只是发出一个getShopItems查询请求,以获取返回大括号中字段的数据。

接下来,我们通过编写以下代码,添加submitForm方法来发出变异请求,以添加一个店铺项目条目:

<script>
... 
export default {
  ...
  methods: {
    ...
    async submitForm({ name, description, imageUrl, price: 
      oldPrice }) {
      const mutation = gql`
        mutation addShopItem(
          $name: String
          $description: String
          $image_url: String
          $price: Float
        ) {
...
        description,
        image_url: imageUrl,
        price: +oldPrice,
      };
      await graphQLClient.request(mutation, variables);
      this.showDialog = false;
      await this.getShopItems();
    },
    ...
  },
};
</script>

我们通过对参数中的对象进行解构得到所有表单字段值,然后调用graphQLClient.request使用参数的解构属性设置的变量发出请求。根据我们在后端创建的模式,我们将price转换为一个数字,因为price应该是一个浮点。

请求完成后,我们将showDialog设置为false以关闭对话框,并再次调用getShopItems以获取店铺商品。

我们要添加的最后一个方法是deleteItem方法。在以下代码段中可以看到这方面的代码:

<script>
... 
export default {
  ...
  methods: {
    ...
    async deleteItem({ shop_item_id: shopItemId }) {
      const mutation = gql`
        mutation removeShopItem($shopItemId: Int) {
          removeShopItem(shopItemId: $shopItemId) {
            status
          }
        }
      `;
      const variables = {
        shopItemId,
      };
      await graphQLClient.request(mutation, variables);
      await this.getShopItems();
    },
    ...
  },
};
</script>

我们发出removeShopItem变异请求删除一个店铺项目条目。请求完成后,我们再次调用getShopItems获取最新数据。

在以下屏幕截图中可以看到管理员对商店项目页面的看法:

Figure 7.4 – Shop items page: admin view

图 7.4–商店项目页面:管理员视图

src/App.vue中,我们编写以下代码来添加router-view组件,以显示路由组件内容:

<template>
  <router-view></router-view>
</template>
<script>
export default {
  name: "App",
};
</script>

src/main.js中,我们编写以下代码将路由添加到我们的应用中:

import { createApp } from 'vue'
import App from './App.vue'
import router from '@/plugins/router'
const app = createApp(App)
app.use(router)
app.mount('#app')

最后,在package.json中,我们更改服务器脚本,从不同的端口为应用提供服务,这样它就不会与前端冲突。为此,我们编写以下代码:

{
  ...
  "scripts": {
    "serve": "vue-cli-service serve --port 8090",
    ...
  },
  ...
}

我们现在已经完成了管理前端,并将继续这个项目的最后一部分,这是一个前端的客户,以便他们可以订购项目。

创建客户前端

现在我们已经完成了管理前端,我们通过创建客户的前端来完成本章的项目。这与管理员前端类似,只是使用它不需要身份验证。

我们首先安装为管理前端安装的相同软件包。因此,我们转到frontend文件夹并运行以下命令来安装所有软件包:

npm i vee-validate@next vue-router@4 yup vuex@4 vuex-persistedstate@ ^4.0.0-beta.3 graphql graphql-request

我们需要带有[T0]插件的 Vuex 来存储购物车项目。其余的包与管理前端的包相同。

创建插件文件夹

我们在src文件夹中创建plugins文件夹,通过在文件夹中创建router.js文件并编写以下代码来添加路由:

import { createRouter, createWebHashHistory } from 'vue-router'
import Shop from '@/views/Shop'
import OrderForm from '@/views/OrderForm'
import Success from '@/views/Success'
const routes = [
  { path: '/', component: Shop },
  { path: '/order-form', component: OrderForm },
  { path: '/success', component: Success },
]
const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

接下来,我们通过创建src/plugins/vuex.js文件,然后编写以下代码来创建 Vuex 存储:

import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";
const store = createStore({
  state() {
    return {
      cartItems: []
    }
  },
  getters: {
    cartItemsAdded(state) {
      return state.cartItems
    }
  },
  mutations: {
    addCartItem(state, cartItem) {
      const cartItemIds = state.cartItems.map(c => 
        c.cartItemId).filter(id => typeof id === 'number')
      state.cartItems.push({
...
      state.cartItems = []
    }
  },
  plugins: [createPersistedState({
    key: 'cart'
  })],
});
export default store

我们呼叫createStore创建 Vuex 商店。在我们传递到createStore的对象中,我们有state方法返回初始化到数组的cartItems状态。getters属性有一个使用cartItemsAdded方法返回cartItems状态值的对象。

mutations属性对象中,我们有addCartItem方法调用state.cartItems.pushcartItems状态添加cartItem值。我们使用mapfilter方法获取现有购物车项目 ID。我们只想要数字的。新购物车项目的 ID 将是cartItemIds数组加1中的最高 ID。

removeCartItem方法允许我们调用splice按索引移除购物车项目,clearCartcartItems状态重置为空数组。

最后,我们将plugins属性设置为具有createPersistedState函数的对象,创建Vuex-Persistedstate插件,将cartItems状态存储到本地存储。key值是存储下cartItem值的键。我们可以稍后将其添加到应用商店。

创建订单表单页面

接下来,我们创建一个订单页面。这有一个表单,让客户输入他们的个人信息并编辑购物车。要创建它,我们创建一个src/views文件夹(如果还不存在),然后创建一个OrderForm.vue组件文件。我们首先编写以下模板代码:

<template>
  <h1>Order Form</h1>
  <div v-for="(cartItem, index) of cartItemsAdded" 
    :key="cartItem.cartItemId">
    <h2>{{ cartItem.name }}</h2>
    <p>Description: {{ cartItem.description }}</p>
    <p>Price: ${{ cartItem.price }}</p>
    <br />
...
      <Field name="phone" type="text" placeholder="Phone" />
      <ErrorMessage name="phone" />
    </div>
    <br />
    <div>
      <label for="address">Address</label>
      <br />
      <Field name="address" type="text" placeholder="Address" 
         />
      <ErrorMessage name="address" />
    </div>
    <br />
    <input type="submit" />
  </Form>
</template>

我们有类似于管理前端的表单。我们使用 VeeValidate 相同的FormFieldErrorMessage组件。

我们使用v-for循环遍历购物车项目,将它们呈现在屏幕上。它们通过Vuex-PersistedstatecartItemsAddedgetter 从本地存储中检索。

接下来,我们通过编写以下代码以相同的方式创建脚本:

<script>
import { GraphQLClient, gql } from "graphql-request";
import { mapMutations, mapGetters } from "vuex";
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";
...
export default {
  name: "OrderForm",
  data() {
    return {
      schema,
    };
  },
  components: {
    Form,
    Field,
    ErrorMessage,
  },
  computed: {
    ...mapGetters(["cartItemsAdded"]),
  },
  ...
};
</script>

我们创建 GraphQL 客户端和验证模式,并以与在管理前端的 shop item 页面中相同的方式注册组件。唯一的新功能是调用mapGetters方法将 Vuex getter 添加为组件的计算属性。我们只需传入一个字符串数组,其中包含要将计算属性映射到的 getter 的名称。接下来,我们通过编写以下代码来添加这些方法:

<script>
...
export default {  
  ...
  methods: {
    async submitOrder({ name, phone, address }) {
      const mutation = gql`
        mutation addOrder(
          $name: String
          $phone: String
          $address: String
          $ordered_items: [ShopItem]
...
            shop_item_id,
            name,
            description,
            image_url,
            price,,
          })
        ),
      };
      await graphQLClient.request(mutation, variables);
      this.clearCart();
      this.$router.push("/success");
    },
    ...mapMutations(["addCartItem", "removeCartItem", 
        "clearCart"]),
  },
};
</script>

我们有submitOrder方法从订单中获取输入的数据,并向服务器发出addOrder变异请求。在variables对象中,我们需要从每个ordered_items对象中删除cartItemId,以便它与我们在后端创建的ShopItem模式匹配。在发送到服务器的对象中,不能有架构中未包含的额外属性。

一旦请求成功,我们调用clearCart清除购物车,然后调用thus.$router.push进入成功页面。mapMutation方法将突变映射到我们成分中的方法。clearCart方法与clearCartVuex 储存突变相同。

以下屏幕截图显示订单的管理员视图:

Figure 7.5 – Order form: admin view

图 7.5–订单:管理视图

接下来,我们通过编写以下代码来创建一个src/views/Shop.vue文件:

<template>
  <h1>Shop</h1>
  <div>
    <router-link to="/order-form">Check Out</router-link>
  </div>
  <button type="button" @click="clearCart()">Clear Shopping 
     Cart</button>
  <p>{{ cartItemsAdded.length }} item(s) added to cart.</p>
  <div v-for="shopItem of shopItems" :key="shopItem.
     shop_item_id">
    <h2>{{ shopItem.name }}</h2>
    <p>Description: {{ shopItem.description }}</p>
    <p>Price: ${{ shopItem.price }}</p>
    <img :src="shopItem.image_url" :alt="shopItem.name" />
    <br />
    <button type="button" @click="addCartItem(shopItem)">Add
       to Cart</button>
  </div>
</template>

我们用v-for呈现商店项目,就像我们用呈现其他组件一样。我们还有一个router-link组件来呈现页面上的链接。

我们显示使用[T0]getter 添加的购物车项目的数量。点击Clear Shopping Cart时调用clearCartVuex 突变方法。接下来,我们通过编写以下代码为组件添加脚本:

<script>
import { GraphQLClient, gql } from "graphql-request";
import { mapMutations, mapGetters } from "vuex";
const APIURL = "http://localhost:3000/graphql";
const graphQLClient = new GraphQLClient(APIURL);
...
    async getShopItems() {
      const query = gql`
        {
          getShopItems {
            shop_item_id
            name
            description
            image_url
            price
          }
        }
      `;
      const { getShopItems: data } = await 
        graphQLClient.request(query);
      this.shopItems = data;
    },
    ...mapMutations(["addCartItem", "clearCart"]),
  },
};
</script>

我们用同样的方法创建 GraphQL 客户机。在组件中,我们在beforeMount钩子中调用getShopItems来获取购物车项目。我们还调用mapMutations将我们需要的 Vuex 突变映射到组件中的方法中。

最后,我们通过编写以下代码将img元素缩减为100px宽度:

<style scoped>
img {
  width: 100px;
}
</style>

接下来,我们通过创建一个src/views/Success.vue文件并编写以下代码来创建订单成功页面:

<template>
  <div>
    <h1>Order Successful</h1>
    <router-link to="/">Go Back to Shop</router-link>
  </div>
</template>
<script>
export default {
  name: "Success",
};
</script>

订单成功页面只有一些文本和一个返回商店主页的链接。

接下来,在src/App.vue中,我们编写以下代码来添加router-view组件来显示路由页面:

<template>
  <router-view></router-view>
</template>
<script>
export default {
  name: "App",
};
</script>

src/main.js中,我们添加以下代码,将路由和 Vuex 商店添加到我们的应用中:

import { createApp } from 'vue'
import App from './App.vue'
import router from '@/plugins/router'
import store from '@/plugins/vuex'
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')

最后,我们通过编写以下代码来更改 app 项目的服务端口:

{
  ...
  "scripts": {
    "serve": "vue-cli-service serve --port 8091",
    ...
  },
  ...
}

我们的项目现在完成了。

我们可以用npm run serve运行前端项目,用npm start运行后端项目。

通过使用购物车项目,我们了解了如何创建 GraphQLAPI,这是一种 JSON API,可以通过查询和突变处理 GraphQL 指令。

总结

我们可以使用 Express 和express-graphql库轻松创建 GraphQLAPI。为了方便地进行 GraphQL HTTP 请求,我们使用在浏览器中工作的graphql-requestJavaScript GraphQL 客户端。这使我们可以设置请求选项,例如标题、要进行的查询以及与查询一起使用的变量。

使用[T0]GraphQL 客户端代替常规 HTTP 客户端从我们的 Vue 应用向后端发出请求。与使用常规 HTTP 客户端相比,graphql-request库使我们能够更轻松地进行 GraphQL HTTP 请求。有了它,我们可以轻松地传递带有变量的 GraphQL 查询和突变。

GraphQLAPI 是使用映射到解析器函数的模式创建的。模式允许我们定义输入和输出数据的所有数据类型,这样我们就不必猜测必须发送哪些数据。如果我们发送任何无效数据,那么我们将得到一个错误,告诉我们请求到底出了什么问题。我们还必须指定要随 GraphQL 查询返回的数据字段,并且只返回我们指定的字段。这使我们能够返回需要使用的数据,从而使其更加高效。

此外,我们可以在向/graphql端点发出请求之前,通过通常的令牌检查向 GraphQLAPI 请求添加身份验证。

我们可以使用 GraphiQL 交互式沙箱轻松测试 GraphQL 请求,该沙箱允许我们发出所需的请求。为了测试经过身份验证的请求,我们可以使用ModHeader扩展来设置头,这样我们就可以成功地发出经过身份验证的请求。

在下一章中,我们将介绍如何使用 Laravel 和 Vue 3 创建实时聊天应用。