Next.js 和 GraphQL:全栈开发的完美组合
您将学到什么
在今天的文章中,我们将使用 Next.js 和 GraphQL Yoga 创建一个全栈应用程序。
本文涵盖了哪些内容
- Next.js 应用路由器和操作
- GraphQL Yoga 集成
- 对数据库执行获取、创建和删除等操作
先决条件
在开始本文之前,建议您了解 React、Next.js 和 GraphQL。
创建项目
要在 Remix 中初始化项目,我们执行以下命令:
npx create-next-app@latest my-app
所使用的设置包括 TypeScript、ESLint、Tailwind CSS,并且我们正在使用app
路由器。
我们使用以下命令启动开发服务器:
npm run dev
除了基本配置之外,我们还使用daisyUI库来使用预先设计的组件。
npm install daisyui
然后我们将库添加到tailwind.config.js
文件中的插件列表中,我们还可以定义我们想要使用的主题,如下所示:
module.exports = {
// ...
plugins: [require("daisyui")],
daisyui: {
themes: ["winter"],
},
};
应用程序设置完成后,我们可以继续下一步。
后端设置
首先,我们需要配置与数据库的连接,以便将数据持久化到应用程序中。为了简化整个过程,我们将使用 ORM,在本文中我选择了Drizzle ORM。至于数据库,我选择使用 SQLite,因为它最易于访问。
我们首先安装依赖项:
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
然后在server/
文件夹内我们将创建一个名为的文件夹db/
,其中包含连接和数据库模式。
从配置开始,让我们server/db/config.ts
使用以下内容创建文件:
import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import Database from "better-sqlite3";
const sqlite = new Database("sqlite.db");
export const db = drizzle(sqlite);
migrate(db, { migrationsFolder: "./server/db/migrations" });
下一步是创建数据库模式,在今天的文章中,我们将在数据库中只有一个表,其中todos
包含三列,如下所示:
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const todos = sqliteTable("todos", {
id: integer("id").primaryKey(),
title: text("username").notNull(),
createdAt: integer("createdAt").notNull(),
});
上述代码位于server/db/schema.ts
文件中,该文件将被考虑用于创建数据库迁移和实体。现在package.json
让我们添加以下脚本:
{
// ...
"scripts": {
// ...
"db:migrations": "drizzle-kit generate:sqlite --out ./server/db/migrations --schema ./server/db/schema.ts"
},
// ...
}
从上面的脚本中,我们可以运行以下命令来创建迁移,同时考虑到已创建的模式:
npm run db:migrations
一旦完成,预计migrations/
将在文件夹内创建文件夹server/db/
。
一旦我们准备好数据层,我们就可以开始处理我们的 GraphQL 层,首先安装以下依赖项:
# graphql related dependencies
npm install garph graphql-yoga graphql
# JS Dates
npm install dayjs
下一步无疑是使用Garph创建我们的 GraphQL Schema ,以创建一个完全类型安全的 API,而无需进行代码生成。
我们的应用程序架构将只有一个查询 (Query),它将负责返回todos
数据库中的所有内容。此外,我们还将有两个变更 (Mutations),一个用于插入一个todo
,另一个用于删除一个现有的todo
。
为此,让我们创建一个名为的文件夹,gql/
其中server/
将包含与我们的模式相关的所有内容,可能类似于以下内容:
import { GarphSchema } from "garph";
export const g = new GarphSchema();
export const TodoGQL = g.type("Todo", {
id: g.int(),
title: g.string(),
createdAt: g.int(),
});
export const queryType = g.type("Query", {
getTodos: g.ref(TodoGQL).list().description("Gets an array of todos"),
});
export const mutationType = g.type("Mutation", {
addTodo: g
.ref(TodoGQL)
.args({
title: g.string(),
})
.description("Adds a new todo"),
removeTodo: g
.ref(TodoGQL)
.optional()
.args({
id: g.int(),
})
.description("Removes an existing todo"),
});
有了文件中创建的模式schema.ts
,我们现在可以创建resolvers.ts
包含查询逻辑和 API 的每个变更的 . 代码如下:
import { InferResolvers } from "garph";
import { YogaInitialContext } from "graphql-yoga";
import { eq } from "drizzle-orm";
import dayjs from "dayjs";
import { mutationType, queryType } from "./schema";
import { db } from "../db/config";
import { todos } from "../db/schema";
type Resolvers = InferResolvers<
{ Query: typeof queryType; Mutation: typeof mutationType },
{ context: YogaInitialContext }
>;
export const resolvers: Resolvers = {
Query: {
getTodos: (_, __, ctx) => {
return db.select().from(todos).all();
},
},
Mutation: {
addTodo: (_, { title }, ctx) => {
return db
.insert(todos)
.values({
title,
createdAt: dayjs().unix(),
})
.returning()
.get();
},
removeTodo: (_, { id }, ctx) => {
return db.delete(todos).where(eq(todos.id, id)).returning().get();
},
},
};
创建模式和解析器后,我们现在需要创建index.ts
构建 GraphQL 模式的文件,以便 GraphQL Yoga 可以使用它。
import { buildSchema } from "garph";
import { resolvers } from "./resolvers";
import { g } from "./schema";
export const schema = buildSchema({ g, resolvers });
这样,一切就绪,我们现在可以跳转到router app
创建以下文件夹结构的位置,app/api/graphql/
其中route.ts
包含包含以下内容的文件:
import { createYoga } from "graphql-yoga";
import { schema } from "../../../server/gql";
const { handleRequest } = createYoga({
schema,
graphqlEndpoint: '/api/graphql',
fetchAPI: { Request, Response }
});
export { handleRequest as GET, handleRequest as POST }
在上面的代码块中,我们创建了一个自定义路由处理程序,它将创建一个GraphQL Yoga实例,该实例将根据刚刚创建的模式和解析器提供我们的 API。
后端完成后,我们可以继续下一步。
可重用组件
在开始页面设计之前,我们先来了解一下页面中需要用到的一些组件。这些组件与列表、列表内渲染的元素(行)以及一些操作相关。
首先安装以下依赖项:
npm install graphql-request zod
接下来,让我们创建一个<ListItem />
组件,该组件将与列表中的每一行相对应,并将在应用的根目录中渲染。该组件将接收一些 props,例如todoId
,title
以及一个removeItem()
函数。为了实现交互,该组件将在客户端渲染。
"use client";
import { LiHTMLAttributes } from "react";
interface Props extends LiHTMLAttributes<HTMLLIElement> {
todoId: number;
title: string;
removeItem: (id: number) => void;
}
export default function ListItem({ todoId, title, removeItem, ...rest }: Props) {
return (
<li
className="card w-96 bg-base-100 shadow-xl cursor-pointer"
{...rest}
onClick={() => removeItem(todoId)}
>
<div className="card-body">
<p>{title}</p>
</div>
</li>
);
}
然后我们可以创建一些将在应用程序列表中使用的操作,我们只需要确保这些函数在调用时始终在服务器端运行。
"use server";
import { revalidatePath } from "next/cache";
import { GraphQLClient, gql } from "graphql-request";
const mutation = gql`
mutation removeTodo($id: Int!) {
removeTodo(id: $id) {
id
}
}
`;
export async function removeTodo(id: number) {
const graphQLClient = new GraphQLClient("http://localhost:3000/api/graphql");
await graphQLClient.request(mutation, { id });
revalidatePath("/");
}
上述代码是在app/components/
文件夹内创建的,更具体地说,是在list/
包含文件中函数的文件夹中创建的actions.ts
,并且文件中将包含组件代码index.tsx
。该组件仍需创建,并将在服务器端渲染。
import { Infer } from "garph";
import { request, gql } from "graphql-request";
import { TodoGQL } from "../../server/gql/schema";
import ListItem from "../ListItem";
import { removeTodo } from "./actions";
const query = gql`
query getTodos {
getTodos {
id
title
}
}
`;
interface QueryData {
getTodos: Array<Infer<typeof TodoGQL>>;
}
export default async function List() {
const { getTodos } = await request<QueryData>(
"http://localhost:3000/api/graphql",
query
);
return (
<ul className="space-y-4">
{getTodos?.map((todo) => {
return (
<ListItem
key={todo.id}
title={todo.title}
todoId={todo.id}
removeItem={removeTodo}
/>
);
})}
</ul>
);
}
创建可重用组件后,我们现在可以进入下一步。
路线设置
现在我们已经准备好了所有需要用到的东西,可以开始定义应用程序的路由了。应用程序中将包含的路由如下:
app/page.tsx
- 应用程序的主路由,我们将在其中列出所有应用的列表,并可以与它们进行交互以删除它们app/new/page.tsx
- 表单将出现在哪里以及将验证提交的数据并进行相应修改的操作
现在考虑到这一点,我们可以转到layout.tsx
文件并进行以下更改:
import "./globals.css";
export const metadata = {
title: "Today's tasks",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="h-screen w-screen bg-neutral">
<section className="container mx-auto p-4">{children}</section>
</body>
</html>
);
}
接下来,在page.tsx
文件中,我们将添加一个Suspense
边界,以便我们可以利用 html 流,并且当列表解析异步请求并呈现 html 时,我们将显示一个回退。
import React, { Suspense } from "react";
import Link from "next/link";
import dayjs from "dayjs";
import List from "../components/List";
export default function Page() {
return (
<div className="space-y-6">
<div className="flex flex-row items-start justify-between max-w-xl">
<span className="space-y-2">
<h1 className="text-3xl text-primary-content">Today's tasks</h1>
<p className="text-lg">{dayjs().format("dddd, D MMM")}</p>
</span>
<Link className="btn" href="/new">
New Task
</Link>
</div>
<Suspense fallback={<span className="loading loading-ring loading-lg" />}>
<List />
</Suspense>
</div>
);
}
最后,同样重要的是,我们需要创建一个页面,用于在文件夹todo
中插入新的表单数据。在这个组件中,我们将创建一个zod模式来验证表单数据,并在此页面中添加一个名为 的函数,该函数应仅在服务器端运行。app/new/
page.tsx
addTodo()
import Link from "next/link";
import { redirect } from "next/navigation";
import { GraphQLClient, gql } from "graphql-request";
import { z } from "zod";
const mutation = gql`
mutation addTodo($title: String!) {
addTodo(title: $title) {
id
}
}
`;
const formValuesSchema = z.object({
title: z.string().min(3),
});
async function addTodo(formData: FormData) {
"use server";
const formValues = {} as any;
for (const [key, value] of [...formData.entries()]) {
if (key.includes("ACTION_ID")) continue;
formValues[key] = value.valueOf();
}
const parsed = await formValuesSchema.parseAsync(formValues);
const graphQLClient = new GraphQLClient("http://localhost:3000/api/graphql");
await graphQLClient.request(mutation, parsed);
redirect("/");
}
export default function Page() {
return (
<div className="max-w-xs space-y-6">
<Link href=".." className="btn btn-ghost">
Go back
</Link>
<form action={addTodo} className="space-y-4">
<div>
<label className="label">
<span className="label-text">Task title</span>
</label>
<input
type="text"
name="title"
placeholder="Type here..."
className="input input-bordered w-full max-w-xs"
required
minLength={3}
/>
</div>
<button className="btn btn-block" type="submit">
Submit
</button>
</form>
</div>
);
}
至此,我结束了本文的最后一步。
结论
我希望您发现这篇文章很有帮助,无论您是在现有项目中使用这些信息还是只是为了好玩而尝试一下。
如果您发现本文有任何错误,请通过评论告诉我。如果您想查看本文的源代码,可以在下方链接的 GitHub 仓库中找到。
鏂囩珷鏉ユ簮锛�https://dev.to/franciscomendes10866/nextjs-and-graphql-the-perfect-combination-for-full-stack-development-18l7