七、使用 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
全局包。
为了设置本章的项目,我们执行以下步骤:
- 首先,我们创建一个文件夹来存放所有项目,并将其命名为
shopping-cart
。 - 然后在主文件夹中创建
admin-frontend
、frontend
和backend
文件夹。 - 接下来,我们进入
admin-frontend
文件夹并运行npx vue create
将 Vue 项目的脚手架代码添加到admin-frontend
文件夹中。 - 如果要求我们在当前文件夹中创建项目,我们选择
Y
,然后当要求我们选择项目的 Vue 版本时,我们选择Vue 3
。同样,我们对[T2]文件夹以相同的方式运行 Vue CLI。 -
To create the Express project, we run the Express application generator app. To run it, we go into the
backend
folder and then runnpx express-generator
.此命令将把项目所需的所有文件添加到
backend
文件夹中。如果出现错误,请尝试以管理员身份运行express-generator
包。
现在我们已经完成了项目的设置,我们可以开始编写代码了。接下来,我们将从创建 GraphQL 后端开始。
使用 Express 创建 GraphQL API
为了启动购物车系统项目,我们首先使用 Express 创建一个 GraphQL API。我们从后端开始,因为两个前端都需要它。首先,我们必须添加一些库,这些库是操作 SQLite 数据库和向我们的应用添加身份验证所必需的。此外,我们需要库在我们的应用中启用跨来源资源共享(CORS。
CORS 是一种让我们从浏览器向承载前端的不同域中的端点发出请求的方法。
为了让我们的 Express 应用接受 GraphQL 请求,我们使用graphql
和express-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
属性和username
和password
属性,我们使用它们来检查凭证。我们检查用户名是否为'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.serialize
在serialize
回调中一系列地运行所有语句,所以可以顺序运行结构化查询语言(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_id
和shop_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.serialize
和db.prepare
的方式与之前相同。唯一的区别是我们发布了DELETE
语句,删除了order_shop_items
和orders
表中给定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.serialize
和db.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
关键字来定义响应的数据类型,我们有Response
和Token
类型来用作响应。express-graphql
库将根据数据类型中指定的内容检查响应的结构,因此返回Response
类型数据的任何查询或变异都应该具有status string
属性。这是可选的,因为字符串后面没有感叹号。
input
关键字让我们定义一个input
类型。input
类型用于指定请求有效负载的数据结构。它们的定义方式与具有属性列表的output
类型相同,其数据类型位于冒号之后。
我们可以将一种数据类型嵌套在另一种数据类型中,就像我们在OrderOutput
类型中使用ordered_items
属性所做的那样。我们指定它保存一个具有ShopItemOutput
数据类型的对象数组。同样,我们在Order
数据类型中为ordered_items
属性指定了类似的数据类型。方括号表示数据类型是数组。
Query
和Mutation
是特殊的数据类型,允许我们在冒号之前添加解析器名称,在冒号之后添加输出的数据类型。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
响应。如果query
或mutation
属性不需要身份验证,那么我们只需调用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
属性有一个包含所有解析程序的对象。解析程序名称应与Query
和Mutation
类型中指定的名称匹配。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
扩展,可以在屏幕右上角添加进行身份验证请求所需的标题:
图 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 浏览器进行 SQLite(DB4S程序,我们可以从下载该程序 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-request
GraphQL 包和 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
显示带有表单字段的错误消息。如果Field
和ErrorMessage
的name
属性值匹配,则会自动显示给定名称字段的任何表单验证。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
验证模式,该模式包含name
和password
字段。这两个字段都是字符串,它们都是必需的,如方法调用所示。属性名称必须与Field
和ErrorMessage
组件的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 软件包导入的Form
、Field
和ErrorMessage
组件。我们有data
方法,它返回一个带有模式的对象,以便我们可以在模板中使用它。最后,我们使用submitForm
方法,从Field
组件中获取username
和password
值,并发出登录变异 GraphQL 请求。
我们将$username
和$password
值传递到括号中,以将它们传递到我们的突变中。这些值将从variablesvariables
对象中获得,我们将其传递到graphQLClient.request
方法中。如果请求成功,我们将从请求中取回令牌。一旦我们拿到代币,我们就把它放在localStorage.setItem
中放入本地存储。
gql
标记是一个函数,允许我们将字符串转换为可以发送到服务器的查询 JSON 对象。
如果登录请求失败,我们将显示警报。下面的屏幕截图显示了登录屏幕:
图 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
来发出查询请求,并将返回的数据分配给orders
reactive 属性。
deleteOrder
方法获取order
对象并向服务器发出removeOrder
变异请求。orderId
在变量中,因此正确的顺序将被删除。删除最新订单后,我们再次致电getOrders
获取。
以下屏幕截图显示管理员看到的订单页面:
图 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
获取最新数据。
在以下屏幕截图中可以看到管理员对商店项目页面的看法:
图 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.push
向cartItems
状态添加cartItem
值。我们使用map
和filter
方法获取现有购物车项目 ID。我们只想要数字的。新购物车项目的 ID 将是cartItemIds
数组加1
中的最高 ID。
removeCartItem
方法允许我们调用splice
按索引移除购物车项目,clearCart
将cartItems
状态重置为空数组。
最后,我们将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 相同的Form
、Field
和ErrorMessage
组件。
我们使用v-for
循环遍历购物车项目,将它们呈现在屏幕上。它们通过Vuex-Persistedstate
和cartItemsAdded
getter 从本地存储中检索。
接下来,我们通过编写以下代码以相同的方式创建脚本:
<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
方法与clearCart
Vuex 储存突变相同。
以下屏幕截图显示订单的管理员视图:
图 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时调用clearCart
Vuex 突变方法。接下来,我们通过编写以下代码为组件添加脚本:
<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-request
JavaScript GraphQL 客户端。这使我们可以设置请求选项,例如标题、要进行的查询以及与查询一起使用的变量。
使用[T0]GraphQL 客户端代替常规 HTTP 客户端从我们的 Vue 应用向后端发出请求。与使用常规 HTTP 客户端相比,graphql-request
库使我们能够更轻松地进行 GraphQL HTTP 请求。有了它,我们可以轻松地传递带有变量的 GraphQL 查询和突变。
GraphQLAPI 是使用映射到解析器函数的模式创建的。模式允许我们定义输入和输出数据的所有数据类型,这样我们就不必猜测必须发送哪些数据。如果我们发送任何无效数据,那么我们将得到一个错误,告诉我们请求到底出了什么问题。我们还必须指定要随 GraphQL 查询返回的数据字段,并且只返回我们指定的字段。这使我们能够返回需要使用的数据,从而使其更加高效。
此外,我们可以在向/graphql
端点发出请求之前,通过通常的令牌检查向 GraphQLAPI 请求添加身份验证。
我们可以使用 GraphiQL 交互式沙箱轻松测试 GraphQL 请求,该沙箱允许我们发出所需的请求。为了测试经过身份验证的请求,我们可以使用ModHeader
扩展来设置头,这样我们就可以成功地发出经过身份验证的请求。
在下一章中,我们将介绍如何使用 Laravel 和 Vue 3 创建实时聊天应用。
版权属于:月萌API www.moonapi.com,转载请注明出处