Stories

Detail Return Return

NextJS14 app + Trpc + PayloadCMS + MongoDB 自定義服務器搭建 - Stories Detail

自定義服務器啓動

相關依賴

  • 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
    image.png
  • 在引入 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

聯繫郵箱:1612565136@qq.com

環境變量

克隆後根目錄新建 .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
user avatar vleedesigntheory Avatar evenboy Avatar hea1066 Avatar fanudekaixinguo Avatar songhuijin Avatar gomi Avatar jump_and_jump Avatar jueqiangdeqianbi Avatar wisdomqq Avatar xiange Avatar mengdong_5927a02673e54 Avatar syfssb Avatar
Favorites 15 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.