自定义服务器启动
相关依赖
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();
- 配置
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-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;
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>
报错信息
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
文件数据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();
- 配置
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-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;
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>
报错信息
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。