使用 CF 部署 t3 stack 应用
t3 stack 应用使用的组件:
- tRPC
- Typescript
- Tailwind
- Next.js
- Prisma
要部署到 CF, 主要进行工程配置和代码上匹配:
- wrangler 配置
- nextjs 配置
- 数据库配置
下面分别说明上述三个内容.
针对 Next.js 项目集成 CF 支持
参考 CF 官方文档.
-
安装 next-on-pages 插件:
npm install --save-dev @cloudflare/next-on-pages
-
配置 wrangler(详见下方示例文件).
#:schema node_modules/wrangler/config-schema.json
name = "my-next-app"
compatibility_date = "2024-09-24"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
[[d1_databases]]
binding = "DB"
database_name = "cf-pages-next-app-db"
database_id = "xxx-xxx"
[[kv_namespaces]]
binding = "MY_KV_NAMESPACE"
id = "xxxx"对应上述配置, 使用 TS 时需要确认类型定义中有对应 binding:
interface CloudflareEnv {
MY_KV_NAMESPACE: KVNamespace;
DB: D1Database;
} -
更新 next.config 文件以便在本地开发时可以访问 bindings.
import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';
/** @type {import('next').NextConfig} */
const nextConfig = {};
if (process.env.NODE_ENV === 'development') {
await setupDevPlatform();
}
export default nextConfig; -
在所有 API route 或者使用
getServerSideProps
的页面最开头添加export const runtime = "edge";
. -
package.json 中添加如下内容:
"pages:build": "npx @cloudflare/next-on-pages",
"preview": "npm run pages:build && wrangler pages dev",
"deploy": "npm run pages:build && wrangler pages deploy",
"cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
D1 数据库的使用: Prisma + D1
D1 可以结合 Prisma 作为 ORM 使用, 因此同样适用于 t3 stack. 要在工程中使用 D1 数据库, 有两个阶段:
- 建立绑定
- 数据库迁移和访问代码编写
建立 D1 数据库绑定
-
CF 后台新建数据库, 获得其 name 和 id.
-
在
wrangler.toml
中添加名字和 id, 并自定义一个本地绑定名字:[[d1_databases]]
binding = "DB"
database_name = "cf-pages-next-app-db"
database_id = "xxx-xxx" -
更新
env.d.ts
:interface CloudflareEnv {
DB: D1Database;
}
即可完成绑定配置.
Prisma 的使用
开发时, wrangler 会在本地创建一个 sqlite 数据库作为开发环境数据库使用. 生产环境下, 代码则会连接到 D1 真实数据库. 我们在开发过程中, 主要需要对 Prisma 进行对应配置和代码编写.
在项目中集成 Prisma
-
添加 Prisma 客户端生成所需的 cli 工具:
yarn add prisma --dev
, 之后可以通过此工具生成客户端. -
添加 Prisma client 运行依赖:
yarn add @prisma/client @prisma/adapter-d1 # prisma 连接器和客户端
-
可以使用这个命令生成初始 schema 结构:
npx prisma init --datasource-provider sqlite
-
确认生成的 schema 类似如下
prisma/schema.prisma
配置:// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// prisma client 是自动生成的类型安全客户端
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
// 本地数据库配置(在 D1 上不是用的这个)
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
} -
通过 schema 生成客户端代码:
npx prisma generate
(客户端代码在node_modules/prisma
目录下), 此命令可自动查找prisma/schema.prisma
文件. -
在代码中使用客户端:
import { PrismaD1 } from '@prisma/adapter-d1'
import { PrismaClient } from '@prisma/client'
// 确保唯一的一个 prisma 实例, 通过下方中间件注入到每个请求处理过程中, 暂不提供全局变量访问.
let d1db: PrismaClient | undefined
/// 创建 prisma 客户端(每次请求都有全新的 context, 因此也就有了全新的 prisma, 这个方法无法保证客户端重用)
///
/// 因此需要结合全局变量去存储而非放到 ctx 中
///
/// https://hono.dev/docs/guides/middleware#extending-the-context-in-middleware
export const createPrismaClient = createMiddleware<AppEnv>(async (c, next) => {
console.log({ info: 'entered prisma client preparing middleware' })
if (!d1db) {
const adapter = new PrismaD1(c.env.DB)
d1db = new PrismaClient({ adapter })
console.log({ info: 'created' })
}
c.set('d1db', d1db) // 注入到 context 中
await next()
})由于上方将 client 注入到 context, 因此使用时, 按如下进行:
// 在 Hono 框架下的用法: 定义一个从 context 中取得 client 的函数
export function d1db(c: Context<AppEnv, string>): PrismaClient | undefined {
return c.get('d1db')
}
// ...
// 使用时比较简单:
const db = d1db(c)
if (!db) {
return c.text('internal error', 500)
}
// 若存在的 Email 则直接返回
if (await db.user.findFirst({where: { email } })) {
return c.text('user exists', 400)
}
// ...
数据迁移
要将 schema 写入数据库, 需要进行数据库迁移操作. 在 D1 数据库情况下, 迁移有两步操作:
- 生成迁移文件
- 将迁移应用到 D1 数据库
详细步骤如下:
-
生成初始迁移文件(此时文件为空):
npx wrangler d1 migrations create __YOUR_DATABASE_NAME__ create_user_table
, 执行后, 会在migration
目录下生成一个0001_create_user_table.sql
文件. -
生成初始迁移文件内容:
npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prisma --script --output migrations/0001_create_user_table.sql
其中针对首次迁移, 可以使用特殊的--from-empty
标志. 生成文件内容如下所示:-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -
执行迁移: 本地开发迁移和远程迁移分别使用如下命令, 执行后, 即可将 schema 同步到本地开发/远程生产数据库中
- 本地:
npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --local
- 远程:
npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --remote
- 本地:
-
下面再看第二次迁移, 首先在 schema 文件中添加另外一张表定义:
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
} -
创建下一个迁移文件:
npx wrangler d1 migrations create __YOUR_DATABASE_NAME__ create_post_table
-
再生成迁移文件内容:
migrations
目录下名为0002_create_post_table.sql
的 sql 文件:npx prisma migrate diff \
--from-local-d1 \
--to-schema-datamodel ./prisma/schema.prisma \
--script \
--output migrations/0002_create_post_table.sql可以看到这里使用的是
--from-local-d1
而非首次迁移使用的--from-empty
.此外可以看到, 生成的 sql 文件内容对应如下:
-- CreateTable
CREATE TABLE "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"authorId" INTEGER NOT NULL,
CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
); -
再次将迁移应用到本地 & 远程:
- 本地: npx wrangler d1 migrations apply YOUR_DATABASE_NAME --local
- 远程: npx wrangler d1 migrations apply YOUR_DATABASE_NAME --remote
使用 prisma 进行 CRUD 的注意事项
再来回顾一下 prisma 在代码中的使用步骤:
-
执行
npx prisma generate
生成 prisma 客户端. -
代码中使用即可:
// 已创建的 binding 中读取 DB 创建 adapter
const adapter = new PrismaD1(env.DB)
// 创建客户端
const prisma = new PrismaClient({ adapter })
// 使用客户端
const users = await prisma.user.findMany()
const result = JSON.stringify(users)
注意:
- 在 serverless 环境下同样需要保证一个实例, 同时可以考虑依赖注入, 参考这个链接:
- 主要思路是在 Env 中放入变量, 然后使用中间件去初始化连接器(如果没有的情况下)
- 或者也可以使用一个全局变量来处理 prisma 的客户端创建
- https://github.com/kristianfreeman/cloudflare-d1-prisma-honox-starter/blob/main/app/routes/_middleware.ts
- 原因:
- 最佳实践: https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices