本文应用了 MongoDB, 还没有集成的能够看一下上篇文章

  • Next14 app +Vercel 集成 MongoDB
next13 能够参考 trpc 文档 而且谷歌上曾经有不少问题解答,然而目前 next14 app 只看到一个我的项目中有用到 Github 仓库,目前这个仓库中服务端的上下文获取存在问题,目前找到一个有用的能够看 Issus。目前 trpcnext14 app 的反对进度能够看 Issus

好的进入 注释

  1. 装置依赖包(这里我的依赖包版本是 10.43.1

    yarn add @trpc/serve @trpc/client @trpc/react-query zod
  2. 创立中间件 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>>;
  3. 创立 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;
  4. 创立 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;

至此路由文件曾经定义实现。

  1. 创立客户端 trpc 申请, client.ts

    import { createTRPCReact } from '@trpc/react-query';import { type AppRouter } from './routers';export const trpc = createTRPCReact<AppRouter>({});
  2. 创立 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>  );}
  3. 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 路由申请

  4. 定义服务端申请,创立 serverClient.ts

    import { appRouter } from './routers';import { createCallerFactory } from './trpc';const createCaller = createCallerFactory(appRouter);// 这里目前博主拿不到 req 和可写的 resHeadersexport const serverClient = createCaller({});
  5. 服务端组件调用

    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

示例仓库:暂无(后续补上)