
掌握ChatGPT插件與自定義GPT
id: ID!
name: String!
email: String!
friends: [User!]!
}
該符號表示該字段為必填字段。!
在此示例中,“User” 對象類型有四個字段:“id”、“name”、“email”和“friends”。“id”字段的類型為 ID,它是 GraphQL 中的內置標量類型,表示唯一標識符。“name” 和 “email” 字段的類型為 String,“friends” 字段的類型為 “User” 對象列表。
下面是在庫應用程序中表示 “Book” 的 Object Type 的另一個示例:
type Book {
id: ID!
title: String!
author: Author!
genre: String!
published: Int!
}
在此示例中,“Book”對象類型有五個字段:“id”、“title”、“author”、“genre”和“published”。“id”字段的類型為 ID,“title”和“genre”字段的類型為 String,“published”字段的類型為 Int,“author”字段的類型為“Author”對象。
對象類型可用于定義從 GraphQL API 中的查詢或更改返回的數據的結構。例如,返回用戶列表的查詢可能如下所示:
query {
users {
id
name
email
friends {
id
name
}
}
}
在此查詢中,“users”字段返回“User”對象的列表,查詢指定要包含在響應中的字段。
在 GraphQL 中,查詢是來自服務器的特定數據的請求。查詢指定客戶端想要接收的數據的形狀,服務器以相同的形狀響應請求的數據。
GraphQL 中的查詢遵循與預期接收的數據形狀類似的結構。它由一組字段組成,這些字段對應于客戶端要檢索的數據的屬性。每個字段還可以具有修改返回數據的參數。
以下是 GraphQL 中的簡單查詢示例:
query {
user(id: "1") {
name
email
age
}
}
在此示例中,查詢請求有關 ID 為“1”的用戶的信息。查詢中指定的字段是 “name”、“email” 和 “age”,它們對應于 user 對象的屬性。
來自服務器的響應將與查詢的形狀相同,請求的數據將在相應的字段中返回:
{
"data": {
"user": {
"name": "John Doe",
"email": "johndoe@example.com",
"age": 25
}
}
}
此處,服務器在 “name”、“email” 和 “age” 字段中返回了有關用戶的請求數據。數據包含在 “data” 對象中,以將其與響應中可能包含的任何錯誤或其他元數據區分開來。
在 GraphQL 中,更改用于在服務器上修改或創建數據。與查詢類似,mutation 也用于指定發送到服務器和從服務器接收的數據的形狀。它們之間的主要區別在于,查詢僅用于讀取數據,而 mutation 則既可以讀取數據也可以寫入數據。
以下是 GraphQL 中的簡單更改示例:
mutation {
createUser(name: "Jane Doe", email: "janedoe@example.com", age: 30) {
id
name
email
age
}
}
在此示例中,更改是在服務器上創建一個名為 “Jane Doe” 、電子郵件 “janedoe@example.com” 且年齡為 30 的新用戶。更改中指定的字段是 “id”、“name”、“email” 和 “age”,它們對應于 user 對象的屬性。
來自服務器的響應將與 mutation 的形狀相同,新創建的用戶數據將在相應的字段中返回:
{
"data": {
"createUser": {
"id": "123",
"name": "Jane Doe",
"email": "janedoe@example.com",
"age": 30
}
}
}
在這里,服務器在 “id”、“name”、“email” 和 “age” 字段中返回了有關新創建用戶的數據。
更改還可用于更新或刪除服務器上的數據。下面是一個更新用戶名的 mutation 示例:
mutation {
updateUser(id: "123", name: "Jane Smith") {
id
name
email
age
}
}
在此示例中,更改將 ID 為“123”的用戶更新為名稱“Jane Smith”。更改中指定的字段與上一個示例中指定的字段相同。
來自服務器的響應將是更新的用戶數據:
{
"data": {
"updateUser": {
"id": "123",
"name": "Jane Smith",
"email": "janedoe@example.com",
"age": 30
}
}
}
GraphQL 中的突變設計為可組合的,這意味著可以將多個突變合并到一個請求中。這允許客戶端使用單個網絡往返執行復雜的操作。
在 GraphQL 中,解析程序是負責獲取 GraphQL 架構中定義的特定字段的數據的函數。解析程序是架構和數據源之間的橋梁。resolver 函數接收四個參數:parent、args、context 和 info。
parent
:當前字段的父對象。在嵌套查詢中,它引用父字段的值。args
:傳遞給當前字段的參數。它是一個對象,其中包含參數名稱及其值的鍵值對。context
:在特定請求的所有解析程序之間共享的對象。它包含有關請求的信息,例如當前經過身份驗證的用戶、數據庫連接等。info
:包含有關查詢的信息,包括字段名稱、別名和查詢文檔 AST。以下是類型字段的解析程序函數示例:User posts
const resolvers = {
User: {
posts: (parent, args, context, info) => {
return getPostsByUserId(parent.id);
},
},
};
在此示例中,是具有字段的 GraphQL 對象類型。查詢字段時,將使用父對象、傳遞的任何參數、上下文對象和查詢信息調用解析程序函數。在此示例中,resolver 函數調用函數來獲取當前用戶的帖子。User posts posts User getPostsByUserId
解析程序還可用于更改以創建、更新或刪除數據。以下是 mutation 的解析程序函數示例:createUser
const resolvers = {
Mutation: {
createUser: (parent, args, context, info) => {
const user = { name: args.name, email: args.email };
const createdUser = createUser(user);
return createdUser;
},
},
};
在此示例中,是具有 mutation 字段的 GraphQL 對象類型。調用更改時,將使用父對象、傳遞的參數、上下文對象和查詢信息調用解析程序函數。在此示例中,resolver 函數調用一個函數來創建具有給定名稱和電子郵件的新用戶,并返回新創建的用戶。Mutation createUser createUser
在 GraphQL 中,架構是一個藍圖,用于定義可在 API 中查詢的數據的結構。它定義了可對這些類型執行的可用類型、字段和操作。
GraphQL 架構是用 GraphQL 架構定義語言 (SDL) 編寫的,該語言使用簡單的語法來定義 API 中可用的類型和字段。服務器端代碼通常會定義架構,這個架構隨后被用來驗證和執行傳入的查詢。
以下是一個簡單的 GraphQL 架構定義示例:
type Book {
id: ID!
title: String!
author: String!
published: Int!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(title: String!, author: String!, published: Int!): Book!
updateBook(id: ID!, title: String, author: String, published: Int): Book
deleteBook(id: ID!): Book
}
在此架構中,我們有三種類型: 、 和 。該類型有四個字段: 、 、 和 。該類型有兩個字段: 和 ,分別可用于按 ID 檢索書籍列表或特定書籍。該類型有三個字段: 、 、 和 ,可用于創建、更新或刪除書籍。BookQueryMutation Book id title author published Query books book Mutation addBook updateBook delete Book
請注意,每個字段都有一個類型,可以是內置標量類型(如 或 ),也可以是自定義類型(如 .類型之后表示該字段不可為 null,這意味著它必須始終返回一個值(即,它不能為 null)。String Int Book!
總體而言,GraphQL 和 REST API 在處理數據獲取和修改的方式上有所不同。
REST API 依賴于多個終端節點和 HTTP 方法來獲取和修改數據,而 GraphQL 使用單個終端節點和查詢/更改來完成相同的操作。
與 REST API 相比,GraphQL 通過使用單個架構來定義整個 API 的數據模型,從而大大簡化了數據模型的理解和維護工作。相比之下,REST API 往往需要使用多種文檔格式來描述相同的數據模型,這不僅增加了復雜性,還可能導致不一致性和混淆。
Node.js?是一個開源、跨平臺的后端 JavaScript 運行時環境,允許開發人員在 Web 瀏覽器之外執行 JavaScript 代碼。它由 Ryan Dahl 于 2009 年創建,此后成為構建 Web 應用程序、API 和服務器的流行選擇。
Node.js 提供了一種事件驅動的非阻塞 I/O 模型,使其輕量級且高效,使其能夠以高性能處理大量數據。它還擁有一個龐大而活躍的社區,有許多庫和模塊可用于幫助開發人員更快、更輕松地構建他們的應用程序。
Apollo GraphQL 是用于構建 GraphQL API 的全棧平臺。它提供的工具和庫可簡化構建、管理和使用 GraphQL API 的過程。
Apollo GraphQL 平臺的核心是 Apollo Server,這是一種輕量級且靈活的服務器,可以輕松構建可擴展且高性能的 GraphQL API。Apollo Server 能夠支持多種數據源,諸如數據庫、REST API 以及其他服務,這使得它非常便于與現有的系統進行集成。
Apollo 還提供了許多客戶端庫,包括適用于 Web 和移動設備的 Apollo 客戶端,這簡化了使用 GraphQL API 的過程。Apollo Client 使查詢和更改數據變得容易,并提供緩存、樂觀 UI 和實時更新等高級功能。
除了 Apollo Server 和 Apollo Client,Apollo 還提供許多其他工具和服務,包括架構管理平臺、GraphQL 分析服務以及一組用于構建和調試 GraphQL API 的開發人員工具。
如果您是 GraphQL 或 Apollo 本身的新手,我真的建議您查看他們的文檔。在我看來,他們是最好的。
對于這個項目,我們將在代碼庫中遵循 layers 架構。層架構是將關注點和職責劃分為不同的文件夾和文件,并且只允許某些文件夾和文件之間直接通信。
你的項目應該有多少個層,每個層應該有什么名稱,以及它應該處理什么操作,這些都是討論的問題。那么,讓我們看看我認為對于我們的示例來說,什么是好方法。
我們的應用程序將有五個不同的層,它們將按以下方式排序:
應用程序層
要記住的重要一點是,在這些類型的架構中,各層之間有一個定義的通信流,必須遵循該流才能有意義。
這意味著請求首先必須經過第一層,然后是第二層,然后是第三層,依此類推。任何請求都不應該跳過層,因為這會擾亂架構的邏輯以及它給我們帶來的組織和模塊化的好處。
如果您想了解其他一些 API 架構選項,我推薦我前段時間寫的這篇軟件架構文章。
在跳轉到代碼之前,我們先提一下我們實際要構建的內容。我們將執行基本的 CRUD 操作(創建、讀取、更新和刪除)。
我們使用的是與我關于完全實現 REST API 的文章中使用的完全相同的示例。如果您也有興趣閱讀這些內容,這應該有助于比較 REST 和 GraphQL 之間的概念,并了解它們的異同。;)
現在讓我們開始這件事。創建一個新目錄,跳入該目錄,然后通過運行 .對于我們的 GraphQL 服務器,我們還需要兩個依賴項,因此 run 和 too。npm init -y
npm i @apollo/server
npm i graphql
在項目的根目錄中,創建一個文件并將以下代碼放入其中:app.js
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
import { typeDefs, resolvers } from './pets/index.js'
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers
})
// Passing an ApolloServer instance to the startStandaloneServer
function:
// 1. creates an Express app
// 2. installs your ApolloServer instance as middleware
// 3. prepares your app to handle incoming requests
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 }
})
console.log(?? Server ready at: ${url}
)
在這里,我們設置了我們的 Apollo 服務器,通過向它傳遞我們的 typeDefs 和解析器(我們稍后會解釋它們),然后在端口 4000 中啟動服務器。
接下來,在您的項目中創建以下文件夾結構:
我們的文件夾結構
在文件中放置以下代碼:index.js
import { addPet, editPet, deletePet } from './mutations/pets.mutations.js'
import { listPets, getPet } from './queries/pets.queries.js'
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against your data.
export const typeDefs = `#graphql
# OBJECT TYPES
# This "Pet" type defines the queryable fields for every pet in our data source.
type Pet {
id: ID!
name: String!
type: String!
age: Int!
breed: String!
}
# INPUT TYPES
# Define the input objects for addPet and editPet mutations
input PetToEdit {
id: ID!
name: String!
type: String!
age: Int!
breed: String!
}
input PetToAdd {
name: String!
type: String!
age: Int!
breed: String!
}
# The "Query" type is special: it lists all of the available queries that
# clients can execute, along with the return type for each. In this
# case, the "pets" query returns an array of zero or more pets.
# QUERY TYPES
type Query {
pets: [Pet],
pet(id: ID!): Pet
}
# MUTATION TYPES
type Mutation {
addPet(petToAdd: PetToAdd!): Pet,
editPet(petToEdit: PetToEdit!): Pet,
deletePet(id: ID!): [Pet],
}
`
export const resolvers = {
// Resolvers for Queries
Query: {
pets: () => listPets(),
pet: (_, { id }) => getPet(id)
},
// Resolvers for Mutations
Mutation: {
addPet: (_, { petToAdd }) => addPet(petToAdd),
editPet: (_, { petToEdit }) => editPet(petToEdit),
deletePet: (_, { id }) => deletePet(id)
}
}
這里我們有兩個主要的東西:typeDefs 和 resolvers。
typeDefs 定義可在 API 中查詢的數據的類型(在我們的例子中是對象),以及查詢/更改的輸入(在我們的例子中是 and)。pet PetToEdit PetToAdd
最后,它還定義了 API 的可用查詢和更改,聲明它們的名稱以及它們的輸入和返回值。在我們的例子中,我們有兩個查詢 ( 和 ) 和三個變化 ( 和 )。pets pet addPet editPet deletePet
Resolvers 包含我們的 queries 和 mutations 類型的實際實現。在這里,我們聲明了每個 query 和 mutation,并指出每個 query 和 mutation 應該做什么。在我們的例子中,我們將它們與我們從 queries/mutations 層導入的 queries/mutation 鏈接起來
In your file drop this:pets.queries.js
import { getItem, listItems } from '../models/pets.models.js'
export const getPet = id => {
try {
const resp = getItem(id)
return resp
} catch (err) {
return err
}
}
export const listPets = () => {
try {
const resp = listItems()
return resp
} catch (err) {
return err
}
}
如您所見,此文件非常簡單。它聲明文件中導入的函數,并將它們鏈接到 models 層中聲明的函數。index.js
我們的文件也是如此,但現在有了 mutations。pets.mutations.js
import { editItem, addItem, deleteItem } from '../models/pets.models.js'
export const addPet = petToAdd => {
try {
const resp = addItem(petToAdd)
return resp
} catch (err) {
return err
}
}
export const editPet = petToEdit => {
try {
const resp = editItem(petToEdit?.id, petToEdit)
return resp
} catch (err) {
return err
}
}
export const deletePet = id => {
try {
const resp = deleteItem(id)
return resp
} catch (err) {
return err
}
}
現在轉到 models 文件夾并創建一個包含此代碼的文件:pets.models.js
import db from '../../db/db.js'
export const getItem = id => {
try {
const pet = db?.pets?.filter(pet => pet?.id === parseInt(id))[0]
return pet
} catch (err) {
console.error('Error', err)
return err
}
}
export const listItems = () => {
try {
return db?.pets
} catch (err) {
console.error('Error', err)
return err
}
}
export const editItem = (id, data) => {
try {
const index = db.pets.findIndex(pet => pet.id === parseInt(id))
if (index === -1) throw new Error('Pet not found')
else {
data.id = parseInt(data.id)
db.pets[index] = data
return db.pets[index]
}
} catch (err) {
console.error('Error', err)
return err
}
}
export const addItem = data => {
try {
const newPet = { id: db.pets.length + 1, ...data }
db.pets.push(newPet)
return newPet
} catch (err) {
console.error('Error', err)
return err
}
}
export const deleteItem = id => {
try {
// delete item from db
const index = db.pets.findIndex(pet => pet.id === parseInt(id))
if (index === -1) throw new Error('Pet not found')
else {
db.pets.splice(index, 1)
return db.pets
}
} catch (err) {
console.error('Error', err)
return err
}
}
這些是負責與我們的數據層(數據庫)交互并將相應信息返回給我們的控制器的函數。
對于此示例,我們不會使用真實的數據庫。相反,為了示例的目的,我們將僅使用一個簡單的數組。盡管這樣做意味著每次服務器重置時數據都會被重置,但它已經足夠滿足我們的演示需求。
在項目的根目錄中,創建一個文件夾和一個文件,其中包含以下代碼:db db.js
const db = {
pets: [
{
id: 1,
name: 'Rex',
type: 'dog',
age: 3,
breed: 'labrador',
},
{
id: 2,
name: 'Fido',
type: 'dog',
age: 1,
breed: 'poodle',
},
{
id: 3,
name: 'Mittens',
type: 'cat',
age: 2,
breed: 'tabby',
},
]
}
export default db
如你所見,我們的對象包含一個屬性,其值是一個對象數組,每個對象都是一個寵物。對于每只寵物,我們都會存儲一個 ID、名稱、類型、年齡和品種。db
pets
現在轉到您的終端并運行 。您應該會看到以下消息,確認您的服務器處于活動狀態: 。nodemon app.js
?? Server ready at: [http://localhost:4000/](http://localhost:4000/)
現在我們的服務器已經啟動并運行,讓我們實現一個簡單的測試套裝來檢查我們的查詢和更改是否按預期運行。
如果您不熟悉自動化測試,我建議您閱讀我前段時間寫的這篇介紹性文章。
SuperTest 是一個 JavaScript 庫,用于測試發出 HTTP 請求的 HTTP 服務器或 Web 應用程序。它為測試 HTTP 提供了高級抽象,允許開發人員發送 HTTP 請求并對收到的響應進行斷言,從而更輕松地為 Web 應用程序編寫自動化測試。
SuperTest 可與任何 JavaScript 測試框架(如 Mocha 或 Jest)配合使用,也可與任何 HTTP 服務器或 Web 應用程序框架(如 Express)一起使用。
SuperTest 建立在流行的測試庫 Mocha 之上,并使用 Chai 斷言庫對收到的響應進行斷言。它提供了一個易于使用的 API 來發出 HTTP 請求,包括對身份驗證、標頭和請求正文的支持。
SuperTest 還允許開發人員測試整個請求/響應周期,包括中間件和錯誤處理,使其成為測試 Web 應用程序的強大工具。
總體而言,對于希望為其 Web 應用程序編寫自動化測試的開發人員來說,SuperTest 是一個有價值的工具。它有助于確保他們的應用程序能夠正常運行,同時也能夠避免他們對代碼庫所做的任何更改引入新的錯誤或問題。
首先,我們需要安裝一些依賴項。要保存終端命令,請轉到您的文件并將您的部分替換為下面的代碼。然后運行 .package.json devDependencies npm install
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"babel-jest": "^29.5.0",
"jest": "^29.5.0",
"jest-babel": "^1.0.1",
"nodemon": "^2.0.22",
"supertest": "^6.3.3"
}
在這里,我們將安裝 and 庫,這是我們運行測試所需的庫,以及我們的項目需要的一些東西,以便正確識別哪些文件是測試文件。super test jest babel
仍在 您的 中,添加以下腳本:package.json
"scripts": {
"test": "jest"
},
要以樣板結束,請在項目的根目錄中創建一個文件并將以下代碼放入其中:babel.config.cjs
//babel.config.cjs
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
現在讓我們編寫一些實際測試!在 pets 文件夾中,創建一個包含以下代碼的文件:pets.test.js
import request from 'supertest'
const graphQLEndpoint = 'http://localhost:4000/'
describe('Get all pets', () => {
const postData = {
query: `query Pets {
pets {
id
name
type
age
breed
}
}`
}
test('returns all pets', async () => {
request(graphQLEndpoint)
.post('?')
.send(postData)
.expect(200)
.end((error, response) => {
if (error) console.error(error)
const res = JSON.parse(response.text)
expect(res.data.pets).toEqual([
{
id: '1',
name: 'Rex',
type: 'dog',
age: 3,
breed: 'labrador'
},
{
id: '2',
name: 'Fido',
type: 'dog',
age: 1,
breed: 'poodle'
},
{
id: '3',
name: 'Mittens',
type: 'cat',
age: 2,
breed: 'tabby'
}
])
})
})
})
describe('Get pet detail', () => {
const postData = {
query: `query Pet {
pet(id: 1) {
id
name
type
age
breed
}
}`
}
test('Return pet detail information', async () => {
request(graphQLEndpoint)
.post('?')
.send(postData)
.expect(200)
.end((error, response) => {
if (error) console.error(error)
const res = JSON.parse(response.text)
expect(res.data.pet).toEqual({
id: '1',
name: 'Rex',
type: 'dog',
age: 3,
breed: 'labrador'
})
})
})
})
describe('Edit pet', () => {
const postData = {
query: `mutation EditPet($petToEdit: PetToEdit!) {
editPet(petToEdit: $petToEdit) {
id
name
type
age
breed
}
}`,
variables: {
petToEdit: {
id: 1,
name: 'Rexo',
type: 'dogo',
age: 4,
breed: 'doberman'
}
}
}
test('Updates pet and returns it', async () => {
request(graphQLEndpoint)
.post('?')
.send(postData)
.expect(200)
.end((error, response) => {
if (error) console.error(error)
const res = JSON.parse(response.text)
expect(res.data.editPet).toEqual({
id: '1',
name: 'Rexo',
type: 'dogo',
age: 4,
breed: 'doberman'
})
})
})
})
describe('Add pet', () => {
const postData = {
query: `mutation AddPet($petToAdd: PetToAdd!) {
addPet(petToAdd: $petToAdd) {
id
name
type
age
breed
}
}`,
variables: {
petToAdd: {
name: 'Salame',
type: 'cat',
age: 6,
breed: 'pinky'
}
}
}
test('Adds new pet and returns the added item', async () => {
request(graphQLEndpoint)
.post('?')
.send(postData)
.expect(200)
.end((error, response) => {
if (error) console.error(error)
const res = JSON.parse(response.text)
expect(res.data.addPet).toEqual({
id: '4',
name: 'Salame',
type: 'cat',
age: 6,
breed: 'pinky'
})
})
})
})
describe('Delete pet', () => {
const postData = {
query: `mutation DeletePet {
deletePet(id: 2) {
id,
name,
type,
age,
breed
}
}`
}
test('Deletes given pet and returns updated list', async () => {
request(graphQLEndpoint)
.post('?')
.send(postData)
.expect(200)
.end((error, response) => {
if (error) console.error(error)
const res = JSON.parse(response.text)
expect(res.data.deletePet).toEqual([
{
id: '1',
name: 'Rexo',
type: 'dogo',
age: 4,
breed: 'doberman'
},
{
id: '3',
name: 'Mittens',
type: 'cat',
age: 2,
breed: 'tabby'
},
{
id: '4',
name: 'Salame',
type: 'cat',
age: 6,
breed: 'pinky'
}
])
})
})
})
這是我們的 GraphQL API 的測試套件。它使用該庫向 API 端點 () 發出 HTTP 請求,并驗證 API 是否正確響應各種查詢和更改。supertest http://localhost:4000/
該代碼有五個不同的測試用例:
Get all pets
:此測試查詢所有寵物的 API,并驗證響應是否與預期的寵物列表匹配。Get pet detail
:此測試查詢 API 以獲取特定寵物的詳細信息,并驗證響應是否與該寵物的預期詳細信息匹配。Edit pet
:此測試執行更改以編輯特定寵物的詳細信息,并驗證響應是否與該寵物的預期編輯詳細信息匹配。Add pet
:此測試執行更改以添加新寵物,并驗證響應是否與新添加的寵物的預期詳細信息匹配。Delete pet
:此測試執行更改以刪除特定寵物,并驗證響應是否與刪除后的預期寵物列表匹配。每個測試用例都包含一個對象,該對象包含要發送到 API 終端節點的 GraphQL 查詢或更改以及任何必要的變量。postData
實際的 HTTP 請求是使用庫中的函數發出的,該函數將 POST 請求發送到 API 終端節點,并在請求正文中包含對象。然后,響應被解析為 JSON,測試用例使用 Jest 測試框架中的函數驗證響應是否與預期結果匹配。request supertest postData expect
現在轉到您的終端,運行 ,您應該會看到所有測試都通過了:npm test
> jest
PASS pets/pets.test.js
Get all pets
? returns all pets (15 ms)
Get pet detail
? Return pet detail information (2 ms)
Edit pet
? Updates pet and returns it (1 ms)
Add pet
? Adds new pet and returns the added item (1 ms)
Delete pet
? Deletes given pet and returns updated list (1 ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 0.607 s, estimated 1 s
Ran all test suites.
現在我們知道我們的服務器正在運行并按預期運行。讓我們看一些更實際的示例,了解前端應用程序如何使用我們的 API。
在這個例子中,我們將使用 React 應用程序和 Apollo 客戶端來發送和處理我們的請求。
React 是一個流行的 JavaScript 庫,用于構建用戶界面。它允許開發人員創建可重用的 UI 組件,并有效地更新和呈現它們以響應應用程序狀態的變化。
關于 Apollo 客戶端,我們已經介紹了它。
旁注 – 我們在這里選擇使用 Apollo 客戶端,是因為它是一個非常流行的工具,而且讓前端和后端使用相同的庫集是很有意義的。如果你對從前端 React 應用程序使用 GraphQL API 的其他可能方式感興趣,Reed Barger 有一篇關于這個主題的非常酷的文章。
讓我們通過運行并按照終端提示來創建我們的 React 應用程序。完成后,運行 (我們將使用它來在我們的應用程序中設置基本路由)。yarn create vite
yarn add react-router-dom
將此代碼放入您的文件中:App.jsx
import { Suspense, lazy, useState } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import './App.css'
const PetList = lazy(() => import('./pages/PetList'))
const PetDetail = lazy(() => import('./pages/PetDetail'))
const EditPet = lazy(() => import('./pages/EditPet'))
const AddPet = lazy(() => import('./pages/AddPet'))
function App() {
const [petToEdit, setPetToEdit] = useState(null)
return (
<div className='App'>
<Router>
<h1>Pet shelter</h1>
<Routes>
<Route
path='/'
element={
<Suspense fallback={<></>}>
<PetList />
</Suspense>
}
/>
<Route
path='/:petId'
element={
<Suspense fallback={<></>}>
<PetDetail setPetToEdit={setPetToEdit} />
</Suspense>
}
/>
<Route
path='/:petId/edit'
element={
<Suspense fallback={<></>}>
<EditPet petToEdit={petToEdit} />
</Suspense>
}
/>
<Route
path='/add'
element={
<Suspense fallback={<></>}>
<AddPet />
</Suspense>
}
/>
</Routes>
</Router>
</div>
)
}
export default App
在這里,我們只定義我們的路由。我們的應用程序中將有 4 個主要路由,每個路由對應不同的視圖:
此外,我們還有一個用于添加新寵物的按鈕和一個 state,該 state 將存儲我們要編輯的寵物的信息。
接下來,創建一個包含這些文件的目錄:pages
文件夾結構
在跳轉到我們的頁面之前,我們必須設置 Apollo 客戶端庫。運行 并安裝必要的依賴項。yarn add @apollo/client
yarn add graphql
轉到文件并將以下代碼放入其中:main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'
const client = new ApolloClient({
uri: 'http://localhost:4000/',
cache: new InMemoryCache(),
})
ReactDOM.createRoot(document.getElementById('root')).render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
)
在這里,我們初始化 ,向其構造函數傳遞一個帶有 and 字段的配置對象:ApolloClient
uri
cache
uri
指定 GraphQL 服務器的 URL。cache
是 的一個實例,Apollo Client 在獲取查詢結果后使用它來緩存查詢結果。InMemoryCache
然后我們用 ApolloProvider 包裝我們的組件。這允許我們組件樹中的任何組件使用 Apollo 客戶端提供的鉤子,就像 React Context 一樣。;)App
在項目的根目錄中,創建以下文件夾結構:
文件夾結構
在這兩個文件中,我們將聲明將用于查詢和更改的請求正文。我喜歡將其分成不同的文件,因為它可以清楚地看到我們在應用程序中擁有的不同類型請求,并且它還使我們組件的代碼更簡潔。
在文件中拖放以下內容:queries.js
import { gql } from '@apollo/client'
export const GET_PETS = gql`
query Pets {
pets {
id
name
type
breed
}
}
`
export const GET_PET = gql`
query Pet($petId: ID!) {
pet(id: $petId) {
id
name
type
age
breed
}
}
`
在文件中拖放以下內容:mutations.js
import { gql } from '@apollo/client'
export const DELETE_PET = gql`
mutation DeletePet($deletePetId: ID!) {
deletePet(id: $deletePetId) {
id
}
}
`
export const ADD_PET = gql`
mutation AddPet($petToAdd: PetToAdd!) {
addPet(petToAdd: $petToAdd) {
id
name
type
age
breed
}
}
`
export const EDIT_PET = gql`
mutation EditPet($petToEdit: PetToEdit!) {
editPet(petToEdit: $petToEdit) {
id
name
type
age
breed
}
}
`
如您所見,查詢和更改的語法非常相似。請求正文以 GraphQL 查詢語言編寫,用于定義可從 GraphQL API 請求的數據的結構和數據類型。
export const GET_PETS = gql`
query Pets {
pets {
id
name
type
breed
}
}
`
此查詢已命名,它從字段請求數據。字段 、 、 和 是從 API 返回的每個對象中請求的。Pets pets id name type breed Pet
在 GraphQL 中,查詢始終以關鍵字開頭,后跟查詢名稱(如果提供)。請求的字段括在大括號中,可以嵌套以請求相關字段中的數據。query
export const ADD_PET = gql`
mutation AddPet($petToAdd: PetToAdd!) {
addPet(petToAdd: $petToAdd) {
id
name
type
age
breed
}
}
`
此更改已命名,并發送一個新對象,以通過更改添加到 API。該變量定義為 類型的必需輸入 。執行 mutation 時,input 變量將作為參數傳遞給 mutation。然后,更改返回新創建的對象的 、 和 字段。AddPet Pet addPet $petToAdd PetToAdd addPet id name type age breed Pet
在 GraphQL 中,更改始終以關鍵字開頭,后跟更改的名稱(如果提供)。更改響應中請求的字段也用大括號括起來。mutation
請注意,GraphQL 中的查詢和更改都可以接受變量作為輸入,這些變量使用特殊語法 () 在查詢或更改正文中定義。這些變量可以在執行查詢或更改時傳入,從而允許更多動態和可重用的查詢和更改。$variableName: variableType!
讓我們從負責渲染整個 pets 列表的文件開始:
import { Link } from 'react-router-dom'
import { useQuery } from '@apollo/client'
import { GET_PETS } from '../api/queries'
function PetList() {
const { loading, error, data } = useQuery(GET_PETS)
return (
<>
<h2>Pet List</h2>
<Link to='/add'>
<button>Add new pet</button>
</Link>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data?.pets?.map(pet => {
return (
<div key={pet?.id}>
<p>
{pet?.name} - {pet?.type} - {pet?.breed}
</p>
<Link to={/${pet?.id}
}>
<button>Pet detail</button>
</Link>
</div>
)
})}
</>
)
}
export default PetList
此代碼定義了一個名為 React 函數組件,該組件使用庫提供的鉤子從 GraphQL API 獲取寵物列表。用于獲取寵物的查詢在名為 的單獨文件中定義,該文件導出名為 的 GraphQL 查詢。PetList useQuery @apollo/client queries.js GET_PETS
鉤子返回一個具有三個屬性的對象:、 和 。這些屬性從對象中解構出來,用于根據 API 請求的狀態有條件地呈現不同的 UI 元素。useQuery loading error data
如果為 true,則屏幕上會顯示一條加載消息。如果已定義,則會顯示一條錯誤消息,其中包含 API 返回的特定錯誤消息。如果已定義并包含一個數組,則每個數組都顯示在 div 中,其中包含其 、 和 。每個 pet div 還包含一個鏈接,用于查看有關寵物的更多詳細信息。loading error data pets pet name type breed
鉤子的工作原理是執行查詢并將結果作為具有 、 和 屬性的對象返回。當組件首次呈現時,在執行查詢時為 true。如果查詢成功,則為 false 并填充結果。如果查詢遇到錯誤,則填充特定的錯誤消息。useQuery GET_PETS loading error data loading loading data error
如您所見,使用 Apollo 客戶端管理請求真的很好而且很簡單。它提供的鉤子為我們節省了相當多的代碼,通常用于執行請求、存儲響應和處理錯誤。
請記住,要調用我們的服務器,我們必須通過在服務器項目終端中運行來啟動并運行它。nodemon app.js
為了表明這里沒有奇怪的魔術,如果我們轉到瀏覽器,打開開發工具并轉到網絡選項卡,我們可以看到我們的應用程序正在向我們的服務器端點發出 POST 請求。payload 是我們的 String 形式的請求正文。
POST 請求
請求正文
這意味著,如果我們愿意,我們也可以通過使用 fetch 來使用我們的 GraphQL API,如下所示:
import { Link } from 'react-router-dom'
import { useEffect, useState } from 'react'
function PetList() {
const [pets, setPets] = useState([])
const getPets = () => {
fetch('http://localhost:4000/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `
query Pets {
pets {
id
name
type
breed
}
}
`
})
})
.then(response => response.json())
.then(data => setPets(data?.data?.pets))
.catch(error => console.error(error))
}
useEffect(() => {
getPets()
}, [])
return (
<>
<h2>Pet List</h2>
<Link to='/add'>
<button>Add new pet</button>
</Link>
{pets?.map(pet => {
return (
<div key={pet?.id}>
<p>
{pet?.name} - {pet?.type} - {pet?.breed}
</p>
<Link to={/${pet?.id}
}>
<button>Pet detail</button>
</Link>
</div>
)
})}
</>
)
}
export default PetList
如果您再次檢查您的 network 選項卡,您應該仍然會看到相同的 POST 請求和 some 請求正文。
當然,這種方法不是很實用,因為它需要更多的代碼行來執行相同的操作。但重要的是要明白,像 Apollo 這樣的庫主要是為我們提供了一個聲明式的 API,以此來簡化和優化我們的代碼使用。然而,在這一切便利的背后,實際上我們仍然是在處理常規的 HTTP 請求。
現在讓我們轉到文件:PetDetail.jsx
import { useEffect } from 'react'
import { useParams, Link } from 'react-router-dom'
import { useQuery, useMutation } from '@apollo/client'
import { GET_PET } from '../api/queries'
import { DELETE_PET } from '../api/mutations'
function PetDetail({ setPetToEdit }) {
const { petId } = useParams()
const { loading, error, data } = useQuery(GET_PET, {
variables: { petId }
})
useEffect(() => {
if (data && data?.pet) setPetToEdit(data?.pet)
}, [data])
const [deletePet, { loading: deleteLoading, error: deleteError, data: deleteData }] = useMutation(DELETE_PET, {
variables: { deletePetId: petId }
})
useEffect(() => {
if (deleteData && deleteData?.deletePet) window.location.href = '/'
}, [deleteData])
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', aligniItems: 'center' }}>
<h2>Pet Detail</h2>
<Link to='/'>
<button>Back to list</button>
</Link>
{(loading || deleteLoading) && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{deleteError && <p>deleteError: {deleteError.message}</p>}
{data?.pet && (
<>
<p>Pet name: {data?.pet?.name}</p>
<p>Pet type: {data?.pet?.type}</p>
<p>Pet age: {data?.pet?.age}</p>
<p>Pet breed: {data?.pet?.breed}</p>
<div style={{ display: 'flex', justifyContent: 'center', aligniItems: 'center' }}>
<Link to={/${data?.pet?.id}/edit
}>
<button style={{ marginRight: 10 }}>Edit pet</button>
</Link>
<button style={{ marginLeft: 10 }} onClick={() => deletePet()}>
Delete pet
</button>
</div>
</>
)}
</div>
)
}
export default PetDetail
此組件通過執行查詢來加載 pet 的詳細信息,其方式與上一個組件非常相似。
此外,它還執行刪除 pet register 所需的 mutation。你可以看到,為此我們使用了 hook。它與 非常相似,但除了值之外,它還提供了一個函數,用于在給定事件后執行我們的查詢。useMutation useQuery loading, error and data
你可以看到,對于這個 mutation 鉤子,我們傳遞了一個對象作為第二個參數,其中包含這個 mutation 需要的變量。在本例中,它是我們要刪除的寵物登記冊的 ID。
const [deletePet, { loading: deleteLoading, error: deleteError, data: deleteData }] = useMutation(DELETE_PET, {
variables: { deletePetId: petId }
})
請記住,當我們聲明我們的 mutation 時,我們已經聲明了這個 mutation 將使用的變量。mutations.js
export const DELETE_PET = gql`
mutation DeletePet($deletePetId: ID!) {
deletePet(id: $deletePetId) {
id
}
}
`
這是負責將新寵物添加到我們的注冊中的文件:
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useMutation } from '@apollo/client'
import { ADD_PET } from '../api/mutations'
function AddPet() {
const [petName, setPetName] = useState()
const [petType, setPetType] = useState()
const [petAge, setPetAge] = useState()
const [petBreed, setPetBreed] = useState()
const [addPet, { loading, error, data }] = useMutation(ADD_PET, {
variables: {
petToAdd: {
name: petName,
type: petType,
age: parseInt(petAge),
breed: petBreed
}
}
})
useEffect(() => {
if (data && data?.addPet) window.location.href = /${data?.addPet?.id}
}, [data])
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', aligniItems: 'center' }}>
<h2>Add Pet</h2>
<Link to='/'>
<button>Back to list</button>
</Link>
{loading || error ? (
<>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
</>
) : (
<>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet name</label>
<input type='text' value={petName} onChange={e => setPetName(e.target.value)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet type</label>
<input type='text' value={petType} onChange={e => setPetType(e.target.value)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet age</label>
<input type='text' value={petAge} onChange={e => setPetAge(e.target.value)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet breed</label>
<input type='text' value={petBreed} onChange={e => setPetBreed(e.target.value)} />
</div>
<button
style={{ marginTop: 30 }}
disabled={!petName || !petType || !petAge || !petBreed}
onClick={() => addPet()}
>
Add pet
</button>
</>
)}
</div>
)
}
export default AddPet
這里我們有一個組件,它加載一個表單來添加新的寵物,并在發送數據時執行更改。它接受新的 pet info 作為參數,就像 mutation 接受 pet id 一樣。deletePet
最后,負責編輯寵物登記冊的文件:
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useMutation } from '@apollo/client'
import { EDIT_PET } from '../api/mutations'
function EditPet({ petToEdit }) {
const [petName, setPetName] = useState(petToEdit?.name)
const [petType, setPetType] = useState(petToEdit?.type)
const [petAge, setPetAge] = useState(petToEdit?.age)
const [petBreed, setPetBreed] = useState(petToEdit?.breed)
const [editPet, { loading, error, data }] = useMutation(EDIT_PET, {
variables: {
petToEdit: {
id: parseInt(petToEdit.id),
name: petName,
type: petType,
age: parseInt(petAge),
breed: petBreed
}
}
})
useEffect(() => {
if (data && data?.editPet?.id) window.location.href = /${data?.editPet?.id}
}, [data])
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', aligniItems: 'center' }}>
<h2>Edit Pet</h2>
<Link to='/'>
<button>Back to list</button>
</Link>
{loading || error ? (
<>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
</>
) : (
<>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet name</label>
<input type='text' value={petName} onChange={e => setPetName(e.target.value)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet type</label>
<input type='text' value={petType} onChange={e => setPetType(e.target.value)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet age</label>
<input type='text' value={petAge} onChange={e => setPetAge(e.target.value)} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', margin: 20 }}>
<label>Pet breed</label>
<input type='text' value={petBreed} onChange={e => setPetBreed(e.target.value)} />
</div>
<button
style={{ marginTop: 30 }}
disabled={!petName || !petType || !petAge || !petBreed}
onClick={() => editPet()}
>
Save changes
</button>
</>
)}
</div>
)
}
export default EditPet
最后,我們有一個組件,用于通過表單編輯寵物登記冊。它在發送數據時執行 mutation,并作為參數接受新的 pet 信息。
就是這樣!我們在前端應用程序中使用了所有的 API 查詢和更改。
Apollo 最酷的功能之一是它帶有一個內置沙箱,您可以使用它來測試和記錄您的 API。
Apollo Sandbox 是一個基于 Web 的 GraphQL IDE,它提供了一個用于測試 GraphQL 查詢、更改和訂閱的沙盒環境。它是 Apollo 提供的免費在線工具,允許您與 GraphQL API 交互并探索其架構、數據和功能。
以下是 Apollo Sandbox 的一些主要功能:
要使用我們的沙盒,只需在 http://localhost:4000/
打開瀏覽器即可。您應該看到如下內容:
阿波羅沙箱
在這里,您可以查看 API 數據架構和可用的更改和查詢,還可以執行它們并查看 API 的響應方式。例如,通過執行查詢,我們可以在右側面板上看到該響應。pets
執行查詢
如果您跳到 schema 部分,您可以在我們的 API 中看到可用查詢、更改對象和輸入類型的完整詳細信息。
schema 部分
Apollo sandbox 是一個很棒的工具,既可以用作我們 API 的自我文檔,也可以用作出色的開發和測試工具。
好吧,大家一如既往,我希望你喜歡這篇文章并學到一些新東西。
如果你愿意,你也可以在 LinkedIn 或 Twitter 上關注我。下期見!
原文鏈接:https://www.freecodecamp.org/news/building-consuming-and-documenting-a-graphql-api/