关于next.js:NextJS-Trpc-PayloadCMS-MongoDB-自定义服务器搭建

6次阅读

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

自定义服务器启动

相干依赖

  • dotenv 读取 env 文件数据
  • express node 框架

<details> <summary> 根底示例如下 </summary>

// src/server/index.ts
import 'dotenv/config';
import express from 'express';
import chalk from 'chalk';

const port = Number(process.env.PORT) || 3000;
const app = express();
const nextApp = next({
  dev: process.env.NODE_ENV !== 'production',
  port: PORT,
});

const nextHandler = nextApp.getRequestHandler();
const start = async () => {
  // 筹备生成 .next 文件
  nextApp.prepare().then(() => {app.all('*', (req, res) => {return nextHandler(req, res);
    });

    app.listen(port, () => {
      console.log(
        '\x1b[36m%s\x1b[0m',
        `🎉🎉> Ready on http://localhost:${port}`
      );
    });
  });
};

start();

</details>

// package.json
// ...
// 这里须要应用 esno 而不能应用 node. 因为 node 是 CommonJs 而咱们代码中应用 es 标准
"dev": "esno src/server/index.ts"
// ...

配置 payload cms

集体了解 payload 和 cms 是两个货色,只是应用 payload 时主动应用了 cms, 如果不应用 cms 的话就不论。
payload 次要是操作数据库数据的,也有一些集成

相干依赖

  • @payloadcms/bundler-webpack
  • @payloadcms/db-mongodb
  • @payloadcms/richtext-slate
  • payload

开始前先抽离 nextApp nextHandler 函数,server 文件夹新建 next-utils.ts

import next from 'next';

const PORT = Number(process.env.PORT) || 3000;

// 创立 Next.js 利用实例
export const nextApp = next({
  dev: process.env.NODE_ENV !== 'production',
  port: PORT,
});

// 获取 Next.js 申请处理器。用于解决传入的 HTTP 申请,并依据 Next.js 利用的路由来响应这些申请。export const nextRequestHandler = nextApp.getRequestHandler();
  1. 配置 config. 在 server 文件夹下创立 payload.config.ts

<details> <summary> 根底示例如下 </summary>

/**
 * 配置 payload CMS 无头内容管理系统
 * @author peng-xiao-shuai
 * @see https://www.youtube.com/watch?v=06g6YJ6JCJU&t=8070s
 */

import path from 'path';
import {postgresAdapter} from '@payloadcms/db-postgres';
import {mongooseAdapter} from '@payloadcms/db-mongodb';
import {webpackBundler} from '@payloadcms/bundler-webpack';
import {slateEditor} from '@payloadcms/richtext-slate';
import {buildConfig} from 'payload/config';

export default buildConfig({
  // 设置服务器的 URL,从环境变量 NEXT_PUBLIC_SERVER_URL 获取。serverURL: process.env.NEXT_PUBLIC_SERVER_URL || '',
  admin: {
    // 设置用于 Payload CMS 治理界面的打包工具,这里应用了
    bundler: webpackBundler(),
    // 配置管理系统 Meta
    meta: {titleSuffix: 'Payload manage',},
  },
  // 定义路由,例如治理界面的路由。routes: {admin: '/admin',},
  // 设置富文本编辑器,这里应用了 Slate 编辑器。editor: slateEditor({}),
  typescript: {outputFile: path.resolve(__dirname, 'payload-types.ts'),
  },
  // 配置申请的速率限度,这里设置了最大值。rateLimit: {max: 2000,},

  // 上面 db 二选一。提醒:如果是用 mongodb 没有问题,应用 postgres 时存在问题,请更新依赖包
  db: mongooseAdapter({url: process.env.DATABASE_URI!,}),

  db: postgresAdapter({
    pool: {connectionString: process.env.SUPABASE_URL,},
  }),
});

</details>

  1. 初始化 payload.init. 这里初始化的时候还做了缓存机制. 在 server 文件夹下创立 get-payload.ts

<details> <summary> 根底示例如下 </summary>

/**
 * 解决缓存机制。确保利用中多处须要应用 Payload 客户端时不会反复初始化,提高效率。* @author peng-xiao-shuai
 */
import type {InitOptions} from 'payload/config';
import type {Payload} from 'payload';
import payload from 'payload';

// 应用 Node.js 的 global 对象来存储缓存。let cached = (global as any).payload;

