برنامه نویسی

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&apos;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

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا