本文应用了 MongoDB, 还没有集成的能够看一下上篇文章
- Next14 app +Vercel 集成 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'; // 格式化 cookieimport 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-callerexport const createCallerFactory = t.createCallerFactory;
- 创立
trpc
路由auth-router.ts
points-router.ts
routers.ts
特地留神,在服务端组件中申请时没有 ctx
// auth-router.ts// auth-router.tsimport { 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.tsimport { 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.tsimport { router } from './trpc';import { authRouter } from './auth-router';import { PointsRouter } from './points-router';export const appRouter = router({ authRouter, PointsRouter,});// export type definition of APIexport type AppRouter = typeof appRouter;
至此路由文件曾经定义实现。
创立客户端
trpc
申请,client.ts
import { 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.tsximport { 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.ts
import { appRouter } from './routers';import { createCallerFactory } from './trpc';const createCaller = createCallerFactory(appRouter);// 这里目前博主拿不到 req 和可写的 resHeadersexport 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 已实现。
分割:1612565136@qq.com
示例仓库:暂无(后续补上)