Next.js و GraphQL: ترکیبی عالی برای توسعه Full Stack

آنچه خواهید آموخت
در مقاله امروز ما قصد داریم با استفاده از Next.js با GraphQL Yoga یک برنامه کامل پشته ایجاد کنیم.
این مقاله چه چیزی را پوشش می دهد
- روتر و اقدامات برنامه Next.js
- ادغام یوگا GraphQL
- عملیاتی مانند Get, Create و Delete را در پایگاه داده انجام دهید
پیش نیازها
قبل از شروع مقاله، توصیه می شود که از 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"],
},
};
با آماده شدن تنظیمات برنامه، می توانیم به مرحله بعدی برویم.
راه اندازی Backend
اول از همه، ما باید اتصال به پایگاه داده خود را پیکربندی کنیم تا بتوانیم داده ها را در برنامه خود نگه داریم. برای سهولت در کل فرآیند، قصد داریم از یک ORM استفاده کنیم که برای مقاله تصمیم گرفتم ORM Drizzle را انتخاب کنم. و برای پایگاه داده، تصمیم گرفتم از 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
گام بعدی بدون شک ایجاد طرحواره GraphQL ما با استفاده از Garph برای ایجاد یک API کاملاً ایمن بدون نیاز به کدژن است.
طرحواره برنامه ما فقط یک Query خواهد داشت که مسئول بازگرداندن همه خواهد بود todos
که در پایگاه داده هستند. و همچنین دو جهش خواهیم داشت، یکی برای درج a 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
که حاوی منطق Query و هر یک از جهش های 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 ما را با در نظر گرفتن طرح و حلکنندههای ایجاد شده در حال حاضر ارائه میکند.
با تمام شدن Backend می توانیم به نقطه بعدی برویم.
اجزای قابل استفاده مجدد
قبل از شروع کار بر روی صفحات، اجازه دهید با کار بر روی برخی از مؤلفه هایی که در آنها استفاده می شود شروع کنیم. این مؤلفهها به فهرست و عناصری که در آن رندر میشوند (ردیفها) و همچنین برخی اقدامات مربوط میشوند.
با نصب وابستگی های زیر شروع کنید:
npm install graphql-request zod
بعد، بیایید ایجاد کنیم <ListItem />
مؤلفه ای که با هر یک از ردیف های موجود در لیستی که در ریشه برنامه ما ارائه می شود مطابقت دارد. این کامپوننت برخی از لوازم جانبی مانند 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
boundary تا بتوانیم از جریان 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
که در داخل خواهد بود app/new/
پوشه در page.tsx
فایل. در این کامپوننت قصد داریم یک schema zod برای اعتبارسنجی داده های فرم ایجاد کنیم و در داخل این صفحه تابعی به نام خواهیم داشت 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 لینک زیر پیدا کنید.
Github Repo