本文使用了 MongoDB, 还没有集成的可以看一下上篇文章
next13
可以参考 trpc 文档 而且谷歌上已经有不少问题解答,但是目前next14 app
只看到一个项目中有用到 Github 仓库,目前这个仓库中服务端的上下文获取存在问题,目前找到一个有用的可以看 Issus。目前trpc
对next14 app
的支持进度可以看 Issus
好的进入 正文
- 安装依赖包(这里我的依赖包版本是
10.43.1
)
base
yarn add @trpc/serve @trpc/client @trpc/react-query zod
- 创建中间件
context.ts
(我的trpc 相关文件的路径是src/lib/trpc/
)。这里我有用到JWT
将用户信息挂载在上下文中
ts
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
文件存放实例
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.ts
points-router.ts
routers.ts
特别注意,在服务端组件中请求时没有 ctx
ts
// 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;
}),
});
ts
// 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,
});
}
}),
});
ts
// 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.ts
ts
import { createTRPCReact } from '@trpc/react-query';
import { type AppRouter } from './routers';
export const trpc = createTRPCReact<AppRouter>({});
- 创建
trpc
上下文组件
tsx
'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
文件中引入
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
路由请求
tsx
'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.ts
ts
import { appRouter } from './routers';
import { createCallerFactory } from './trpc';
const createCaller = createCallerFactory(appRouter);
// 这里目前博主拿不到 req 和可写的 resHeaders
export const serverClient = createCaller({});
- 服务端组件调用
tsx
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 已完成。
示例仓库:暂无(后续补上)