介紹

在本課程中,您將學(xué)習(xí)如何構(gòu)建“awesome-links”,這是一個全棧應(yīng)用程序,用戶可以在其中瀏覽精選鏈接列表并為他們最喜歡的鏈接添加書簽。

在上一部分中,您已經(jīng)使用Prisma設(shè)置好了數(shù)據(jù)庫層。當(dāng)您完成本部分的學(xué)習(xí)后,將會對GraphQL有所了解:知道它是什么,以及如何在Next.js應(yīng)用程序中利用它來構(gòu)建API。

開發(fā)環(huán)境

要學(xué)習(xí)本教程,您需要安裝 Node.js 和GraphQL 擴(kuò)展。您還需要有一個正在運(yùn)行的 PostgreSQL 實(shí)例。

注意:您可以選擇在本地安裝PostgreSQL,或者在Heroku上設(shè)置一個托管的數(shù)據(jù)庫實(shí)例。但請注意,為了執(zhí)行課程結(jié)束時的部署步驟,您將需要一個遠(yuǎn)程數(shù)據(jù)庫。

克隆存儲庫

您可以在 GitHub 上找到該課程的完整源代碼。

:每篇文章都有相應(yīng)的分支。這樣,您就可以跟著進(jìn)行。通過查看第 2 部分分支,您將獲得與本文相同的起點(diǎn)。

首先,導(dǎo)航到您選擇的目錄并運(yùn)行以下命令來克隆存儲庫。

git clone -b part-2 https://github.com/m-abdelwahab/awesome-links.git

現(xiàn)在,您可以進(jìn)入已克隆的目錄,安裝所需的依賴項(xiàng),并啟動開發(fā)服務(wù)器。

cd awesome-linksnpm installnpm run dev

該應(yīng)用程序?qū)⑦\(yùn)行http://localhost:3000/,您將看到四個項(xiàng)目。數(shù)據(jù)是硬編碼的,來源于/data/links.ts文件。

數(shù)據(jù)初始化

設(shè)置好PostgreSQL數(shù)據(jù)庫后,請將env.example文件重命名為.env,并配置好數(shù)據(jù)庫的連接字符串。接著,運(yùn)行以下命令來在數(shù)據(jù)庫中創(chuàng)建遷移和所需的表:

npx prisma migrate dev --name init

如果prisma migrate dev沒有觸發(fā)種子步驟,請運(yùn)行以下命令來初始化數(shù)據(jù):

npx prisma db seed

此命令將運(yùn)行seed.ts位于/prisma目錄中的腳本。此腳本使用 Prisma 客戶端向您的數(shù)據(jù)庫添加四個鏈接和一個用戶。

查看項(xiàng)目結(jié)構(gòu)和依賴關(guān)系

您將看到以下文件夾結(jié)構(gòu):

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 應(yīng)用程序,帶有 TailwindCSS 和 Prisma 設(shè)置。

在該pages目錄中,您將找到三個文件:

接下來,您將找到一個prisma包含以下文件的目錄:

以傳統(tǒng)方式構(gòu)建 API:REST

在課程的最后部分,您將利用Prisma來配置數(shù)據(jù)庫層。緊接著的下一步,就是在數(shù)據(jù)模型的基礎(chǔ)上構(gòu)建API層,這將使得您能夠從客戶端接收請求或向其發(fā)送數(shù)據(jù)。

構(gòu)造 API 的常見方法是讓客戶端向不同的 URL 端點(diǎn)發(fā)送請求。服務(wù)器將根據(jù)請求類型檢索或修改資源并發(fā)回響應(yīng)。這種架構(gòu)風(fēng)格稱為 REST,它具有以下幾個優(yōu)點(diǎn):

REST API 及其缺點(diǎn)

雖然 REST API 具有優(yōu)點(diǎn),但它們也有一些缺點(diǎn)。我們將以此awesome-links為例。

以下是構(gòu)建 REST API 的一種可能方法:

資源HTTP方法路線描述
UserGET/users返回所有用戶及其信息
UserGET/users/:id返回單個用戶
LinkGET/links返回所有鏈接
LinkGETPUT,DELETE/links/:id返回單個鏈接、更新或刪除它。id是鏈接的 id
UserGET/favorites返回用戶添加書簽的鏈接
UserPOST/link/save添加指向用戶收藏夾的鏈接
LinkPOST/link/new創(chuàng)建一個新鏈接(由管理員完成)

