关于next.js:Next14-app-Trpc-部署到-Vercel

119次阅读

共计 7968 个字符,预计需要花费 20 分钟才能阅读完成。

本文应用了 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'; // 格式化 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;
  4. 创立 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>({});
  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.tsx
    import {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 和可写的 resHeaders
    export 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

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

正文完
 0