本文使用了 MongoDB, 還沒有集成的可以看一下上篇文章
next13可以參考 trpc 文檔 而且谷歌上已經有不少問題解答,但是目前next14 app只看到一個項目中有用到 Github 倉庫,目前這個倉庫中服務端的上下文獲取存在問題,目前找到一個有用的可以看 Issus。目前trpc對next14 app的支持進度可以看 Issus
好的進入 正文
-
安裝依賴包(這裏我的依賴包版本是
10.43.1)yarn add @trpc/serve @trpc/client @trpc/react-query zod -
創建中間件
context.ts(我的trpc 相關文件的路徑是src/lib/trpc/)。這裏我有用到JWT將用户信息掛載在上下文中import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'; import Jwt from 'jsonwebtoken'; type Opts = Partial<FetchCreateContextFnOptions>; /** * 創建上下文 服務端組件中沒有req resHeaders * @see https://trpc.io/docs/server/adapters/fetch#create-the-context */ export function createContext(opts?: Opts): Opts & { userInfo?: Jwt.JwtPayload; } { const userInfo = {}; return { ...(opts || {}), userInfo }; } export type Context = Awaited<ReturnType<typeof createContext>>; -
創建
trpc.ts文件存放實例/** * @see https://trpc.io/docs/router * @see https://trpc.io/docs/procedures */ import { TRPCError, initTRPC } from '@trpc/server'; import { Context } from './context'; import { parseCookies } from '@/utils/util'; // 格式化 cookie import jwt from 'jsonwebtoken'; // 可以自行放在 utils/util 文件中 // export function parseCookies(cookieString: string) { // const list: { [key: string]: string } = {}; // cookieString && // cookieString.split(';').forEach((cookie) => { // const parts: string[] = cookie.split('='); // if (parts.length) { // list[parts.shift()!.trim()] = decodeURI(parts.join('=')); // } // }); // return list; // } const t = initTRPC.context<Context>().create(); // 鑑權中間件 const authMiddleware = t.middleware(({ ctx, next }) => { const token = parseCookies(ctx.req?.headers.get('cookie') || '').token; const data = jwt.verify(token, process.env.JWT_SECRET!); if (typeof data == 'string') { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { ...ctx, userInfo: data, }, }); }); /** * 需要鑑權的路由 * @see https://trpc.nodejs.cn/docs/server/middlewares#authorization */ export const authProcedure = t.procedure.use( authMiddleware.unstable_pipe(({ ctx, next }) => { return next({ ctx, }); }) ); /** * Unprotected procedure **/ export const publicProcedure = t.procedure; export const router = t.router; // 創建服務端調用在示例倉庫中使用的是 createCaller, 但是 createCaller 在 trpc v11 中已經廢棄 // @see https://trpc.io/docs/server/server-side-calls#create-caller export const createCallerFactory = t.createCallerFactory; - 創建
trpc路由auth-router.tspoints-router.tsrouters.ts
特別注意,在服務端組件中請求時沒有 ctx
// auth-router.ts
// auth-router.ts
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
import { TRPCError } from '@trpc/server';
// 數據庫設置 db.ts 放在 lib/db.ts
// db.ts 內容查看連接 https://juejin.cn/post/7341669201008918565 正題中第 2 點
import clientPromise from '../db';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
export const authRouter = router({
signIn: publicProcedure
.input(
z.object({
name: z.string(),
pwd: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
const { resHeaders } = ctx;
const { name, pwd } = input;
const client = await clientPromise;
const collection = client.db('test').collection('users');
try {
const user = await collection.findOne({
name: name,
});
if (user) {
// 判斷是否有用户存在, 存在直接登錄
const isValid = await bcrypt.compare(pwd, user.password);
if (!isValid) {
// 返回 401
return new TRPCError({
code: 'FORBIDDEN',
message: 'Invalid credentials',
});
}
const token = jwt.sign(
{ userId: user._id, name: user.name },
process.env.JWT_SECRET!, // 這裏需要再環境變量中定義
{ expiresIn: '12h' }
);
// 設置cookie
resHeaders?.set('Set-Cookie', 'token=' + token);
return {
code: 200,
data: token,
success: true,
};
}
// 註冊邏輯
// 加密用户密碼
const hashedPassword = await bcrypt.hash(pwd, 12);
// 存儲用户
const result = await collection.insertOne({
name: name,
points: 0, // 積分
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
});
const token = jwt.sign(
{ userId: result.insertedId, name: name },
process.env.JWT_SECRET!,
{ expiresIn: '12h' }
);
resHeaders?.set('Set-Cookie', 'token=' + token);
return {
code: 200,
data: token,
success: true,
};
} catch (error: any) {
// console.log(error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: error.message,
});
}
}),
login: publicProcedure
.input(
z.object({
name: z.string(),
pwd: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
const { resHeaders } = ctx;
const client = await clientPromise;
const collection = client.db('test').collection('users');
const { name, pwd } = input;
const user = await collection.findOne({
name: name,
});
// 比較恢復的地址和預期的地址
try {
if (user) {
const isValid = await bcrypt.compare(pwd, user.password);
if (!isValid) {
return new TRPCError({
code: 'FORBIDDEN',
message: 'Invalid credentials',
});
}
const token = jwt.sign(
{ userId: user._id, name: name },
process.env.JWT_SECRET!,
{ expiresIn: '12h' }
);
resHeaders?.set('Set-Cookie', 'token=' + token);
return {
code: 200,
data: token,
success: true,
};
} else {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'User information not found',
});
}
} catch (error: any) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: error.message,
});
}
}),
// 測試服務端的 trpc 請求
hello: publicProcedure
.input(
z.object({
name: z.string(),
})
)
.mutation(async ({ input, ctx }) => {
// ctx 是沒有數據的
return input.name;
}),
});
// points-router.ts
import { z } from 'zod';
import { authProcedure, router } from './trpc';
import { TRPCError } from '@trpc/server';
import clientPromise from '../db';
import { ObjectId } from 'mongodb';
export const PointsRouter = router({
// 這裏使用的是 authProcedure 中間件路由,需要有攜帶 token 且鑑權通過才會進入路由,否則返回 401
added: authProcedure
.input(
z.object({
count: z.number(),
})
)
.mutation(async ({ input, ctx }) => {
const { userInfo } = ctx;
const client = await clientPromise;
const collection = client.db('test').collection('points-records');
const userCollection = client.db('test').collection('users');
try {
// 查詢數據
const result = await userCollection.findOne({
_id: new ObjectId(userInfo.userId),
});
// 添加積分記錄數據
await collection.insertOne({
userId: userInfo.userId,
count: input.count,
points: (result?.points || 0) + input.count!,
operateType: (input.count || 0) >= 0 ? 'added' : 'reduce',
createdAt: new Date(),
updatedAt: new Date(),
});
// 修改用户積分數據
await userCollection.updateOne(
{ _id: new ObjectId(userInfo.userId) },
{
$set: {
points: (result?.points || 0) + input.count!,
updatedAt: new Date(),
},
}
);
return {
code: 200,
data: {},
success: true,
};
} catch (error: any) {
console.log(error.message);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: error.message,
});
}
}),
});
// routers.ts
import { router } from './trpc';
import { authRouter } from './auth-router';
import { PointsRouter } from './points-router';
export const appRouter = router({
authRouter,
PointsRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
至此路由文件已經定義完成。
-
創建客户端
trpc請求,client.tsimport { createTRPCReact } from '@trpc/react-query'; import { type AppRouter } from './routers'; export const trpc = createTRPCReact<AppRouter>({}); -
創建
trpc上下文組件'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { httpBatchLink } from '@trpc/client'; import React, { useState } from 'react'; import { trpc } from '@/lib/trpc/client'; function getBaseUrl() { if (typeof window !== 'undefined') { // In the browser, we return a relative URL return ''; } // When rendering on the server, we return an absolute URL // reference for vercel.com if (process.env.VERCEL_URL) { return `https://${process.env.VERCEL_URL}`; } // assume localhost return `http://localhost:${process.env.PORT ?? 3000}`; } export function TrpcProviders({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient({})); const [trpcClient] = useState(() => trpc.createClient({ links: [ httpBatchLink({ url: getBaseUrl() + '/api/trpc', }), ], }) ); return ( <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> </trpc.Provider> ); }在
layout.tsx文件中引入// layout.tsx import { TrpcProviders } from 'xxx' export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <TrpcProviders>{children}</TrpcProviders> </body> </html> ); } -
在
page.tsx文件中調用trpc路由請求'use client'; import { useEffect } from 'react'; import { trpc } from '@/lib/trpc/client'; export function PageHome() { const { mutate } = trpc.authRouter.signIn.useMutation(); useEffect(() => { mutate({ name: 'pxs', pwd: 'pxs', }) }, []); return (<div>1111</div>) }服務端組件使用
trpc路由請求 -
定義服務端請求,創建
serverClient.tsimport { appRouter } from './routers'; import { createCallerFactory } from './trpc'; const createCaller = createCallerFactory(appRouter); // 這裏目前博主拿不到 req 和可寫的 resHeaders export const serverClient = createCaller({}); -
服務端組件調用
import { serverClient } from '@/lib/trpc/serverClient'; export default async function ServerPage() { const res = await serverClient.authRouter.hello({ name: 'pxs' }); return <div>{JSON.stringify(res)}</div>; }
至此 next14 app 使用 trpc 已完成。
示例倉庫:Github