自定義服務器啓動
相關依賴
dotenv讀取env文件數據expressnode框架
<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-slatepayload
開始前先抽離nextAppnextHandler函數,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();
- 配置
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>
- 初始化
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>
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>
dev運行配置. 安裝cross-env nodemon. 設置payload配置文件路徑.nodemon啓動
// package.json
// ...
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/server/payload.config.ts nodemon",
// ...
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 進階
- 定義類型。
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>
- 修改用户
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
// ...
},
// ...
- 新增在創建一個積分記錄集合。
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-queryzod校驗
&是在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;
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>
- 同級目錄新建
client.ts文件 trpc
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './routers';
export const trpc = createTRPCReact < AppRouter > {};
- 在
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>
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>
- 任意
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>
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可訪問的ip為0.0.0.0/0
*
服務端
自定義服務器啓動
相關依賴
dotenv讀取env文件數據expressnode框架
<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-slatepayload
開始前先抽離nextAppnextHandler函數,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();
- 配置
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>
- 初始化
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>
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>
dev運行配置. 安裝cross-env nodemon. 設置payload配置文件路徑.nodemon啓動
// package.json
// ...
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/server/payload.config.ts nodemon",
// ...
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 進階
- 定義類型。
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>
- 修改用户
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
// ...
},
// ...
- 新增在創建一個積分記錄集合。
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-queryzod校驗
&是在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;
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>
- 同級目錄新建
client.ts文件 trpc
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './routers';
export const trpc = createTRPCReact < AppRouter > {};
- 在
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>
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>
- 任意
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>
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可訪問的ip為0.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
聯繫郵箱: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