if (!cached) {cached = (global as any).payload = {
    client: null,
    promise: null,
  };
}

/**
 * 负责初始化 Payload 客户端
 * @return {Promise<Payload>}
 */
export const getPayloadClient = async ({initOptions,}: {initOptions: Partial<InitOptions>;}): Promise<Payload> => {if (!process.env.PAYLOAD_SECRET) {throw new Error('PAYLOAD_SECRET is missing');
  }

  if (cached.client) {return cached.client;}

  if (!cached.promise) {
    // payload 初始化赋值
    cached.promise = payload.init({
      // email: {
      //   transport: transporter,
      //   fromAddress: 'hello@joshtriedcoding.com',
      //   fromName: 'DigitalHippo',
      // },
      secret: process.env.PAYLOAD_SECRET,
      local: initOptions?.express ? false : true,
      ...(initOptions || {}),
    });
  }

  try {cached.client = await cached.promise;} catch (e: unknown) {
    cached.promise = null;
    throw e;
  }

  return cached.client;
};

</details>

  1. index.ts 引入

<details> <summary> 根底示例如下 </summary>

// 读取环境变量
import 'dotenv/config';
import express from 'express';
import {nextApp, nextRequestHandler} from './next-utils';
import {getPayloadClient} from './get-payload';

const port = Number(process.env.PORT) || 3000;
const app = express();
const start = async () => {
  // 获取 payload
  const payload = await getPayloadClient({
    initOptions: {
      express: app,
      onInit: async (cms) => {console.log('\x1b[36m%s\x1b[0m', '✨✨Admin URL:' + cms.getAdminURL());
      },
    },
  });

  app.use((req, res) => nextRequestHandler(req, res));

  // 筹备生成 .next 文件
  nextApp.prepare().then(() => {app.listen(port, () => {
      console.log(
        '\x1b[36m%s\x1b[0m',
        `🎉🎉> Ready on http://localhost:${port}`
      );
    });
  });
};

start();

</details>

  1. dev 运行配置. 装置 cross-env nodemon. 设置 payload 配置文件门路. nodemon 启动
// package.json
// ...
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/server/payload.config.ts nodemon",
// ...
  1. nodemon 配置。根目录创立 nodemon.json
{"watch": ["src/server/index.ts"],
  "exec": "ts-node --project tsconfig.server.json src/server/index.ts -- -I",
  "ext": "js ts",
  "stdin": false
}

<!– 先跑起来根底示例后再浏览 –>

payload 进阶

  1. 定义类型。payload.config.ts 同级目录新增 payload-types.ts

<details> <summary> 示例如下 </summary>

// payload.config.ts
// ...
typescript: {outputFile: path.resolve(__dirname, 'payload-types.ts'),
}
// ...
// package.json 新增命令
// ...
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/server/payload.config.ts payload generate:types",
// ...

执行 yarn generate:types 那么会在 payload-types.ts 文件中写入根底汇合(Collection)类型

</details>

  1. 批改用户 Collection 汇合。collection

前提 server 文件夹下新增 collections 文件夹而后新增 Users.ts 文件

<details> <summary> 示例如下 </summary>

// Users.ts
import {CollectionConfig} from 'payload/types';
export const Users: CollectionConfig = {
  slug: 'users',
  auth: true,
  fields: [
    {
      // 定义地址
      name: 'address',
      required: true,
      type: 'text', // 贴别留神不同的类型有不同的数据 https://payloadcms.com/docs/fields/text
    },
    {
      name: 'points',
      hidden: true,
      defaultValue: 0,
      type: 'number',
    },
  ],
  access: {read: () => true,
    delete: () => false,
    create: ({data, id, req}) => {
      // 设置管理系统不能增加
      return !req.headers.referer?.includes('/admin');
    },
    update: ({data, id, req}) => {
      // 设置管理系统不能增加
      return !req.headers.referer?.includes('/admin');
    },
  },
};

还须要更改 payload.config.ts 中配置

import {Users} from './collections/Users';
// ...
collections: [Users],
admin: {
  user: 'users', // @see https://payloadcms.com/docs/admin/overview#the-admin-user-collection
  //  ...
},
// ...
  1. 新增在创立一个积分记录汇合。collections 文件夹下新增 PointsRecord.ts 文件
/**
 * 积分记录
 */
