
云原生 API 網關 APISIX 入門教程
本文是課程的第二部分,您將使用Next.js、GraphQL、TypeScript、Prisma和PostgreSQL來構建一個全棧應用程序。在本文中,您將創建 GraphQL API 并在前端與其交互。
在本課程中,您將學習如何構建“awesome-links”,這是一個全棧應用程序,用戶可以在其中瀏覽精選鏈接列表并為他們最喜歡的鏈接添加書簽。
在上一部分中,您已經使用Prisma設置好了數據庫層。當您完成本部分的學習后,將會對GraphQL有所了解:知道它是什么,以及如何在Next.js應用程序中利用它來構建API。
要學習本教程,您需要安裝 Node.js 和GraphQL 擴展。您還需要有一個正在運行的 PostgreSQL 實例。
注意:您可以選擇在本地安裝PostgreSQL,或者在Heroku上設置一個托管的數據庫實例。但請注意,為了執行課程結束時的部署步驟,您將需要一個遠程數據庫。
您可以在 GitHub 上找到該課程的完整源代碼。
注:每篇文章都有相應的分支。這樣,您就可以跟著進行。通過查看第 2 部分分支,您將獲得與本文相同的起點。
首先,導航到您選擇的目錄并運行以下命令來克隆存儲庫。
git clone -b part-2 https://github.com/m-abdelwahab/awesome-links.git
現在,您可以進入已克隆的目錄,安裝所需的依賴項,并啟動開發服務器。
cd awesome-linksnpm installnpm run dev
該應用程序將運行http://localhost:3000/
,您將看到四個項目。數據是硬編碼的,來源于/data/links.ts
文件。
設置好PostgreSQL數據庫后,請將env.example
文件重命名為.env
,并配置好數據庫的連接字符串。接著,運行以下命令來在數據庫中創建遷移和所需的表:
npx prisma migrate dev --name init
如果prisma migrate dev
沒有觸發種子步驟,請運行以下命令來初始化數據:
npx prisma db seed
此命令將運行seed.ts
位于/prisma
目錄中的腳本。此腳本使用 Prisma 客戶端向您的數據庫添加四個鏈接和一個用戶。
您將看到以下文件夾結構:
awesome-links/
┣ components/
┃ ┣ Layout/
┃ ┗ AwesomeLink.tsx
┣ data/
┃ ┗ links.ts
┣ pages/
┃ ┣ _app.tsx
┃ ┣ about.tsx
┃ ┗ index.tsx
┣ prisma/
┃ ┣ migrations/
┃ ┣ schema.prisma
┃ ┗ seed.ts
┣ public/
┣ styles/
┃ ┗ tailwind.css
┣ .env.example
┣ .gitignore
┣ next-env.d.ts
┣ package-lock.json
┣ package.json
┣ postcss.config.js
┣ README.md
┣ tailwind.config.js
┗ tsconfig.json
這是一個 Next.js 應用程序,帶有 TailwindCSS 和 Prisma 設置。
在該pages
目錄中,您將找到三個文件:
_app.tsx
:全局App組件用于添加一個在頁面切換之間持續顯示的導航欄,并應用全局CSS樣式。about.tsx
:此文件導出一個 React 組件,該組件呈現位于http://localhost:3000/about 的頁面。index.tsx
:主頁,其中包含鏈接列表。這些鏈接被硬編碼在/data/links.ts
文件中。接下來,您將找到一個prisma包含以下文件的目錄:
schema.prisma
:我們數據庫的模式,用 PSL(Prisma 模式語言)編寫。seed.ts
:使用虛擬數據為數據庫播種的腳本。在課程的最后部分,您將利用Prisma來配置數據庫層。緊接著的下一步,就是在數據模型的基礎上構建API層,這將使得您能夠從客戶端接收請求或向其發送數據。
構造 API 的常見方法是讓客戶端向不同的 URL 端點發送請求。服務器將根據請求類型檢索或修改資源并發回響應。這種架構風格稱為 REST,它具有以下幾個優點:
雖然 REST API 具有優點,但它們也有一些缺點。我們將以此awesome-links
為例。
以下是構建 REST API 的一種可能方法:
資源 | HTTP方法 | 路線 | 描述 |
---|---|---|---|
User | GET | /users | 返回所有用戶及其信息 |
User | GET | /users/:id | 返回單個用戶 |
Link | GET | /links | 返回所有鏈接 |
Link | GET , PUT ,DELETE | /links/:id | 返回單個鏈接、更新或刪除它。id 是鏈接的 id |
User | GET | /favorites | 返回用戶添加書簽的鏈接 |
User | POST | /link/save | 添加指向用戶收藏夾的鏈接 |
Link | POST | /link/new | 創建一個新鏈接(由管理員完成) |
其他開發人員可能會根據自己的喜好和認為合適的方式來構建他們的REST API,這種靈活性確實帶來了一定的代價,即每個API都可能存在差異。
這意味著每次使用 REST API 時,您都需要閱讀其文檔并了解:
當第一次使用 API 時,這種學習曲線會增加摩擦并降低開發人員的工作效率。
另一方面,構建 API 的后端開發人員需要管理它并維護其文檔。
隨著應用程序復雜度的提升,API也會相應地變得更加復雜:更多的需求會導致需要創建更多的端點來滿足這些需求。
端點的增加很可能會帶來兩個問題:數據過度獲取和數據獲取不足。
當您獲取的數據多于所需的數據時,就會發生過度獲取。這會導致性能下降,因為您消耗了更多帶寬。
另一方面,有時您可能會發現某個端點沒有返回在用戶界面(UI)上顯示所需的所有信息,因此您最終不得不向另一個端點或多個端點發出請求來獲取這些信息。這種做法會導致性能下降,因為需要執行大量的網絡請求。
在“awesome-links”應用程序中,如果您希望頁面顯示所有用戶及其鏈接,您將需要對端點進行 API 調用/users/
,然后發出另一個請求以/favorites
獲取他們的收藏夾。
讓/users
端點返回用戶及其收藏夾并不能解決問題。這是因為您最終會得到一個重要的 API 響應,需要很長時間才能加載。
REST API 的另一個缺點是它們沒有類型。您不知道端點返回的數據類型,也不知道要發送的數據類型。這會導致對 API 做出假設,從而可能導致錯誤或不可預測的行為。
例如,在發出請求時,您是否將用戶 ID 作為字符串或數字傳遞?哪些請求參數是可選的,哪些是必需的?這就是您需要依賴文檔的原因,然而,隨著API的不斷演進,文檔可能會變得不再準確或過時。盡管存在一些可以解決這些挑戰的解決方案,但在本課程中我們不會詳細介紹它們。
GraphQL 是一種新的 API 標準,由 Facebook 開發并開源。它提供了一種比 REST 更高效、更靈活的替代方案,客戶端可以準確接收其所需的數據。
您只需要將請求發送到單一的端點,而不是分別向一個或多個端點發送請求,然后再將它們的響應結果進行拼接。
以下是 GraphQL 查詢的示例,該查詢返回“awesome-links”應用程序中的所有鏈接。您稍后將在構建 API 時定義此查詢:
query {
links {
id
title
description
}
}
即使鏈接有更多字段,API 也僅返回id
和title
。
注意:這是 GraphQL,一個運行 GraphQL 操作的游樂場。它提供了很好的功能,我們將更詳細地介紹這些功能
現在您將了解如何開始構建 GraphQL API。
這一切都是從GraphQL架構開始的,在這個架構中,您可以定義API所能執行的所有操作。同時,您還可以明確指定每個操作的輸入參數以及預期的響應類型。
該模式充當客戶端和服務器之間的契約。它還可以作為使用 GraphQL API 的開發人員的文檔。您可以使用 GraphQL 的 SDL(模式定義語言)來定義模式。
讓我們看看如何為“awesome-links”應用程序定義 GraphQL 模式。
您需要做的第一件事是定義一個對象類型。對象類型表示您可以從 API 獲取的一種對象。
每種對象類型可以有一個或多個字段。由于您希望應用程序中有用戶,因此您需要定義一個User
對象類型:
type User {
id: ID
email: String
image: String
role: Role
bookmarks: [Link]
}
enum Role {
ADMIN
USER
}
該User
類型具有以下字段:
id
,其類型為ID
。email
,其類型為String.
image
,其類型為String
。role
,其類型為Role
。這是一個枚舉,這意味著用戶的角色可以采用兩個值之一:USER
或ADMIN
。bookmarks
,這是一個Link
類型的數組。這意味著用戶可以擁有多個鏈接。接下來,您將定義這個Link
對象。這是對象類型的定義:
type Link {
id: ID
category: String
description: String
imageUrl: String
title: String
url: String
users: [User]
}
Link
和User
之間存在多對多的關系,因為一個Link
可以有多個用戶,同時一個User
也可以有多個鏈接。這是使用Prisma在數據庫中建模的。
要從 GrahQL API 獲取數據,您需要定義一個Query
對象類型。在這種類型中,您可以定義每個 GraphQL 查詢的入口點。對于每個入口點,您定義其參數及其返回類型。
這是返回所有鏈接的查詢。
type Query {
links: [Link]!
}
查詢links
返回類型為Link
的數組。用于!
指示該字段不可為 null,這意味著 API 在查詢該字段時將始終返回一個值。
您可以根據要構建的 API 類型添加更多查詢。對于“awesome-links”應用程序,您可以添加一個查詢來返回單個鏈接,另一個查詢返回單個用戶,另一個查詢返回所有用戶。
type Query {
links: [Link]!
link(id: ID!): Link!
user(id: ID!): User!
users: [User]!
}
這個查詢接收一個類型為ID
、名為id
的參數(這個參數是必需的),并且返回一個Link
類型的結果(響應結果不可為空)。其中,id
用于指定要查詢的鏈接的唯一標識符。
要創建、更新或刪除數據,您需要定義Mutation
對象類型。按照約定,任何導致寫入的操作都應通過突變顯式發送。同樣,您不應該使用GET
請求來修改數據。
對于“awesome-links”應用程序,您將需要不同的突變來創建、更新和刪除鏈接:
type Mutation {
createLink(category: String!, description: String!, imageUrl: String!, title: String!, url: String!): Link!
deleteLink(id: ID!): Link!
updateLink(category: String, description: String, id: String, imageUrl: String, title: String, url: String): Link!
}
category
、description
、title
、url
以及imageUrl
。所有這些字段都是字符串(String
)類型,并且是必需的。此突變返回一個Link
對象類型的結果。deleteLink
突變采用id
of 類型ID
作為必需參數。它返回一個必需的Link
.updateLink
采用與突變createLink
相同的參數,但是這些參數在updateLink
中是可選的。這意味著,當您更新一個Link
時,只需要傳遞您想要更新的那些字段。此突變返回一個Link
對象,且該對象是必需的。到目前為止,您只定義了 GraphQL API 的架構,但尚未指定查詢或突變運行時應該發生什么。負責執行查詢或突變的函數被稱為解析器(Resolver)。在解析器的內部,您可以執行向數據庫發送查詢的操作,或者向第三方API發起請求。
在本教程中,您將在解析器中使用Prisma將查詢發送到 PostgreSQL 數據庫。
要構建 GraphQL API,您將需要一個為單個端點提供服務的 GraphQL 服務器。
該服務器將包含 GraphQL 架構以及解析器。對于此項目,您將使用 GraphQL Yoga。
首先,在您一開始克隆的入門存儲庫中,在終端中運行以下命令:
npm install graphql graphql-yoga
graphql
包是GraphQL的JavaScript參考實現。而graphql-yoga
則是一種基于graphql
的對等依賴項。
接下來,您需要定義 GraphQL 模式。在項目的根文件夾中創建一個新graphql
目錄,并在其中創建一個新schema.ts
文件。您將定義該Link
對象以及返回所有鏈接的查詢。
// graphql/schema.ts
export const typeDefs = `
type Link {
id: ID
title: String
description: String
url: String
category: String
imageUrl: String
users: [String]
}
type Query {
links: [Link]!
}
您需要做的下一件事是為查詢創建解析器函數links
。為此,請創建一個/graphql/resolvers.ts
文件并添加以下代碼:
// /graphql/resolvers.ts
export const resolvers = {
Query: {
links: () => {
return [
{
category: 'Open Source',
description: 'Fullstack React framework',
id: 1,
imageUrl: 'https://nextjs.org/static/twitter-cards/home.jpg',
title: 'Next.js',
url: 'https://nextjs.org',
},
{
category: 'Open Source',
description: 'Next Generation ORM for TypeScript and JavaScript',
id: 2,
imageUrl: 'https://www.prisma.io/images/og-image.png',
title: 'Prisma',
url: 'https://www.prisma.io',
},
{
category: 'Open Source',
description: 'GraphQL implementation',
id: 3,
imageUrl: 'https://www.apollographql.com/apollo-home.jpg',
title: 'Apollo GraphQL',
url: 'https://apollographql.com',
},
]
},
},
}
resolvers
是一個對象,您將在其中定義每個查詢和突變的實現。對象內的Query
函數中的方法名稱必須與GraphQL架構中定義的查詢名稱相匹配。同樣地,對于突變(Mutation)也是如此,這里links
解析器函數返回一個對象數組,其中每個對象的類型為Link
。
要創建 GraphQL 端點,您將利用 Next.js 的API 路由。文件夾內的任何文件/pages/api
都會映射到/api/*
端點并被視為 API 端點。
繼續創建一個/pages/api/graphql.ts
文件并添加以下代碼:
// pages/api/graphql.ts
import { createSchema, createYoga } from 'graphql-yoga'
import type { NextApiRequest, NextApiResponse } from 'next'
import { resolvers } from '../../graphql/resolvers'
import { typeDefs } from '../../graphql/schema'
export default createYoga<{
req: NextApiRequest
res: NextApiResponse
}>({
schema: createSchema({
typeDefs,
resolvers
}),
graphqlEndpoint: '/api/graphql'
})
export const config = {
api: {
bodyParser: false
}
}
您創建了一個新的 GraphQL Yoga 服務器實例,該實例是默認導出。您還使用createSchema
將類型定義和解析器作為參數的函數創建了一個架構。
然后,您使用graphqlEndpoint
屬性來指定GraphQL API的路徑為/api/graphql
。
最后,每個 API 路由都可以導出一個config
對象來更改默認配置。
完成前面的步驟后,通過運行以下命令啟動服務器:
npm run dev
當您導航到 時http://localhost:3000/api/graphql/
,您應該看到以下頁面:
GraphQL Yoga 提供了一個交互式游樂場,名為GraphiQL,通過它您可以探索GraphQL架構并與API進行交互。
使用以下查詢更新右側選項卡上的內容,然后點擊CMD/ CTRL+Enter執行查詢:
query {
links {
id
title
description
}
}
類似的屏幕截圖中的左側面板應該能夠顯示出響應內容。
文檔資源管理器(頁面左上角按鈕)將允許您單獨探索每個查詢/突變,查看不同的所需參數及其類型。
到目前為止,GraphQL API 在解析器函數中返回硬編碼數據。您將在這些函數中使用 Prisma Client 將查詢發送到數據庫。
Prisma Client 是一個自動生成的、類型安全的查詢生成器。為了能夠在您的項目中使用它,您應該實例化它一次,然后在整個項目中重用它。繼續/lib
在項目的根文件夾中創建一個文件夾,并在其中創建一個prisma.ts
文件。接下來,添加以下代碼:
// /lib/prisma.ts
import { PrismaClient } from '@prisma/client'
let prisma: PrismaClient
declare global {
var prisma: PrismaClient;
}
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
export default prisma
首先,您要創建一個新的 Prisma 客戶端實例。那么,如果你不在生產環境中,Prisma 會將其附加到全局對象上,這樣你就不會因為數據庫連接限制而被耗盡。有關更多詳細信息,請參考Next.js和Prisma Client的最佳實踐文檔。
現在您可以更新解析器以從數據庫返回數據。在文件內/graphql/resolvers.ts
,將links
函數更新為以下代碼:
// /graphql/resolvers.ts
import prisma from '../lib/prisma'
export const resolvers = {
Query: {
links: () => {
return prisma.link.findMany()
},
},
}
如果一切設置正確,當您轉到 GraphiQL,athttp://localhost:3000/api/graphql
并重新運行鏈接查詢時,應該從數據庫中檢索數據。
當 GraphQL API 變得越來越復雜時,當前手動創建模式和解析器的工作流程可能會降低開發人員的工作效率:
為了解決這些問題,可以使用 GraphQL 代碼生成器等工具組合。或者,您可以在使用解析器構建架構時使用代碼優先方法。
Pothos 是一個 GraphQL 模式構建庫,您可以在其中使用代碼定義 GraphQL 模式。這種方法的價值主張是您使用編程語言來構建 API,這有多種好處:
這些好處能夠減少開發過程中的摩擦,并帶來更加出色的開發體驗。
在本教程中,您將使用 Pothos。它還為 Prisma 提供了一個很棒的插件,可以在 GraphQL 類型和 Prisma 架構之間提供良好的開發體驗和類型安全性。
注意:Pothos 能夠與 Prisma 以類型安全的方式協同工作,且無需依賴插件,但這一過程相對較為手動。
首先,運行以下命令來安裝 Pothos 和 Pothos 的 Prisma 插件:
npm install @pothos/plugin-prisma @pothos/core
接下來,將pothos
生成器塊添加到生成器正下方的 Prisma 架構中client
:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
generator pothos {
provider = "prisma-pothos-types"
}
運行以下命令重新生成 Prisma Client 和 Pothos 類型:
npx prisma generate
接下來,創建 Pothos 架構構建器的實例作為可共享模塊。在該graphql
文件夾內,創建一個名為的新文件builder.ts
并添加以下代碼片段:
// graphql/builder.ts
// 1.
import SchemaBuilder from "@pothos/core";
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '@pothos/plugin-prisma/generated';
import prisma from "../lib/prisma";
// 2.
export const builder = new SchemaBuilder<{
// 3.
PrismaTypes: PrismaTypes
}>({
// 4.
plugins: [PrismaPlugin],
prisma: {
client: prisma,
}
})
// 5.
builder.queryType({
fields: (t) => ({
ok: t.boolean({
resolve: () => true,
}),
}),
});// graphql/builder.ts
// 1.
import SchemaBuilder from "@pothos/core";
import PrismaPlugin from '@pothos/plugin-prisma';
import type PrismaTypes from '@pothos/plugin-prisma/generated';
import prisma from "../lib/prisma";
// 2.
export const builder = new SchemaBuilder<{
// 3.
PrismaTypes: PrismaTypes
}>({
// 4.
plugins: [PrismaPlugin],
prisma: {
client: prisma,
}
})
// 5.
builder.queryType({
fields: (t) => ({
ok: t.boolean({
resolve: () => true,
}),
}),
});
SchemaBuilder
實例SchemaBuilder
時,可以定義包括將使用的插件、Prisma 客戶端實例等在內的多項選項。queryType
帶有名為ok
返回布爾值的查詢接下來,您需要在?/graphql/schema.ts
?文件中,用以下從 Pothos 構建器創建的 GraphQL 架構來替換?typeDefs
。
// graphql/schema.ts
import { builder } from "./builder";
export const schema = builder.toSchema()
最后,更新文件中的導入/pages/api/graphql.ts
:
// /pages/api/graphql.ts
import { createSchema, createYoga } from 'graphql-yoga'
import { createYoga } from 'graphql-yoga'
import type { NextApiRequest, NextApiResponse } from 'next'
import { resolvers } from '../../graphql/resolvers'
import { typeDefs } from '../../graphql/schema'
import { schema } from '../../graphql/schema'
export default createYoga<{
req: NextApiRequest
res: NextApiResponse
}>({
schema: createSchema({
typeDefs,
resolvers
}),
schema,
graphqlEndpoint: '/api/graphql'
})
export const config = {
api: {
bodyParser: false
}
}
“graphql.ts”文件的更新內容:
// /pages/api/graphql.ts
import { createYoga } from 'graphql-yoga'
import type { NextApiRequest, NextApiResponse } from 'next'
import { schema } from '../../graphql/schema'
export default createYoga<{
req: NextApiRequest
res: NextApiResponse
}>({
schema,
graphqlEndpoint: '/api/graphql'
})
export const config = {
api: {
bodyParser: false
}
}
確保服務器正在運行并導航到http://localhost:3000/api/graphql
.您將能夠發送帶有ok
字段的查詢,該字段將返回true
第一步是Link
使用 Pothos 定義對象類型。繼續創建一個/graphql/types/Link.ts
文件,添加以下代碼:
// /graphql/types/Link.ts
import { builder } from "../builder";
builder.prismaObject('Link', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
url: t.exposeString('url'),
description: t.exposeString('description'),
imageUrl: t.exposeString('imageUrl'),
category: t.exposeString('category'),
users: t.relation('users')
})
})
由于您正在使用 Pothos 的 Prisma 插件,因此該?builder
?實例提供了諸如?prismaObject
?等實用方法,用于定義 GraphQL 模式。
prismaObject
接受兩個參數:
name
:您想要公開的Prisma 模式中模型的名稱。options
:用于定義要公開的類型的選項,例如描述、字段等。注意:您可以使用CTRL+Space調用編輯器的智能感知并查看可用參數。
fields
?屬性用于指定您希望通過“暴露”函數從 Prisma 架構中獲取并包含在 GraphQL 模式中的字段。在本教程中,我們將公開?id
、title
、url
、imageUrl
?和?category
?這幾個字段。
該t.relation
方法用于定義您希望從 Prisma 架構中公開的關系字段。
現在創建一個新/graphql/types/User.ts
文件并將以下內容添加到代碼中以創建User
類型:
// /graphql/types/User.ts
import { builder } from "../builder";
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeID('id'),
email: t.exposeString('email', { nullable: true, }),
image: t.exposeString('image', { nullable: true, }),
role: t.expose('role', { type: Role, }),
bookmarks: t.relation('bookmarks'),
})
})
const Role = builder.enumType('Role', {
values: ['USER', 'ADMIN'] as const,
})
由于 Prisma 模式中的 email
字段(以及其他可能同樣可為空的字段)允許為空值,因此在通過“暴露”方法將其添加到 GraphQL 架構時,我們需要將 { nullable: true }
作為第二個參數進行傳遞。
在從生成的架構中“公開”字段類型時,role
?字段的默認類型是其原有類型。但在上面的示例中,您首先定義了一個顯式的枚舉類型?Role
,隨后使用它來明確指定?role
?字段的類型。
要使架構的定義對象類型在 GraphQL 架構中可用,請將導入添加到您剛剛在graphql/schema.ts
文件中創建的類型:
// graphql/schema.ts
import "./types/Link"
import "./types/User"
import { builder } from "./builder";
export const schema = builder.toSchema()
在該graphql/types/Link.ts
文件中,在對象類型定義下方添加以下代碼Link
:
// graphql/types/Link.ts
// code above unchanged
// 1.
builder.queryField("links", (t) =>
// 2.
t.prismaField({
// 3.
type: ['Link'],
// 4.
resolve: (query, _parent, _args, _ctx, _info) =>
prisma.link.findMany({ ...query })
})
)
在上面的代碼片段中:
links
。Link
類型的數組query
解析器函數中的參數將或添加到select
您include
的查詢中,以在單個請求中解析盡可能多的關系字段。
現在,如果您回到 GraphiQL 界面,就可以發送一個查詢請求,這個請求會返回數據庫中所有的鏈接數據。
對于此項目,您將使用 Apollo 客戶端。您可以通過發送常規的 HTTP POST 請求來與剛剛構建好的 GraphQL API 進行通信。不過,使用 GraphQL 客戶端能夠帶來諸多便利和優勢。
Apollo 客戶端負責請求和緩存您的數據,以及更新您的 UI。它還包括查詢批處理、查詢重復數據刪除和分頁功能。
要開始使用 Apollo Client,請通過運行以下命令添加到您的項目:
npm install @apollo/client
接下來,在/lib
目錄中創建一個名為的新文件apollo.ts
,并向其中添加以下代碼:
// /lib/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client'
const apolloClient = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache(),
})
export default apolloClient
您正在創建一個新ApolloClient
實例,并向其中傳遞帶有uri
和cache
字段的配置對象。
uri
字段指定您將與之交互的 GraphQL 端點。部署應用程序時,這將更改為生產 URL。cache
字段是 InMemoryCache 的一個實例,Apollo Client 會獲取查詢結果,并使用?cache
來存儲這些查詢結果以便緩存。接下來,轉到該/pages/_app.tsx
文件并向其中添加以下代碼,以設置 Apollo 客戶端:
// /pages/_app.tsx
import '../styles/tailwind.css'
import Layout from '../components/Layout'
import { ApolloProvider } from '@apollo/client'
import apolloClient from '../lib/apollo'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<Layout>
<Component {...pageProps} />
</Layout>
</ApolloProvider>
)
}
export default MyApp
您正在使用 Apollo Provider 包裝全局App
組件,以便項目的所有組件都可以發送 GraphQL 查詢。
注意:Next.js 支持不同的數據獲取策略。您可以在服務器端、客戶端或構建時獲取數據。為了支持分頁,您需要在客戶端獲取數據。
要使用 Apollo 客戶端在前端加載數據,請更新/pages/index.tsx
文件以使用以下代碼:
// /pages/index.tsx
import Head from 'next/head'
import { gql, useQuery } from '@apollo/client'
import type { Link } from '@prisma/client'
const AllLinksQuery = gql`
query {
links {
id
title
url
description
imageUrl
category
}
}
`
export default function Home() {
const { data, loading, error } = useQuery(AllLinksQuery)
if (loading) return <p>Loading...</p>
if (error) return <p>Oh no... {error.message}</p>
return (
<div>
<Head>
<title>Awesome Links</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="container mx-auto max-w-5xl my-20">
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{data.links.map((link: Link) => (
<li key={link.id} className="shadow max-w-md rounded">
<img className="shadow-sm" src={link.imageUrl} />
<div className="p-5 flex flex-col space-y-2">
<p className="text-sm text-blue-500">{link.category}</p>
<p className="text-lg font-medium">{link.title}</p>
<p className="text-gray-600">{link.description}</p>
<a href={link.url} className="flex hover:text-blue-500">
{link.url.replace(/(^\w+:|^)\/\//, '')}
<svg
className="w-4 h-4 my-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</div>
</li>
))}
</ul>
</div>
</div>
)
}
您正在使用該useQuery
掛鉤將查詢發送到 GraphQL 端點。這個掛鉤需要一個必需的參數,即 GraphQL 查詢字符串。當組件進行渲染時,useQuery
?會返回一個對象,該對象包含三個值:
loading
:一個布爾值,確定數據是否已返回。error
:包含錯誤消息的對象,以防發送查詢后發生錯誤。data
:包含從 API 端點返回的數據。保存文件并導航到 后http://loclahost:3000
,您將看到從數據庫獲取的鏈接列表。
AllLinksQuery
?會返回數據庫中的所有鏈接。隨著應用程序的不斷擴展和鏈接數量的增加,您可能會收到大量的 API 響應,這將導致加載時間變長。此外,解析器發送的數據庫查詢也會變慢,因為您使用該prisma.link.findMany()
函數返回數據庫中的鏈接。
提升性能的一種常見做法就是引入分頁支持。分頁能夠將大型數據集切分成更小的數據塊,這樣我們就可以根據需要來請求特定的數據塊了。
有多種不同的方法來實現分頁。您可以進行編號頁面,例如 Google 搜索結果,也可以進行無限滾動,如 Twitter 的提要。
現在在數據庫級別,您可以使用兩種分頁技術:基于偏移量和基于游標的分頁。
游標通常需要是唯一且連續的列,比如 ID 或時間戳。與基于偏移量的分頁相比,這種方法更加高效,并且也是本教程中將要采用的方法。
為了使 GraphQL API 支持分頁,您需要向 GraphQL 架構引入中繼游標連接規范。這是 GraphQL 服務器應如何公開分頁數據的規范。
分頁查詢如下所示:
query allLinksQuery($first: Int, $after: ID) {
links(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
imageUrl
title
description
url
category
}
}
}
}
該查詢采用兩個參數,first
并且after
:
first
:Int
指定您希望 API 返回的項目數。after
:ID
為結果集中的最后一項添加書簽的參數,這就是光標。此查詢返回一個包含兩個字段的對象,pageInfo
并且edges
:
pageInfo
:這個對象會協助客戶端判斷是否還有更多數據需要獲取。它包含兩個字段:endCursor
?和?hasNextPage
。
endCursor
:結果集中最后一項的光標。該游標的類型為String
hasNextPage
:API 返回的布爾值,讓客戶端知道是否有更多頁面可以獲取。edges
是一個對象數組,其中每個對象都有 一個cursor
和 一個node
字段。這里的字段node
返回Link
對象類型。您將實現單向分頁,在頁面首次加載時請求一些鏈接,然后用戶可以通過單擊按鈕獲取更多鏈接。
或者,您也可以選擇在用戶滾動到頁面底部時自動發出這個請求。
其工作原理是在頁面首次加載時獲取一些數據。然后,單擊按鈕后,您向 API 發送第二個請求,其中包括您想要返回的項目數和光標。然后將數據返回并顯示在客戶端上。
注意:雙向分頁的一個示例是 Slack 等聊天應用程序,您可以在其中向前或向后加載消息。
Pothos 提供了一個插件,該插件專門用于處理中繼式光標分頁,其中包含了節點、連接以及其他一些實用的工具。
使用以下命令安裝插件:
npm install @pothos/plugin-relay
更新graphql/builder.ts
以包含中繼插件:
// graphql/builder.ts
import SchemaBuilder from "@pothos/core";
import PrismaPlugin from '@pothos/plugin-prisma';
import prisma from "../lib/prisma";
import type PrismaTypes from '@pothos/plugin-prisma/generated';
import RelayPlugin from '@pothos/plugin-relay';
export const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes
}>({
plugins: [PrismaPlugin],
plugins: [PrismaPlugin, RelayPlugin],
relayOptions: {},
prisma: {
client: prisma,
}
})
builder.queryType({
fields: (t) => ({
ok: t.boolean({
resolve: () => true,
}),
}),
});
要使用基于游標的分頁,請對查詢進行以下更新:
// ./graphql/types/Link.ts
// code remains unchanged
builder.queryField('links', (t) =>
t.prismaField({
t.prismaConnection({
type: ['Link'],
type: 'Link',
cursor: 'id',
resolve: (query, _parent, _args, _ctx, _info) =>
prisma.link.findMany({ ...query })
})
)
該prismaConnection
方法用于創建一個connection
字段,該字段還預加載該連接內的數據。
“Link.ts”文件的更新內容:
// /graphql/types/Link.ts
import { builder } from "../builder";
builder.prismaObject('Link', {
fields: (t) => ({
id: t.exposeID('id'),
title: t.exposeString('title'),
url: t.exposeString('url'),
description: t.exposeString('description'),
imageUrl: t.exposeString('imageUrl'),
category: t.exposeString('category'),
users: t.relation('users')
}),
})
builder.queryField('links', (t) =>
t.prismaConnection({
type: 'Link',
cursor: 'id',
resolve: (query, _parent, _args, _ctx, _info) =>
prisma.link.findMany({ ...query })
})
)
下面的圖表總結了分頁在服務器上的工作原理:
現在API支持分頁,您可以使用Apollo Client在客戶端獲取分頁數據。
useQuery
?鉤子會返回一個對象,該對象包含?data
、loading
?和?errors
?這幾個屬性。除此之外,useQuery
?還會提供一個?fetchMore()
?函數,這個函數用于處理分頁邏輯,并在獲取到新結果時更新用戶界面。導航到該/pages/index.tsx
文件并更新它以使用以下代碼添加對分頁的支持:
// /pages/index.tsx
import Head from "next/head";
import { gql, useQuery, useMutation } from "@apollo/client";
import { AwesomeLink } from "../components/AwesomeLink";
import type { Link } from "@prisma/client";
const AllLinksQuery = gql`
query allLinksQuery($first: Int, $after: ID) {
links(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
imageUrl
url
title
category
description
id
}
}
}
}
`;
function Home() {
const { data, loading, error, fetchMore } = useQuery(AllLinksQuery, {
variables: { first: 2 },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
const { endCursor, hasNextPage } = data.links.pageInfo;
return (
<div>
<Head>
<title>Awesome Links</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="container mx-auto max-w-5xl my-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{data?.links.edges.map(({ node }: { node: Link }) => (
<AwesomeLink
title={node.title}
category={node.category}
url={node.url}
id={node.id}
description={node.description}
imageUrl={node.imageUrl}
/>
))}
</div>
{hasNextPage ? (
<button
className="px-4 py-2 bg-blue-500 text-white rounded my-10"
onClick={() => {
fetchMore({
variables: { after: endCursor },
updateQuery: (prevResult, { fetchMoreResult }) => {
fetchMoreResult.links.edges = [
...prevResult.links.edges,
...fetchMoreResult.links.edges,
];
return fetchMoreResult;
},
});
}}
>
more
</button>
) : (
<p className="my-10 text-center font-medium">
You've reached the end!{" "}
</p>
)}
</div>
</div>
);
}
export default Home;
您在使用?useQuery
?掛鉤時,需要先傳遞一個?variables
?對象,該對象含有一個名為?first
?的鍵,其對應的值設為?2
。這意味著您希望初始時獲取兩個鏈接。當然,您可以根據實際需求將這個值設置為您想要的任何數字。
該data
變量將包含從對 API 的初始請求返回的數據。
然后,您將解構對象中的值。
如果 hasNextPage
的值為 true
,我們將展示一個按鈕,并為其設置 onClick
事件處理程序。這個處理程序會返回一個函數,該函數在被調用時會執行 fetchMore()
方法,而 fetchMore()
方法則需要接收一個對象作為參數,該對象包含以下字段:
variables
獲取endCursor
從初始數據返回的對象。updateQuery
函數,它負責通過將先前的結果與第二個查詢返回的結果相結合來更新 UI。如果hasNextPage
是false
,則意味著沒有更多可以獲取的鏈接。
如果您已經保存了更改并且應用程序正在運行,那么您應該能夠成功地從數據庫中獲取分頁數據。
恭喜!您已成功完成課程的第二部分!如果您遇到任何問題或有任何疑問,請隨時聯系我們的 Slack 社區。
在這一部分中,您了解了:
在課程的下一部分中,您將:
原文鏈接:https://www.prisma.io/blog/fullstack-nextjs-graphql-prisma-2-fwpc6ds155