共计 23490 个字符,预计需要花费 59 分钟才能阅读完成。
自定义服务器启动
相干依赖
dotenv
读取env
文件数据express
node
框架
<details> <summary> 根底示例如下 </summary>
// src/server/index.ts
import 'dotenv/config';
import express from 'express';
import chalk from 'chalk';
const port = Number(process.env.PORT) || 3000;
const app = express();
const nextApp = next({
dev: process.env.NODE_ENV !== 'production',
port: PORT,
});
const nextHandler = nextApp.getRequestHandler();
const start = async () => {
// 筹备生成 .next 文件
nextApp.prepare().then(() => {app.all('*', (req, res) => {return nextHandler(req, res);
});
app.listen(port, () => {
console.log(
'\x1b[36m%s\x1b[0m',
`🎉🎉> Ready on http://localhost:${port}`
);
});
});
};
start();
</details>
// package.json
// ...
// 这里须要应用 esno 而不能应用 node. 因为 node 是 CommonJs 而咱们代码中应用 es 标准
"dev": "esno src/server/index.ts"
// ...
配置 payload cms
集体了解 payload 和 cms 是两个货色,只是应用 payload 时主动应用了 cms, 如果不应用 cms 的话就不论。
payload 次要是操作数据库数据的,也有一些集成
相干依赖
@payloadcms/bundler-webpack
@payloadcms/db-mongodb
@payloadcms/richtext-slate
payload
开始前先抽离
nextApp
nextHandler
函数,server
文件夹新建next-utils.ts
import next from 'next';
const PORT = Number(process.env.PORT) || 3000;
// 创立 Next.js 利用实例
export const nextApp = next({
dev: process.env.NODE_ENV !== 'production',
port: PORT,
});
// 获取 Next.js 申请处理器。用于解决传入的 HTTP 申请,并依据 Next.js 利用的路由来响应这些申请。export const nextRequestHandler = nextApp.getRequestHandler();
- 配置
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>
报错信息
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
文件数据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>
报错信息
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
分割邮箱:Email
环境变量
克隆后根目录新建 .env.local
,写入相应环境变量
# 数据库连贯地址
DATABASE_URL
# 邮件 API_KEY 须要去 https://resend.com/ 申请
RESEND_API_KEY
# 邮件 PUSHER_APP_ID NEXT_PUBLIC_PUSHER_APP_KEY PUSHER_APP_SECRET NEXT_PUBLIC_PUSHER_APP_CLUSTER 须要去 https://pusher.com/ 申请
PUSHER_APP_ID
NEXT_PUBLIC_PUSHER_APP_KEY
PUSHER_APP_SECRET
NEXT_PUBLIC_PUSHER_APP_CLUSTER