import {CollectionBeforeChangeHook, CollectionConfig} from 'payload/types';
import {PointsRecord as PointsRecordType} from '../payload-types';
import {getPayloadClient} from '../get-payload';

// @see https://payloadcms.com/docs/hooks/collections#beforechange
// https://payloadcms.com/docs/hooks/collections 中蕴含所有汇合钩子
const beforeChange: CollectionBeforeChangeHook<PointsRecordType> = async ({
  data,
  operate // 操作类型,这里就不须要判断了,因为只有批改前才会触发这个钩子,而批改又只有 update create delete 会触发。update delete 又被咱们禁用了所以只有 create 会触发
}) => {
  // 获取 payload
  const payload = await getPayloadClient();

  // 批改数据
  data.operateType = (data.count || 0) >= 0 ? 'added' : 'reduce';

  // 获取以后用户 ID 的数据
  const result = await payload.findByID({
    collection: 'users', // required
    id: data.userId as number, // required
  });

  // 批改用户数据
  await payload.update({
    collection: 'users', // required
    id: data.userId as number, // required
    data: {
      ...result,
      points: (result.points || 0) + data.count!,
    },
  });

  return data;
};

export const PointsRecord: CollectionConfig = {
  slug: 'points-record', // 汇合名称,也就是数据库表名
  fields: [
    {
      name: 'userId',
      type: 'relationship',
      required: true,
      relationTo: 'users',
    },
    {
      name: 'count',
      type: 'number',
      required: true,
    },
    {
      name: 'operateType',
      type: 'select',
      // 这里暗藏防止在 cms 中显示,因为 operateType 值是由判断 count 生成。hidden: true,
      options: [
        {
          label: '减少',
          value: 'added',
        },
        {
          label: '缩小',
          value: 'reduce',
        },
      ],
    },
  ],
  // 这个汇合操作数据前的钩子
  hooks: {beforeChange: [beforeChange],
  },
  access: {read: () => true,
    create: () => true,
    update: () => false,
    delete: () => false,},
};

</details>

同样还须要更改 payload.config.ts 中配置

import {Users} from './collections/Users';
import {PointsRecord} from './collections/PointsRecord';
// ...
collections: [Users,PointsRecord],
// ...

装置 trpc

相干依赖

  • @trpc/server
  • @trpc/client
  • @trpc/next
  • @trpc/react-query
  • @tanstack/react-query
  • zod 校验

& 是在 next.config.js 文件夹中进行了配置

import path from 'path';
/** @type {import('next').NextConfig} */
const nextConfig = {webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack}) => {
    // 设置别名
    config.resolve.alias['@'] = path.join(__dirname, 'src');
    config.resolve.alias['&'] = path.join(__dirname, 'src/server');

    // 重要: 返回批改后的配置
    return config;
  },
};

module.exports = nextConfig;
  1. server 文件夹上面创立 trpc 文件夹而后创立 trpc.ts 文件。初始化 trpc

<details> <summary> 根底示例如下 </summary>

import {initTRPC} from '@trpc/server';
import {ExpressContext} from '../';

// context 创立上下文
const t = initTRPC.context<ExpressContext>().create();

// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;

</details>

  1. 同级目录新建 client.ts 文件 trpc
import {createTRPCReact} from '@trpc/react-query';
import type {AppRouter} from './routers';

export const trpc = createTRPCReact < AppRouter > {};
  1. app 文件夹下新增 components 文件夹在创立 Providers.tsx 文件为客户端组件

<details> <summary> 根底示例如下 </summary>

'use client';

import {PropsWithChildren, useState} from 'react';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {trpc} from '&/trpc/client';
import {httpBatchLink} from '@trpc/client';

export const Providers = ({children}: PropsWithChildren) => {const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({url: `${process.env.NEXT_PUBLIC_SERVER_URL}/api/trpc`,

          /**
           * @see https://trpc.io/docs/client/headers
           */
          // async headers() {
          //   return {//     authorization: getAuthCookie(),
          //   };
          // },

          /**
           * @see https://trpc.io/docs/client/cors
           */
          fetch(url, options) {
            return fetch(url, {
              ...options,
              credentials: 'include',
            });
          },
        }),
      ],
    })
  );

  return (<trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
};

</details>

  1. server/trpc 文件夹下创立 routers.ts 文件 example

<details> <summary> 根底示例如下 </summary>

