برنامه نویسی

استفاده از TRPC در Astro و جزایر آن (React).

من استفاده از TRPC را در برخی از پروژه‌های Next.js در محل کارم شروع کردم، و واقعاً از ایمنی سرتاسری که به عنوان یک توسعه‌دهنده هنگام کار با APIها به شما می‌دهد، خوشم آمد.
بنابراین تصمیم گرفتم TRPC را در وب سایت خودم که از Astro استفاده می کند، پیاده سازی کنم.
برای شروع استفاده از TRPC در Astro باید مراحلی را طی کرد.

نصب پکیج های مورد نیاز

npm install @tanstack/react-query @trpc/client @trpc/server @trpc/react-query
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیم زمینه TRPC

// /src/server/context.ts

import { getUser } from '@astro-auth/core';
import type { inferAsyncReturnType } from '@trpc/server';
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';

export function createContext({
  req,
  resHeaders,
}: FetchCreateContextFnOptions) {
  const user = getUser({ server: req });
  return { req, resHeaders, user };
}

export type Context = inferAsyncReturnType<typeof createContext>;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که می‌خواهم وقتی یک مسیر TRPC فراخوانی می‌شود، کاربر وارد شده فعلی را بررسی کنم، آن را اضافه می‌کنم getUser() تماس از @astro-auth. با افزودن این مورد به زمینه، می توانم بعداً از کاربر در میان افزار خود استفاده کنم (به زیر مراجعه کنید).

راه اندازی سرور TRPC

// src/server/router.ts

import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
import { prisma } from '../lib/prisma';
import type { Comment } from '@prisma/client';
import type { Context } from './context';

export const t = initTRPC.context<Context>().create();

export const middleware = t.middleware;
export const publicProcedure = t.procedure;

const isAdmin = middleware(async ({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      user: ctx.user,
    },
  });
});

export const adminProcedure = publicProcedure.use(isAdmin);

export const appRouter = t.router({
  getCommentsForBlog: publicProcedure
    .input(z.string())
    .query(async ({ input }) => {
      const blogUrl = input.replace('src/content', '').replace('.mdx', '');
      const commentsForBlogUrl = await prisma?.post.findFirst({
        where: { url: (blogUrl as string) ?? undefined },
        include: { Comment: { orderBy: { createdAt: 'desc' } } },
      });
      const allCommentsInDbForPost = commentsForBlogUrl?.Comment;
      return allCommentsInDbForPost ?? null;
    }),
  createCommentForBlog: publicProcedure
    .input(
      z.object({
        comment: z.string(),
        author: z.string(),
        blogUrl: z.string(),
      })
    )
    .mutation(async ({ input }) => {
      const { comment, blogUrl, author } = input;
      let commentInDb: Comment | undefined;
      const blog = await prisma?.post.findFirst({
        where: { url: blogUrl },
      });
      try {
        commentInDb = await prisma?.comment.create({
          data: {
            author: author ?? '',
            text: comment ?? '',
            post: {
              connectOrCreate: {
                create: {
                  url: blogUrl ?? '',
                },
                where: {
                  id: blog?.id ?? 0,
                },
              },
            },
          },
        });
      } catch (err) {
        console.error('Error saving comment', err);
        return { status: 'error', error: 'Error saving comment' };
      }
      if (!commentInDb) {
        return { status: 'error', error: 'Error saving comment' };
      }
      return { status: 'success' };
    }),
  deleteCommentForBlog: adminProcedure
    .input(z.object({ id: z.number() }))
    .mutation(async ({ input }) => {
      let deleteComment;
      try {
        deleteComment = await prisma?.comment.delete({
          where: {
            id: input.id,
          },
        });
      } catch (e) {
        return { status: 'error', error: 'Error deleting comment' };
      }
      if (!deleteComment) {
        return { status: 'error', error: 'Error deleting comment' };
      }

      return { status: 'success' };
    }),
  sendContactForm: publicProcedure
    .input(
      z.object({ email: z.string().nullable(), message: z.string().nullable() })
    )
    .mutation(async ({ input }) => {
      if (input.email && input.message) {
        await fetch(import.meta.env.FORMSPREE_URL!, {
          method: 'post',
          headers: {
            Accept: 'application/json',
          },
          body: JSON.stringify(input),
        }).catch(e => {
          console.error(e);
          return { status: 'error' };
        });
        return { status: 'success' };
      }
      return { status: 'missingdata' };
    }),
});

