自定義伺服器啟動
相關依賴
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
聯絡郵箱: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