import {procedure, router} from './trpc';
export const appRouter = router({
  hello: procedure
    .input(
      z
        .object({text: z.string().nullish(),})
        .nullish())
    .query((opts) => {
      return {greeting: `hello ${opts.input?.text ?? 'world'}`,
      };
    }),
});
// export type definition of API
export type AppRouter = typeof appRouter;

</details>

  1. 任意 page.tsx 页面 example

<details> <summary> 根底示例如下 </summary>

// 'use client'; // 如果页面有交互的话须要改成客户端组件
import {trpc} from '&/trpc/client';

export function MyComponent() {
  // input is optional, so we don't have to pass second argument
  const helloNoArgs = trpc.hello.useQuery();
  const helloWithArgs = trpc.hello.useQuery({text: 'client'});

  return (
    <div>
      <h1>Hello World Example</h1>
      <ul>
        <li>
          helloNoArgs ({helloNoArgs.status}):{' '}
          <pre>{JSON.stringify(helloNoArgs.data, null, 2)}</pre>
        </li>
        <li>
          helloWithArgs ({helloWithArgs.status}):{' '}
          <pre>{JSON.stringify(helloWithArgs.data, null, 2)}</pre>
        </li>
      </ul>
    </div>
  );
}

</details>

  1. index.ts 文件引入

<details> <summary> 根底示例如下 </summary>

import express from 'express';
import {nextApp, nextRequestHandler} from './next-utils';
import {getPayloadClient} from './get-payload';
import * as trpcExpress from '@trpc/server/adapters/express';
import {inferAsyncReturnType} from '@trpc/server';
import {config} from 'dotenv';
import {appRouter} from './trpc/routers';
config({path: '.env.local'});
config({path: '.env'});

const port = Number(process.env.PORT) || 3000;
const app = express();

const createContext = ({
  req,
  res,
}: trpcExpress.CreateExpressContextOptions) => ({req, res});

export type ExpressContext = inferAsyncReturnType<typeof createContext>;

const start = async () => {
  // 获取 payload
  const payload = await getPayloadClient({
    initOptions: {
      express: app,
      onInit: async (cms) => {console.log('\x1b[36m%s\x1b[0m', '✨✨Admin URL:' + cms.getAdminURL());
      },
    },
  });

  app.use(
    '/api/trpc',
    trpcExpress.createExpressMiddleware({
      router: appRouter,
      /**
       * @see https://trpc.io/docs/server/adapters/express#3-use-the-express-adapter
       * @example
        // 加了 返回了 req, res 之后能够在 trpc 路由中间接拜访
        import {createRouter} from '@trpc/server';
        import {z} from 'zod';

        const exampleRouter = createRouter<Context>()
          .query('exampleQuery', {input: z.string(),
            resolve({input, ctx}) {
              // 间接拜访 req 和 res
              const userAgent = ctx.req.headers['user-agent'];
              ctx.res.status(200).json({message: 'Hello' + input});

              // 你的业务逻辑
              ...
            },
          });
       */
      createContext,
    })
  );
  app.use((req, res) => nextRequestHandler(req, res));

  // 筹备生成 .next 文件
  nextApp.prepare().then(() => {app.listen(port, () => {
      console.log(
        '\x1b[36m%s\x1b[0m',
        `🎉🎉> Ready on http://localhost:${port}`
      );
    });
  });
};

start();

</details>

报错信息

sharp module

ERROR (payload): Error: cannot connect to MongoDB. Details: queryTxt ETIMEOUT xxx.mongodb.net

  • 设置网络 Ipv4 DNS 服务器为 114.114.114.144
  • 敞开防火墙
  • 设置 mongodb 可拜访的 ip0.0.0.0/0
    *

服务端

自定义服务器启动

相干依赖

  • dotenv 读取 env 文件数据
  • express node 框架

<details> <summary> 根底示例如下 </summary>

// src/server/index.ts
import 'dotenv/config';
import express from 'express';
import chalk from 'chalk';

const port = Number(process.env.PORT) || 3000;
const app = express();
const nextApp = next({
  dev: process.env.NODE_ENV !== 'production',
  port: PORT,
});

const nextHandler = nextApp.getRequestHandler();
const start = async () => {
  // 筹备生成 .next 文件
  nextApp.prepare().then(() => {app.all('*', (req, res) => {return nextHandler(req, res);
    });

    app.listen(port, () => {
      console.log(
        '\x1b[36m%s\x1b[0m',
        `🎉🎉> Ready on http://localhost:${port}`
      );
    });
  });
};