export type AppRouter = typeof appRouter;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من یک رویه جداگانه ایجاد کردم adminProcedure، که میان افزار را روی آن اعمال کردم. با این کار مطمئن خواهید شد که هر مسیری در این رویه فقط در صورتی قابل فراخوانی است که کاربر وارد شده باشد.
پس از ایجاد رویه ها، مسیرهای مختلف را اعلام می کنم. بررسی کنید deleteCommentForBlog مسیر که مسیر پشت adminProcedure.

راه اندازی API Route در Astro

// /src/pages/api/trpc/[trpc].ts

import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import type { APIRoute } from 'astro';
import { createContext } from '../../../server/context';
import { appRouter } from '../../../server/router';

export const all: APIRoute = ({ request }) => {
  return fetchRequestHandler({
    endpoint: '/api/trpc',
    req: request,
    router: appRouter,
    createContext,
  });
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من از آداپتور fetch برای رسیدگی به درخواست‌های سمت کلاینت به روتر TRPC استفاده خواهم کرد.
این امکان پذیر است زیرا Astro از API های بستر وب داخلی استفاده می کند Response & Request.
ما روتر و زمینه را به هم پیوند می دهیم و آماده راه اندازی کلاینت TRPC هستیم.

راه اندازی مشتری TRPC

زیرا من می خواهم از TRPC هم در اسکریپت های سمت مشتری در صفحات Astro و هم در جزیره ها استفاده کنم.
جزایر من با استفاده از React ایجاد می‌شوند تا کلاینت TRPC React را نیز راه‌اندازی کنم.

// /src/client/index.ts

import { createTRPCReact } from '@trpc/react-query';
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';

const trpcReact = createTRPCReact<AppRouter>();

const trpcAstro = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/api/trpc',
    }),
  ],
});

export { trpcReact, trpcAstro };
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

استفاده از کلاینت TRPC در فایل های astro

من از کلاینت Astro TRPC برای برقراری ارتباط از طریق خود استفاده خواهم کرد <script> برچسب روی مشتری به مسیرهای TRPC.

// /src/pages/context/index.astro export const prerender = true; import Layout
from '../../layouts/Layout.astro';

<form id="contactForm">
  <label class="flex flex-col gap-2 mb-4" for="email">
    Your e-mail
    <input
      class="py-2 px-4 bg-white border-secondary border-2 rounded-lg"
      id="email"
      type="email"
      name="email"
      placeholder="info@example.com"
      required
    />
  </label>
  <label class="flex flex-col gap-2" for="message">
    Your message
    <textarea
      class="py-2 px-4 bg-white border-secondary border-2 rounded-lg"
      rows={3}
      id="message"
      name="message"
      placeholder="Hey, I would like to get in touch with you"
      required></textarea>
  </label>

  <button
    class="px-8 mt-4 py-4 bg-secondary text-white rounded-lg lg:hover:scale-[1.04] transition-transform disabled:opacity-50"
    type="submit"
    id="submitBtn"
  >
    Submit
  </button>
  <div id="missingData" class="text-red-500 font-bold hidden">
    Something went from while processing the contact form. Try again later.
  </div>
  <div id="error" class="text-red-500 font-bold hidden">
    Something went from while processing the contact form. Try again later.
  </div>
</form>
<script>
  import { trpcAstro } from '../../client';
  const form = document.getElementById('contactForm') as HTMLFormElement | null;
  form?.addEventListener('submit', async e => {
    e.preventDefault();
    const formData = new FormData(form);
    const result = await trpcAstro.sendContactForm.mutate({
      message: formData.get('message') as string | null,
      email: formData.get('email') as string | null,
    });
    if (result.status === 'success') {
      window.location.href="https://dev.to/contact/thanks";
    }
  });
