自定义服务器启动
相干依赖
dotenv
读取env
文件数据express
node
框架
<details> <summary>根底示例如下</summary>
// src/server/index.tsimport 'dotenv/config';import express from 'express';import chalk from 'chalk';const port = Number(process.env.PORT) || 3000;const app = express();const nextApp = next({ dev: process.env.NODE_ENV !== 'production', port: PORT,});const nextHandler = nextApp.getRequestHandler();const start = async () => { // 筹备生成 .next 文件 nextApp.prepare().then(() => { app.all('*', (req, res) => { return nextHandler(req, res); }); app.listen(port, () => { console.log( '\x1b[36m%s\x1b[0m', `> Ready on http://localhost:${port}` ); }); });};start();
</details>
// package.json// ...// 这里须要应用 esno 而不能应用 node. 因为 node 是 CommonJs 而咱们代码中应用 es 标准"dev": "esno src/server/index.ts"// ...
配置 payload cms
集体了解 payload 和 cms 是两个货色,只是应用 payload 时主动应用了 cms, 如果不应用 cms 的话就不论。
payload 次要是操作数据库数据的,也有一些集成
相干依赖
@payloadcms/bundler-webpack
@payloadcms/db-mongodb
@payloadcms/richtext-slate
payload
开始前先抽离nextApp
nextHandler
函数,server
文件夹新建next-utils.ts
import next from 'next';const PORT = Number(process.env.PORT) || 3000;// 创立 Next.js 利用实例export const nextApp = next({ dev: process.env.NODE_ENV !== 'production', port: PORT,});// 获取 Next.js 申请处理器。用于解决传入的 HTTP 申请,并依据 Next.js 利用的路由来响应这些申请。export const nextRequestHandler = nextApp.getRequestHandler();
- 配置
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.tsimport { CollectionConfig } from 'payload/types';export const Users: CollectionConfig = { slug: 'users', auth: true, fields: [ { // 定义地址 name: 'address', required: true, type: 'text', // 贴别留神不同的类型有不同的数据 https://payloadcms.com/docs/fields/text }, { name: 'points', hidden: true, defaultValue: 0, type: 'number', }, ], access: { read: () => true, delete: () => false, create: ({ data, id, req }) => { // 设置管理系统不能增加 return !req.headers.referer?.includes('/admin'); }, update: ({ data, id, req }) => { // 设置管理系统不能增加 return !req.headers.referer?.includes('/admin'); }, },};
还须要更改 payload.config.ts
中配置
import { Users } from './collections/Users';// ...collections: [Users],admin: { user: 'users', // @see https://payloadcms.com/docs/admin/overview#the-admin-user-collection // ...},// ...
- 新增在创立一个积分记录汇合。
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 helpersexport 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 APIexport 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.tsimport 'dotenv/config';import express from 'express';import chalk from 'chalk';const port = Number(process.env.PORT) || 3000;const app = express();const nextApp = next({ dev: process.env.NODE_ENV !== 'production', port: PORT,});const nextHandler = nextApp.getRequestHandler();const start = async () => { // 筹备生成 .next 文件 nextApp.prepare().then(() => { app.all('*', (req, res) => { return nextHandler(req, res); }); app.listen(port, () => { console.log( '\x1b[36m%s\x1b[0m', `> Ready on http://localhost:${port}` ); }); });};start();
</details>
// package.json// ...// 这里须要应用 esno 而不能应用 node. 因为 node 是 CommonJs 而咱们代码中应用 es 标准"dev": "esno src/server/index.ts"// ...
配置 payload cms
集体了解 payload 和 cms 是两个货色,只是应用 payload 时主动应用了 cms, 如果不应用 cms 的话就不论。
payload 次要是操作数据库数据的,也有一些集成
相干依赖
@payloadcms/bundler-webpack
@payloadcms/db-mongodb
@payloadcms/richtext-slate
payload
开始前先抽离nextApp
nextHandler
函数,server
文件夹新建next-utils.ts
import next from 'next';const PORT = Number(process.env.PORT) || 3000;// 创立 Next.js 利用实例export const nextApp = next({ dev: process.env.NODE_ENV !== 'production', port: PORT,});// 获取 Next.js 申请处理器。用于解决传入的 HTTP 申请,并依据 Next.js 利用的路由来响应这些申请。export const nextRequestHandler = nextApp.getRequestHandler();
- 配置
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.tsimport { CollectionConfig } from 'payload/types';export const Users: CollectionConfig = { slug: 'users', auth: true, fields: [ { // 定义地址 name: 'address', required: true, type: 'text', // 贴别留神不同的类型有不同的数据 https://payloadcms.com/docs/fields/text }, { name: 'points', hidden: true, defaultValue: 0, type: 'number', }, ], access: { read: () => true, delete: () => false, create: ({ data, id, req }) => { // 设置管理系统不能增加 return !req.headers.referer?.includes('/admin'); }, update: ({ data, id, req }) => { // 设置管理系统不能增加 return !req.headers.referer?.includes('/admin'); }, },};
还须要更改 payload.config.ts
中配置
import { Users } from './collections/Users';// ...collections: [Users],admin: { user: 'users', // @see https://payloadcms.com/docs/admin/overview#the-admin-user-collection // ...},// ...
- 新增在创立一个积分记录汇合。
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 helpersexport 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 APIexport 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_IDNEXT_PUBLIC_PUSHER_APP_KEYPUSHER_APP_SECRETNEXT_PUBLIC_PUSHER_APP_CLUSTER