start();

</details>

// package.json
// ...
// 这里须要应用 esno 而不能应用 node. 因为 node 是 CommonJs 而咱们代码中应用 es 标准
"dev": "esno src/server/index.ts"
// ...

配置 payload cms

集体了解 payload 和 cms 是两个货色,只是应用 payload 时主动应用了 cms, 如果不应用 cms 的话就不论。
payload 次要是操作数据库数据的,也有一些集成

相干依赖

  • @payloadcms/bundler-webpack
  • @payloadcms/db-mongodb
  • @payloadcms/richtext-slate
  • payload

开始前先抽离 nextApp nextHandler 函数,server 文件夹新建 next-utils.ts

import next from 'next';

const PORT = Number(process.env.PORT) || 3000;

// 创立 Next.js 利用实例
export const nextApp = next({
  dev: process.env.NODE_ENV !== 'production',
  port: PORT,
});

// 获取 Next.js 申请处理器。用于解决传入的 HTTP 申请,并依据 Next.js 利用的路由来响应这些申请。export const nextRequestHandler = nextApp.getRequestHandler();
  1. 配置 config. 在 server 文件夹下创立 payload.config.ts

<details> <summary> 根底示例如下 </summary>

/**
 * 配置 payload CMS 无头内容管理系统
 * @author peng-xiao-shuai
 * @see https://www.youtube.com/watch?v=06g6YJ6JCJU&t=8070s
 */

import path from 'path';
import {postgresAdapter} from '@payloadcms/db-postgres';
import {mongooseAdapter} from '@payloadcms/db-mongodb';
import {webpackBundler} from '@payloadcms/bundler-webpack';
import {slateEditor} from '@payloadcms/richtext-slate';
import {buildConfig} from 'payload/config';

export default buildConfig({
  // 设置服务器的 URL,从环境变量 NEXT_PUBLIC_SERVER_URL 获取。serverURL: process.env.NEXT_PUBLIC_SERVER_URL || '',
  admin: {
    // 设置用于 Payload CMS 治理界面的打包工具,这里应用了
    bundler: webpackBundler(),
    // 配置管理系统 Meta
    meta: {titleSuffix: 'Payload manage',},
  },
  // 定义路由,例如治理界面的路由。routes: {admin: '/admin',},
  // 设置富文本编辑器,这里应用了 Slate 编辑器。editor: slateEditor({}),
  typescript: {outputFile: path.resolve(__dirname, 'payload-types.ts'),
  },
  // 配置申请的速率限度,这里设置了最大值。rateLimit: {max: 2000,},

  // 上面 db 二选一。提醒:如果是用 mongodb 没有问题,应用 postgres 时存在问题,请更新依赖包
  db: mongooseAdapter({url: process.env.DATABASE_URI!,}),

  db: postgresAdapter({
    pool: {connectionString: process.env.SUPABASE_URL,},
  }),
});

</details>

  1. 初始化 payload.init. 这里初始化的时候还做了缓存机制. 在 server 文件夹下创立 get-payload.ts

<details> <summary> 根底示例如下 </summary>

/**
 * 解决缓存机制。确保利用中多处须要应用 Payload 客户端时不会反复初始化,提高效率。* @author peng-xiao-shuai
 */
import type {InitOptions} from 'payload/config';
import type {Payload} from 'payload';
import payload from 'payload';

// 应用 Node.js 的 global 对象来存储缓存。let cached = (global as any).payload;

if (!cached) {cached = (global as any).payload = {
    client: null,
    promise: null,
  };
}

/**
 * 负责初始化 Payload 客户端
 * @return {Promise<Payload>}
 */
export const getPayloadClient = async ({initOptions,}: {initOptions: Partial<InitOptions>;}): Promise<Payload> => {if (!process.env.PAYLOAD_SECRET) {throw new Error('PAYLOAD_SECRET is missing');
  }

  if (cached.client) {return cached.client;}

  if (!cached.promise) {
    // payload 初始化赋值
    cached.promise = payload.init({
      // email: {
      //   transport: transporter,
      //   fromAddress: 'hello@joshtriedcoding.com',
      //   fromName: 'DigitalHippo',
      // },
      secret: process.env.PAYLOAD_SECRET,
      local: initOptions?.express ? false : true,
      ...(initOptions || {}),
    });
  }

  try {cached.client = await cached.promise;} catch (e: unknown) {
    cached.promise = null;
    throw e;
  }

  return cached.client;
};