每個 REST API 都是不同的

其他開發(fā)人員可能會根據(jù)自己的喜好和認(rèn)為合適的方式來構(gòu)建他們的REST API,這種靈活性確實(shí)帶來了一定的代價(jià),即每個API都可能存在差異。

這意味著每次使用 REST API 時,您都需要閱讀其文檔并了解:

當(dāng)?shù)谝淮问褂?API 時,這種學(xué)習(xí)曲線會增加摩擦并降低開發(fā)人員的工作效率。

另一方面,構(gòu)建 API 的后端開發(fā)人員需要管理它并維護(hù)其文檔。

隨著應(yīng)用程序復(fù)雜度的提升,API也會相應(yīng)地變得更加復(fù)雜:更多的需求會導(dǎo)致需要創(chuàng)建更多的端點(diǎn)來滿足這些需求。

端點(diǎn)的增加很可能會帶來兩個問題:數(shù)據(jù)過度獲取和數(shù)據(jù)獲取不足

過度獲取和不足獲取

當(dāng)您獲取的數(shù)據(jù)多于所需的數(shù)據(jù)時,就會發(fā)生過度獲取。這會導(dǎo)致性能下降,因?yàn)槟牧烁鄮挕?/p>

另一方面,有時您可能會發(fā)現(xiàn)某個端點(diǎn)沒有返回在用戶界面(UI)上顯示所需的所有信息,因此您最終不得不向另一個端點(diǎn)或多個端點(diǎn)發(fā)出請求來獲取這些信息。這種做法會導(dǎo)致性能下降,因?yàn)樾枰獔?zhí)行大量的網(wǎng)絡(luò)請求。

在“awesome-links”應(yīng)用程序中,如果您希望頁面顯示所有用戶及其鏈接,您將需要對端點(diǎn)進(jìn)行 API 調(diào)用/users/,然后發(fā)出另一個請求以/favorites獲取他們的收藏夾。

/users端點(diǎn)返回用戶及其收藏夾并不能解決問題。這是因?yàn)槟罱K會得到一個重要的 API 響應(yīng),需要很長時間才能加載。

REST API 未鍵入

REST API 的另一個缺點(diǎn)是它們沒有類型。您不知道端點(diǎn)返回的數(shù)據(jù)類型,也不知道要發(fā)送的數(shù)據(jù)類型。這會導(dǎo)致對 API 做出假設(shè),從而可能導(dǎo)致錯誤或不可預(yù)測的行為。

例如,在發(fā)出請求時,您是否將用戶 ID 作為字符串或數(shù)字傳遞?哪些請求參數(shù)是可選的,哪些是必需的?這就是您需要依賴文檔的原因,然而,隨著API的不斷演進(jìn),文檔可能會變得不再準(zhǔn)確或過時。盡管存在一些可以解決這些挑戰(zhàn)的解決方案,但在本課程中我們不會詳細(xì)介紹它們。

GraphQL的替代方案

GraphQL 是一種新的 API 標(biāo)準(zhǔn),由 Facebook 開發(fā)并開源。它提供了一種比 REST 更高效、更靈活的替代方案,客戶端可以準(zhǔn)確接收其所需的數(shù)據(jù)。

您只需要將請求發(fā)送到單一的端點(diǎn),而不是分別向一個或多個端點(diǎn)發(fā)送請求,然后再將它們的響應(yīng)結(jié)果進(jìn)行拼接。

以下是 GraphQL 查詢的示例,該查詢返回“awesome-links”應(yīng)用程序中的所有鏈接。您稍后將在構(gòu)建 API 時定義此查詢:

query {
links {
id
title
description
}
}

即使鏈接有更多字段,API 也僅返回idtitle

注意:這是 GraphQL,一個運(yùn)行 GraphQL 操作的游樂場。它提供了很好的功能,我們將更詳細(xì)地介紹這些功能

現(xiàn)在您將了解如何開始構(gòu)建 GraphQL API。

定義模式

這一切都是從GraphQL架構(gòu)開始的,在這個架構(gòu)中,您可以定義API所能執(zhí)行的所有操作。同時,您還可以明確指定每個操作的輸入?yún)?shù)以及預(yù)期的響應(yīng)類型。

