Next14 app +Trpc 部署到 Vercel

本文详细介绍了如何在Next14app中集成MongoDB数据库,使用trpc进行服务端和客户端通信,并实现JWT认证。作者分享了创建中间件、处理用户登录和积分操作的步骤,以及客户端和服务端如何调用trpc路由。
摘要由CSDN通过智能技术生成

本文使用了 MongoDB, 还没有集成的可以看一下上篇文章

next13 可以参考 trpc 文档 而且谷歌上已经有不少问题解答,但是目前 next14 app 只看到一个项目中有用到 Github 仓库,目前这个仓库中服务端的上下文获取存在问题,目前找到一个有用的可以看 Issus。目前 trpcnext14 app 的支持进度可以看 Issus

好的进入 正文

  1. 安装依赖包(这里我的依赖包版本是 10.43.1
yarn add @trpc/serve @trpc/client @trpc/react-query zod
  1. 创建中间件 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>>;
  1. 创建 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;
  1. 创建 trpc 路由 auth-router.ts points-router.ts routers.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;

至此路由文件已经定义完成。

  1. 创建客户端 trpc 请求, client.ts
import { createTRPCReact } from '@trpc/react-query';
import { type AppRouter } from './routers';

export const trpc = createTRPCReact<AppRouter>({});
  1. 创建 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>
  );
}
  1. 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 路由请求

  1. 定义服务端请求,创建 serverClient.ts
import { appRouter } from './routers';
import { createCallerFactory } from './trpc';

const createCaller = createCallerFactory(appRouter);

// 这里目前博主拿不到 req 和可写的 resHeaders
export const serverClient = createCaller({});
  1. 服务端组件调用
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

示例仓库:Github

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值