استفاده از 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 من یافت.