該模式充當(dāng)客戶端和服務(wù)器之間的契約。它還可以作為使用 GraphQL API 的開發(fā)人員的文檔。您可以使用 GraphQL 的 SDL(模式定義語言)來定義模式。

讓我們看看如何為“awesome-links”應(yīng)用程序定義 GraphQL 模式。

定義對象類型和字段

您需要做的第一件事是定義一個對象類型。對象類型表示您可以從 API 獲取的一種對象。

每種對象類型可以有一個或多個字段。由于您希望應(yīng)用程序中有用戶,因此您需要定義一個User對象類型:

type User {
id: ID
email: String
image: String
role: Role
bookmarks: [Link]
}

enum Role {
ADMIN
USER
}

User類型具有以下字段:

這是對象類型的定義:

type Link {
id: ID
category: String
description: String
imageUrl: String
title: String
url: String
users: [User]
}

LinkUser之間存在多對多的關(guān)系,因?yàn)橐粋€Link可以有多個用戶,同時一個User也可以有多個鏈接。這是使用Prisma在數(shù)據(jù)庫中建模的。

定義查詢

要從 GrahQL API 獲取數(shù)據(jù),您需要定義一個Query對象類型。在這種類型中,您可以定義每個 GraphQL 查詢的入口點(diǎn)。對于每個入口點(diǎn),您定義其參數(shù)及其返回類型。

這是返回所有鏈接的查詢。

type Query {
links: [Link]!
}

查詢links返回類型為Link的數(shù)組。用于!指示該字段不可為 null,這意味著 API 在查詢該字段時將始終返回一個值。

您可以根據(jù)要構(gòu)建的 API 類型添加更多查詢。對于“awesome-links”應(yīng)用程序,您可以添加一個查詢來返回單個鏈接,另一個查詢返回單個用戶,另一個查詢返回所有用戶。

type Query {
links: [Link]!
link(id: ID!): Link!
user(id: ID!): User!
users: [User]!
}

這個查詢接收一個類型為ID、名為id的參數(shù)(這個參數(shù)是必需的),并且返回一個Link類型的結(jié)果(響應(yīng)結(jié)果不可為空)。其中,id用于指定要查詢的鏈接的唯一標(biāo)識符。

定義突變

要創(chuàng)建、更新或刪除數(shù)據(jù),您需要定義Mutation對象類型。按照約定,任何導(dǎo)致寫入的操作都應(yīng)通過突變顯式發(fā)送。同樣,您不應(yīng)該使用GET請求來修改數(shù)據(jù)。

對于“awesome-links”應(yīng)用程序,您將需要不同的突變來創(chuàng)建、更新和刪除鏈接:

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!
}

定義查詢和突變的實(shí)現(xiàn)

到目前為止,您只定義了 GraphQL API 的架構(gòu),但尚未指定查詢或突變運(yùn)行時應(yīng)該發(fā)生什么。負(fù)責(zé)執(zhí)行查詢或突變的函數(shù)被稱為解析器(Resolver)。在解析器的內(nèi)部,您可以執(zhí)行向數(shù)據(jù)庫發(fā)送查詢的操作,或者向第三方API發(fā)起請求。

在本教程中,您將在解析器中使用Prisma將查詢發(fā)送到 PostgreSQL 數(shù)據(jù)庫。

構(gòu)建 GraphQL API

要構(gòu)建 GraphQL API,您將需要一個為單個端點(diǎn)提供服務(wù)的 GraphQL 服務(wù)器。

該服務(wù)器將包含 GraphQL 架構(gòu)以及解析器。對于此項(xiàng)目,您將使用 GraphQL Yoga。

首先,在您一開始克隆的入門存儲庫中,在終端中運(yùn)行以下命令:

npm install graphql graphql-yoga

graphql包是GraphQL的JavaScript參考實(shí)現(xiàn)。而graphql-yoga則是一種基于graphql的對等依賴項(xiàng)。

定義應(yīng)用程序的架構(gòu)

接下來,您需要定義 GraphQL 模式。在項(xiàng)目的根文件夾中創(chuàng)建一個新graphql目錄,并在其中創(chuàng)建一個新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]!
}

定義解析器