</details>

  1. index.ts 引入

<details> <summary> 根底示例如下 </summary>

// 读取环境变量
import 'dotenv/config';
import express from 'express';
import {nextApp, nextRequestHandler} from './next-utils';
import {getPayloadClient} from './get-payload';

const port = Number(process.env.PORT) || 3000;
const app = express();
const start = async () => {
  // 获取 payload
  const payload = await getPayloadClient({
    initOptions: {
      express: app,
      onInit: async (cms) => {console.log('\x1b[36m%s\x1b[0m', '✨✨Admin URL:' + cms.getAdminURL());
      },
    },
  });

  app.use((req, res) => nextRequestHandler(req, res));

  // 筹备生成 .next 文件
  nextApp.prepare().then(() => {app.listen(port, () => {
      console.log(
        '\x1b[36m%s\x1b[0m',
        `🎉🎉> Ready on http://localhost:${port}`
      );
    });
  });
};

start();

</details>

  1. dev 运行配置. 装置 cross-env nodemon. 设置 payload 配置文件门路. nodemon 启动
// package.json
// ...
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/server/payload.config.ts nodemon",
// ...
  1. nodemon 配置。根目录创立 nodemon.json

<!– 我也不晓得这些配置什么意思配就行了 –>

{"watch": ["src/server/index.ts"],
  "exec": "ts-node --project tsconfig.server.json src/server/index.ts -- -I",
  "ext": "js ts",
  "stdin": false
}

<!– 先跑起来根底示例后再浏览 –>

payload 进阶

  1. 定义类型。payload.config.ts 同级目录新增 payload-types.ts

<details> <summary> 示例如下 </summary>

// payload.config.ts
// ...
typescript: {outputFile: path.resolve(__dirname, 'payload-types.ts'),
}
// ...
// package.json 新增命令
// ...
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/server/payload.config.ts payload generate:types",
// ...

执行 yarn generate:types 那么会在 payload-types.ts 文件中写入根底汇合(Collection)类型

</details>

  1. 批改用户 Collection 汇合。collection

前提 server 文件夹下新增 collections 文件夹而后新增 Users.ts 文件

<details> <summary> 示例如下 </summary>

// Users.ts
import {CollectionConfig} from 'payload/types';
export const Users: CollectionConfig = {
  slug: 'users',
  auth: true,
  fields: [
    {
      // 定义地址
      name: 'address',
      required: true,
      type: 'text', // 贴别留神不同的类型有不同的数据 https://payloadcms.com/docs/fields/text
    },
    {
      name: 'points',
      hidden: true,
      defaultValue: 0,
      type: 'number',
    },
  ],
  access: {read: () => true,
    delete: () => false,
    create: ({data, id, req}) => {
      // 设置管理系统不能增加
      return !req.headers.referer?.includes('/admin');
    },
    update: ({data, id, req}) => {
      // 设置管理系统不能增加
      return !req.headers.referer?.includes('/admin');
    },
  },
};

还须要更改 payload.config.ts 中配置

import {Users} from './collections/Users';
// ...
collections: [Users],
admin: {
  user: 'users', // @see https://payloadcms.com/docs/admin/overview#the-admin-user-collection
  //  ...
},
// ...
  1. 新增在创立一个积分记录汇合。collections 文件夹下新增 PointsRecord.ts 文件
/**
 * 积分记录
 */
import {CollectionBeforeChangeHook, CollectionConfig} from 'payload/types';
import {PointsRecord as PointsRecordType} from '../payload-types';
import {getPayloadClient} from '../get-payload';

// @see https://payloadcms.com/docs/hooks/collections#beforechange
// https://payloadcms.com/docs/hooks/collections 中蕴含所有汇合钩子
const beforeChange: CollectionBeforeChangeHook<PointsRecordType> = async ({
  data,
  operate // 操作类型,这里就不须要判断了,因为只有批改前才会触发这个钩子,而批改又只有 update create delete 会触发。update delete 又被咱们禁用了所以只有 create 会触发
}) => {
  // 获取 payload
  const payload = await getPayloadClient();

  // 批改数据
  data.operateType = (data.count || 0) >= 0 ? 'added' : 'reduce';

  // 获取以后用户 ID 的数据
  const result = await payload.findByID({
    collection: 'users', // required
    id: data.userId as number, // required
  });

  // 批改用户数据
  await payload.update({
    collection: 'users', // required
    id: data.userId as number, // required
    data: {
      ...result,
      points: (result.points || 0) + data.count!,
    },
  });

  return data;
};

