我开始在某些工作中使用TRPC。
因此,我决定还在使用Astro的网站上实现TRPC。
在Astro中开始使用TRPC,需要采取一些步骤。
安装所需软件包
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路由时检查当前登录的用户,所以我添加了@astro-auth
的getUser()
调用。通过将其添加到上下文中,我可以在以后的中间件中使用用户(请参见下文)。
设置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
后面的路线。
在Astro设置API路线
// /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 adapter处理从客户端到TRPC路由器的请求。
这是可能的,因为Astro使用内置的Web Platform Apis Response
&Request
。
我们链接路由器和上下文,我们准备设置TRPC客户端。
设置TRPC客户端
因为我想在Astro页面以及islands上使用trpc。
我的岛屿是使用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 };
在.ASTRO文件中使用TRPC客户端
我将使用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 = '/contact/thanks';
}
});
</script>
因为我正在使用TRPC客户端,所以我在代码上获得自动完成,并且我确切地知道该路线的输入和将返回的内容的预期!
在React Islands中使用TRPC客户端
我决定与@tanstack/react-query
合作,以促进我的React代码中的更轻松的获取/突变。
因此,我需要实例化trpc客户端和 QueryClient
for react-query
。
我在包装器组件中进行了此操作,该组件包装了将打电话给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>
的点击处理程序中完成的。
希望这很有帮助!
代码可以在my GitHub上找到。