您需要做的下一件事是為查詢創(chuàng)建解析器函數(shù)links。為此,請創(chuàng)建一個/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是一個對象,您將在其中定義每個查詢和突變的實(shí)現(xiàn)。對象內(nèi)的Query函數(shù)中的方法名稱必須與GraphQL架構(gòu)中定義的查詢名稱相匹配。同樣地,對于突變(Mutation)也是如此,這里links解析器函數(shù)返回一個對象數(shù)組,其中每個對象的類型為Link

創(chuàng)建 GraphQL 端點(diǎn)

要創(chuàng)建 GraphQL 端點(diǎn),您將利用 Next.js 的API 路由。文件夾內(nèi)的任何文件/pages/api都會映射到/api/*端點(diǎn)并被視為 API 端點(diǎn)。

繼續(xù)創(chuàng)建一個/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
}
}

您創(chuàng)建了一個新的 GraphQL Yoga 服務(wù)器實(shí)例,該實(shí)例是默認(rèn)導(dǎo)出。您還使用createSchema將類型定義和解析器作為參數(shù)的函數(shù)創(chuàng)建了一個架構(gòu)。

然后,您使用graphqlEndpoint屬性來指定GraphQL API的路徑為/api/graphql

最后,每個 API 路由都可以導(dǎo)出一個config對象來更改默認(rèn)配置。

使用 GraphiQL 發(fā)送查詢

完成前面的步驟后,通過運(yùn)行以下命令啟動服務(wù)器:

npm run dev

當(dāng)您導(dǎo)航到 時http://localhost:3000/api/graphql/,您應(yīng)該看到以下頁面:

用于運(yùn)行查詢的 GraphiQL Playground

GraphQL Yoga 提供了一個交互式游樂場,名為GraphiQL,通過它您可以探索GraphQL架構(gòu)并與API進(jìn)行交互。

使用以下查詢更新右側(cè)選項(xiàng)卡上的內(nèi)容,然后點(diǎn)擊CMDCTRL+Enter執(zhí)行查詢:

query {
links {
id
title
description
}
}
帶有示例查詢的 GraphiQL Playground

類似的屏幕截圖中的左側(cè)面板應(yīng)該能夠顯示出響應(yīng)內(nèi)容。

文檔資源管理器(頁面左上角按鈕)將允許您單獨(dú)探索每個查詢/突變,查看不同的所需參數(shù)及其類型。

GraphiQL 文檔瀏覽器

初始化 Prisma 客戶端

到目前為止,GraphQL API 在解析器函數(shù)中返回硬編碼數(shù)據(jù)。您將在這些函數(shù)中使用 Prisma Client 將查詢發(fā)送到數(shù)據(jù)庫。

Prisma Client 是一個自動生成的、類型安全的查詢生成器。為了能夠在您的項(xiàng)目中使用它,您應(yīng)該實(shí)例化它一次,然后在整個項(xiàng)目中重用它。繼續(xù)/lib在項(xiàng)目的根文件夾中創(chuàng)建一個文件夾,并在其中創(chuàng)建一個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

首先,您要創(chuàng)建一個新的 Prisma 客戶端實(shí)例。那么,如果你不在生產(chǎn)環(huán)境中,Prisma 會將其附加到全局對象上,這樣你就不會因?yàn)閿?shù)據(jù)庫連接限制而被耗盡。有關(guān)更多詳細(xì)信息,請參考Next.js和Prisma Client的最佳實(shí)踐文檔。

使用Prisma查詢數(shù)據(jù)庫

現(xiàn)在您可以更新解析器以從數(shù)據(jù)庫返回?cái)?shù)據(jù)。在文件內(nèi)/graphql/resolvers.ts,將links函數(shù)更新為以下代碼:

// /graphql/resolvers.ts
import prisma from '../lib/prisma'
export const resolvers = {
Query: {
links: () => {
return prisma.link.findMany()
},
},
}

如果一切設(shè)置正確,當(dāng)您轉(zhuǎn)到 GraphiQL,athttp://localhost:3000/api/graphql并重新運(yùn)行鏈接查詢時,應(yīng)該從數(shù)據(jù)庫中檢索數(shù)據(jù)。

我們當(dāng)前 GraphQL 設(shè)置的缺陷

當(dāng) GraphQL API 變得越來越復(fù)雜時,當(dāng)前手動創(chuàng)建模式和解析器的工作流程可能會降低開發(fā)人員的工作效率:

為了解決這些問題,可以使用 GraphQL 代碼生成器等工具組合。或者,您可以在使用解析器構(gòu)建架構(gòu)時使用代碼優(yōu)先方法。

使用Pothos 進(jìn)行代碼優(yōu)先的 GraphQL API 開發(fā)

Pothos 是一個 GraphQL 模式構(gòu)建庫,您可以在其中使用代碼定義 GraphQL 模式。這種方法的價(jià)值主張是您使用編程語言來構(gòu)建 API,這有多種好處:

這些好處能夠減少開發(fā)過程中的摩擦,并帶來更加出色的開發(fā)體驗(yàn)。

在本教程中,您將使用 Pothos。它還為 Prisma 提供了一個很棒的插件,可以在 GraphQL 類型和 Prisma 架構(gòu)之間提供良好的開發(fā)體驗(yàn)和類型安全性。

注意:Pothos 能夠與 Prisma 以類型安全的方式協(xié)同工作,且無需依賴插件,但這一過程相對較為手動。

首先,運(yùn)行以下命令來安裝 Pothos 和 Pothos 的 Prisma 插件:

npm install @pothos/plugin-prisma @pothos/core

接下來,將pothos生成器塊添加到生成器正下方的 Prisma 架構(gòu)中client

// prisma/schema.prisma

generator client {
provider = "prisma-client-js"
}

generator pothos {
provider = "prisma-pothos-types"
}

運(yùn)行以下命令重新生成 Prisma Client 和 Pothos 類型:

npx prisma generate

接下來,創(chuàng)建 Pothos 架構(gòu)構(gòu)建器的實(shí)例作為可共享模塊。在該graphql文件夾內(nèi),創(chuàng)建一個名為的新文件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,
}),
}),
});
  1. 定義所需的所有庫和實(shí)用程序
  2. 創(chuàng)建一個新SchemaBuilder實(shí)例
  3. 定義將用于創(chuàng)建 GraphQL 模式的靜態(tài)類型
  4. 在使用 SchemaBuilder 時,可以定義包括將使用的插件、Prisma 客戶端實(shí)例等在內(nèi)的多項(xiàng)選項(xiàng)。
  5. 創(chuàng)建一個queryType帶有名為ok返回布爾值的查詢

接下來,您需要在?/graphql/schema.ts?文件中,用以下從 Pothos 構(gòu)建器創(chuàng)建的 GraphQL 架構(gòu)來替換?typeDefs

// graphql/schema.ts
import { builder } from "./builder";
export const schema = builder.toSchema()

最后,更新文件中的導(dǎo)入/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”文件的更新內(nèi)容:

// /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
}
}

確保服務(wù)器正在運(yùn)行并導(dǎo)航到http://localhost:3000/api/graphql.您將能夠發(fā)送帶有ok字段的查詢,該字段將返回true

詢問

使用 Pothos 定義模式

第一步是Link使用 Pothos 定義對象類型。繼續(xù)創(chuàng)建一個/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?實(shí)例提供了諸如?prismaObject?等實(shí)用方法,用于定義 GraphQL 模式。

prismaObject接受兩個參數(shù):

注意:您可以使用CTRL+Space調(diào)用編輯器的智能感知并查看可用參數(shù)。

fields?屬性用于指定您希望通過“暴露”函數(shù)從 Prisma 架構(gòu)中獲取并包含在 GraphQL 模式中的字段。在本教程中,我們將公開?idtitleurlimageUrl?和?category?這幾個字段。

t.relation方法用于定義您希望從 Prisma 架構(gòu)中公開的關(guān)系字段。

現(xiàn)在創(chuàng)建一個新/graphql/types/User.ts文件并將以下內(nèi)容添加到代碼中以創(chuàng)建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 架構(gòu)時,我們需要將 { nullable: true } 作為第二個參數(shù)進(jìn)行傳遞。

在從生成的架構(gòu)中“公開”字段類型時,role?字段的默認(rèn)類型是其原有類型。但在上面的示例中,您首先定義了一個顯式的枚舉類型?Role,隨后使用它來明確指定?role?字段的類型。

要使架構(gòu)的定義對象類型在 GraphQL 架構(gòu)中可用,請將導(dǎo)入添加到您剛剛在graphql/schema.ts文件中創(chuàng)建的類型:

// graphql/schema.ts
import "./types/Link"
import "./types/User"
import { builder } from "./builder";

export const schema = builder.toSchema()

使用 Pothos 定義查詢

在該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 })
})
)

在上面的代碼片段中:

  1. 定義一個名為 的查詢類型links
  2. 定義將解析為生成的 Prisma 客戶端類型的字段。
  3. 指定 Pothos 將用來解析該字段的字段。在這種情況下,它解析為Link類型的數(shù)組
  4. 定義查詢的邏輯。

query解析器函數(shù)中的參數(shù)將或添加到selectinclude的查詢中,以在單個請求中解析盡可能多的關(guān)系字段。

現(xiàn)在,如果您回到 GraphiQL 界面,就可以發(fā)送一個查詢請求,這個請求會返回?cái)?shù)據(jù)庫中所有的鏈接數(shù)據(jù)。

GraphiQL 上的鏈接查詢響應(yīng)

客戶端 GraphQL 查詢

對于此項(xiàng)目,您將使用 Apollo 客戶端。您可以通過發(fā)送常規(guī)的 HTTP POST 請求來與剛剛構(gòu)建好的 GraphQL API 進(jìn)行通信。不過,使用 GraphQL 客戶端能夠帶來諸多便利和優(yōu)勢。

Apollo 客戶端負(fù)責(zé)請求和緩存您的數(shù)據(jù),以及更新您的 UI。它還包括查詢批處理、查詢重復(fù)數(shù)據(jù)刪除和分頁功能。

在 Next.js 中設(shè)置 Apollo 客戶端

要開始使用 Apollo Client,請通過運(yùn)行以下命令添加到您的項(xiàng)目:

npm install @apollo/client

接下來,在/lib目錄中創(chuàng)建一個名為的新文件apollo.ts,并向其中添加以下代碼:

// /lib/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client'

const apolloClient = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache(),
})

export default apolloClient

您正在創(chuàng)建一個新ApolloClient實(shí)例,并向其中傳遞帶有uricache字段的配置對象。

接下來,轉(zhuǎn)到該/pages/_app.tsx文件并向其中添加以下代碼,以設(shè)置 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組件,以便項(xiàng)目的所有組件都可以發(fā)送 GraphQL 查詢。

注意:Next.js 支持不同的數(shù)據(jù)獲取策略。您可以在服務(wù)器端、客戶端或構(gòu)建時獲取數(shù)據(jù)。為了支持分頁,您需要在客戶端獲取數(shù)據(jù)。

使用發(fā)送請求useQuery

要使用 Apollo 客戶端在前端加載數(shù)據(jù),請更新/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掛鉤將查詢發(fā)送到 GraphQL 端點(diǎn)。這個掛鉤需要一個必需的參數(shù),即 GraphQL 查詢字符串。當(dāng)組件進(jìn)行渲染時,useQuery?會返回一個對象,該對象包含三個值:

保存文件并導(dǎo)航到 后http://loclahost:3000,您將看到從數(shù)據(jù)庫獲取的鏈接列表。

分頁

AllLinksQuery?會返回?cái)?shù)據(jù)庫中的所有鏈接。隨著應(yīng)用程序的不斷擴(kuò)展和鏈接數(shù)量的增加,您可能會收到大量的 API 響應(yīng),這將導(dǎo)致加載時間變長。此外,解析器發(fā)送的數(shù)據(jù)庫查詢也會變慢,因?yàn)槟褂迷?code>prisma.link.findMany()函數(shù)返回?cái)?shù)據(jù)庫中的鏈接。

提升性能的一種常見做法就是引入分頁支持。分頁能夠?qū)⒋笮蛿?shù)據(jù)集切分成更小的數(shù)據(jù)塊,這樣我們就可以根據(jù)需要來請求特定的數(shù)據(jù)塊了。

有多種不同的方法來實(shí)現(xiàn)分頁。您可以進(jìn)行編號頁面,例如 Google 搜索結(jié)果,也可以進(jìn)行無限滾動,如 Twitter 的提要。

無限滾動 GIF https://dribbble.com/artrayd

數(shù)據(jù)庫級別的分頁

現(xiàn)在在數(shù)據(jù)庫級別,您可以使用兩種分頁技術(shù):基于偏移量和基于游標(biāo)的分頁。

基于偏移量的分頁

游標(biāo)通常需要是唯一且連續(xù)的列,比如 ID 或時間戳。與基于偏移量的分頁相比,這種方法更加高效,并且也是本教程中將要采用的方法。

基于光標(biāo)的分頁

GraphQL 中的分頁

為了使 GraphQL API 支持分頁,您需要向 GraphQL 架構(gòu)引入中繼游標(biāo)連接規(guī)范。這是 GraphQL 服務(wù)器應(yīng)如何公開分頁數(shù)據(jù)的規(guī)范。

分頁查詢?nèi)缦滤荆?/p>

query allLinksQuery($first: Int, $after: ID) {
links(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
imageUrl
title
description
url
category
}
}
}
}

該查詢采用兩個參數(shù),first并且after

此查詢返回一個包含兩個字段的對象,pageInfo并且edges

您將實(shí)現(xiàn)單向分頁,在頁面首次加載時請求一些鏈接,然后用戶可以通過單擊按鈕獲取更多鏈接。

或者,您也可以選擇在用戶滾動到頁面底部時自動發(fā)出這個請求。

其工作原理是在頁面首次加載時獲取一些數(shù)據(jù)。然后,單擊按鈕后,您向 API 發(fā)送第二個請求,其中包括您想要返回的項(xiàng)目數(shù)和光標(biāo)。然后將數(shù)據(jù)返回并顯示在客戶端上。

客戶端分頁如何工作

注意:雙向分頁的一個示例是 Slack 等聊天應(yīng)用程序,您可以在其中向前或向后加載消息。

修改 GraphQL 架構(gòu)

Pothos 提供了一個插件,該插件專門用于處理中繼式光標(biāo)分頁,其中包含了節(jié)點(diǎn)、連接以及其他一些實(shí)用的工具。

使用以下命令安裝插件:

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,
}),
}),
});

更新解析器以從數(shù)據(jù)庫返回分頁數(shù)據(jù)

要使用基于游標(biāo)的分頁,請對查詢進(jìn)行以下更新:

// ./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方法用于創(chuàng)建一個connection字段,該字段還預(yù)加載該連接內(nèi)的數(shù)據(jù)。

“Link.ts”文件的更新內(nèi)容:

// /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 })
})
)

下面的圖表總結(jié)了分頁在服務(wù)器上的工作原理:

分頁在服務(wù)器上的工作原理

在客戶端使用分頁fetchMore()

現(xiàn)在API支持分頁,您可以使用Apollo Client在客戶端獲取分頁數(shù)據(jù)。

useQuery?鉤子會返回一個對象,該對象包含?dataloading?和?errors?這幾個屬性。除此之外,useQuery?還會提供一個?fetchMore()?函數(shù),這個函數(shù)用于處理分頁邏輯,并在獲取到新結(jié)果時更新用戶界面。導(dǎo)航到該/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?的鍵,其對應(yīng)的值設(shè)為?2。這意味著您希望初始時獲取兩個鏈接。當(dāng)然,您可以根據(jù)實(shí)際需求將這個值設(shè)置為您想要的任何數(shù)字。

data變量將包含從對 API 的初始請求返回的數(shù)據(jù)。

然后,您將解構(gòu)對象中的值。

如果 hasNextPage 的值為 true,我們將展示一個按鈕,并為其設(shè)置 onClick 事件處理程序。這個處理程序會返回一個函數(shù),該函數(shù)在被調(diào)用時會執(zhí)行 fetchMore() 方法,而 fetchMore() 方法則需要接收一個對象作為參數(shù),該對象包含以下字段:

如果hasNextPagefalse,則意味著沒有更多可以獲取的鏈接。

如果您已經(jīng)保存了更改并且應(yīng)用程序正在運(yùn)行,那么您應(yīng)該能夠成功地從數(shù)據(jù)庫中獲取分頁數(shù)據(jù)。

摘要和后續(xù)步驟

恭喜!您已成功完成課程的第二部分!如果您遇到任何問題或有任何疑問,請隨時聯(lián)系我們的 Slack 社區(qū)。

在這一部分中,您了解了:

在課程的下一部分中,您將:

原文鏈接:https://www.prisma.io/blog/fullstack-nextjs-graphql-prisma-2-fwpc6ds155

上一篇:

使用 Exchangeratesapi.io 自動更新貨幣匯率

下一篇:

2024年最佳的11個酒店API
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實(shí)測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費(fèi)

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費(fèi)