export const PointsRecord: CollectionConfig = {
  slug: 'points-record', // 汇合名称,也就是数据库表名
  fields: [
    {
      name: 'userId',
      type: 'relationship',
      required: true,
      relationTo: 'users',
    },
    {
      name: 'count',
      type: 'number',
      required: true,
    },
    {
      name: 'operateType',
      type: 'select',
      // 这里暗藏防止在 cms 中显示,因为 operateType 值是由判断 count 生成。hidden: true,
      options: [
        {
          label: '减少',
          value: 'added',
        },
        {
          label: '缩小',
          value: 'reduce',
        },
      ],
    },
  ],
  // 这个汇合操作数据前的钩子
  hooks: {beforeChange: [beforeChange],
  },
  access: {read: () => true,
    create: () => true,
    update: () => false,
    delete: () => false,},
};

</details>

同样还须要更改 payload.config.ts 中配置

import {Users} from './collections/Users';
import {PointsRecord} from './collections/PointsRecord';
// ...
collections: [Users,PointsRecord],
// ...

装置 trpc

相干依赖

  • @trpc/server
  • @trpc/client
  • @trpc/next
  • @trpc/react-query
  • @tanstack/react-query
  • zod 校验

& 是在 next.config.js 文件夹中进行了配置

import path from 'path';
/** @type {import('next').NextConfig} */
const nextConfig = {webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack}) => {
    // 设置别名
    config.resolve.alias['@'] = path.join(__dirname, 'src');
    config.resolve.alias['&'] = path.join(__dirname, 'src/server');

    // 重要: 返回批改后的配置
    return config;
  },
};

module.exports = nextConfig;
  1. server 文件夹上面创立 trpc 文件夹而后创立 trpc.ts 文件。初始化 trpc

<details> <summary> 根底示例如下 </summary>

import {initTRPC} from '@trpc/server';
import {ExpressContext} from '../';

// context 创立上下文
const t = initTRPC.context<ExpressContext>().create();

// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;

</details>

  1. 同级目录新建 client.ts 文件 trpc
import {createTRPCReact} from '@trpc/react-query';
import type {AppRouter} from './routers';

export const trpc = createTRPCReact < AppRouter > {};
  1. app 文件夹下新增 components 文件夹在创立 Providers.tsx 文件为客户端组件

<details> <summary> 根底示例如下 </summary>

'use client';

import {PropsWithChildren, useState} from 'react';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {trpc} from '&/trpc/client';
import {httpBatchLink} from '@trpc/client';

export const Providers = ({children}: PropsWithChildren) => {const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({url: `${process.env.NEXT_PUBLIC_SERVER_URL}/api/trpc`,

          /**
           * @see https://trpc.io/docs/client/headers
           */
          // async headers() {
          //   return {//     authorization: getAuthCookie(),
          //   };
          // },

          /**
           * @see https://trpc.io/docs/client/cors
           */
          fetch(url, options) {
            return fetch(url, {
              ...options,
              credentials: 'include',
            });
          },
        }),
      ],
    })
  );

  return (<trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
};

</details>

  1. server/trpc 文件夹下创立 routers.ts 文件 example

<details> <summary> 根底示例如下 </summary>

import {procedure, router} from './trpc';
export const appRouter = router({
  hello: procedure
    .input(
      z
        .object({text: z.string().nullish(),})
        .nullish())
    .query((opts) => {
      return {greeting: `hello ${opts.input?.text ?? 'world'}`,
      };
    }),
});
// export type definition of API
export type AppRouter = typeof appRouter;

</details>

  1. 任意 page.tsx 页面 example

<details> <summary> 根底示例如下 </summary>

// 'use client'; // 如果页面有交互的话须要改成客户端组件
import {trpc} from '&/trpc/client';