</script>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

از آنجایی که من از مشتری TRPC استفاده می کنم، تکمیل خودکار روی کد دریافت می کنم و دقیقاً می دانم چه چیزی به عنوان ورودی مسیر مورد انتظار است و چه چیزی برگردانده می شود!

استفاده از کلاینت TRPC در جزایر React

تصمیم گرفتم باهاش ​​کار کنم @tanstack/react-query برای تسهیل واکشی/جهش آسانتر در کد React من.
به همین دلیل، من نیاز داشتم که کلاینت TRPC را نمونه سازی کنم و آ QueryClient برای react-query.
این کار را در یک کامپوننت wrapper انجام دادم، که مؤلفه واقعی را که تماس‌های مسیرهای TRPC را انجام می‌دهد، می‌پیچد.

// /src/components/CommentOverviewWrapper.tsx

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { CommentOverview } from './CommentOverview';
import { trpcReact } from '../client';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';

const CommentsOverviewWrapper = () => {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpcReact.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
        }),
      ],
    })
  );
  return (
    <trpcReact.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <CommentOverview />
      </QueryClientProvider>
    </trpcReact.Provider>
  );
};
export default CommentsOverviewWrapper;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپوننت واقعی به شکل زیر در آمد:

// /src/components/CommentOverview.tsx

import type { Comment } from '@prisma/client';
import { trpcReact } from '../client';

const CommentOverview = () => {
  const upToDateCommentsQuery = trpcReact.getAllComments.useQuery();
  const { mutate: deleteComment } = trpcReact.deleteCommentForBlog.useMutation({
    onError: () => {
      console.error('Error deleting comment');
    },
    onSuccess: res => {
      if (res.status === 'error') {
        console.log('Succesfully deleted comment');
      }
    },
    onSettled: () => {
      upToDateCommentsQuery.refetch();
    },
  });

  const commentsReduced = upToDateCommentsQuery?.data?.reduce<{
    [key: string]: typeof upToDateCommentsQuery.data;
  }>(
    (acc, cur) => ({
      ...acc,
      [cur.post.url]: [...(acc[cur.post.url] || []), cur],
    }),
    {}
  );
  return (
    <div className="grid lg:grid-cols-2 gap-6">
      {commentsReduced
        ? Object.entries(commentsReduced).map(([key, val]) => {
            return (
              <div key={key}>
                <h2 className="font-bold mb-4 text-xl">{key}</h2>
                <ul className="flex flex-col gap-y-2">
                  {val.map(comment => (
                    <div className="flex gap-x-2" key={comment.id}>
                      <button
                        type="button"
                        onClick={() => {
                          deleteComment({ id: comment.id });
                        }}
                      >
                        <svg
                          xmlns="http://www.w3.org/2000/svg"
                          fill="none"
                          viewBox="0 0 24 24"
                          strokeWidth={1.5}
                          stroke="currentColor"
                          className="min-w-[1.5rem] h-6 text-red-600"
                        >
                          <path
                            strokeLinecap="round"
                            strokeLinejoin="round"
                            d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                          />
                        </svg>
                      </button>
                      <li>
                        <span className="font-bold">{comment.author}</span> :{' '}
                        {comment.text}
                      </li>
                    </div>
                  ))}
                </ul>
              </div>
            );
          })
        : null}
    </div>
  );
};

export default CommentOverview;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین واکشی همه نظرات به آسانی اضافه کردن است const upToDateCommentQuery = trpcReact.getAllComments.useQuery().
حذف نظر با افزودن انجام می شود const { mutate: deleteComment } = trpcReact.deleteCommentForBlog.useMutation و سپس در من <button>تماس گیرنده کلیک deleteComment({ id: comment.id });.

امیدوارم این مفید بوده باشد!
کد را می توان در GitHub من یافت.

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

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

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

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