自定义服务器启动

相干依赖

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

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

// src/server/index.tsimport '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.tsimport { 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 helpersexport 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 APIexport 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.tsimport '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.tsimport { 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 helpersexport 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 APIexport 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_IDNEXT_PUBLIC_PUSHER_APP_KEYPUSHER_APP_SECRETNEXT_PUBLIC_PUSHER_APP_CLUSTER