export function MyComponent() {
  // input is optional, so we don't have to pass second argument
  const helloNoArgs = trpc.hello.useQuery();
  const helloWithArgs = trpc.hello.useQuery({text: 'client'});

  return (
    <div>
      <h1>Hello World Example</h1>
      <ul>
        <li>
          helloNoArgs ({helloNoArgs.status}):{' '}
          <pre>{JSON.stringify(helloNoArgs.data, null, 2)}</pre>
        </li>
        <li>
          helloWithArgs ({helloWithArgs.status}):{' '}
          <pre>{JSON.stringify(helloWithArgs.data, null, 2)}</pre>
        </li>
      </ul>
    </div>
  );
}

</details>

  1. index.ts 文件引入

<details> <summary> 根底示例如下 </summary>

import express from 'express';
import {nextApp, nextRequestHandler} from './next-utils';
import {getPayloadClient} from './get-payload';
import * as trpcExpress from '@trpc/server/adapters/express';
import {inferAsyncReturnType} from '@trpc/server';
import {config} from 'dotenv';
import {appRouter} from './trpc/routers';
config({path: '.env.local'});
config({path: '.env'});

const port = Number(process.env.PORT) || 3000;
const app = express();

const createContext = ({
  req,
  res,
}: trpcExpress.CreateExpressContextOptions) => ({req, res});

export type ExpressContext = inferAsyncReturnType<typeof createContext>;

const start = async () => {
  // 获取 payload
  const payload = await getPayloadClient({
    initOptions: {
      express: app,
      onInit: async (cms) => {console.log('\x1b[36m%s\x1b[0m', '✨✨Admin URL:' + cms.getAdminURL());
      },
    },
  });

  app.use(
    '/api/trpc',
    trpcExpress.createExpressMiddleware({
      router: appRouter,
      /**
       * @see https://trpc.io/docs/server/adapters/express#3-use-the-express-adapter
       * @example
        // 加了 返回了 req, res 之后能够在 trpc 路由中间接拜访
        import {createRouter} from '@trpc/server';
        import {z} from 'zod';

        const exampleRouter = createRouter<Context>()
          .query('exampleQuery', {input: z.string(),
            resolve({input, ctx}) {
              // 间接拜访 req 和 res
              const userAgent = ctx.req.headers['user-agent'];
              ctx.res.status(200).json({message: 'Hello' + input});

              // 你的业务逻辑
              ...
            },
          });
       */
      createContext,
    })
  );
  app.use((req, res) => nextRequestHandler(req, res));

  // 筹备生成 .next 文件
  nextApp.prepare().then(() => {app.listen(port, () => {
      console.log(
        '\x1b[36m%s\x1b[0m',
        `🎉🎉> Ready on http://localhost:${port}`
      );
    });
  });
};

start();

</details>

报错信息

sharp module

ERROR (payload): Error: cannot connect to MongoDB. Details: queryTxt ETIMEOUT xxx.mongodb.net

  • 设置网络 Ipv4 DNS 服务器为 114.114.114.144
  • 敞开防火墙
  • 设置 mongodb 可拜访的 ip0.0.0.0/0
  • 在引入 trpc 的页面,须要将页面改成客户端组件

TypeError: (0 , react**WEBPACK\_IMPORTED\_MODULE\_3**.createContext) is not a function

  • 在引入 trpc 的页面,须要将页面改成客户端组件

重启服务端

  • server 文件夹上面只有 index.ts 文件会被保留会从新加载服务端,其余文件更改须要再去 index.ts 从新保留

或者将 nodemon.json 配置文件更改。watch 中增加其余的文件,保留后主动重启

{"watch": ["src/server/*.ts", "src/server/**/*.ts"],
  "exec": "ts-node --project tsconfig.server.json src/server/index.ts -- -I",
  "ext": "js ts",
  "stdin": false
}

示例仓库地址:Github

分割邮箱:Email

环境变量

克隆后根目录新建 .env.local,写入相应环境变量

# 数据库连贯地址
DATABASE_URL

# 邮件 API_KEY 须要去 https://resend.com/ 申请
RESEND_API_KEY

# 邮件 PUSHER_APP_ID NEXT_PUBLIC_PUSHER_APP_KEY PUSHER_APP_SECRET NEXT_PUBLIC_PUSHER_APP_CLUSTER 须要去 https://pusher.com/ 申请
PUSHER_APP_ID
NEXT_PUBLIC_PUSHER_APP_KEY
PUSHER_APP_SECRET
NEXT_PUBLIC_PUSHER_APP_CLUSTER
正文完
 0