Skip to main content

使用 CF 部署 t3 stack 应用

t3 stack 应用使用的组件:

  • tRPC
  • Typescript
  • Tailwind
  • Next.js
  • Prisma

要部署到 CF, 主要进行工程配置和代码上匹配:

  1. wrangler 配置
  2. nextjs 配置
  3. 数据库配置

下面分别说明上述三个内容.

针对 Next.js 项目集成 CF 支持

参考 CF 官方文档.

  1. 安装 next-on-pages 插件: npm install --save-dev @cloudflare/next-on-pages

  2. 配置 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;
    }
  3. 更新 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;
  4. 在所有 API route 或者使用 getServerSideProps 的页面最开头添加 export const runtime = "edge";.

  5. 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 数据库, 有两个阶段:

  1. 建立绑定
  2. 数据库迁移和访问代码编写

建立 D1 数据库绑定

  1. CF 后台新建数据库, 获得其 name 和 id.

  2. wrangler.toml 中添加名字和 id, 并自定义一个本地绑定名字:

    [[d1_databases]]
    binding = "DB"
    database_name = "cf-pages-next-app-db"
    database_id = "xxx-xxx"
  3. 更新 env.d.ts:

    interface CloudflareEnv {
    DB: D1Database;
    }

即可完成绑定配置.

Prisma 的使用

开发时, wrangler 会在本地创建一个 sqlite 数据库作为开发环境数据库使用. 生产环境下, 代码则会连接到 D1 真实数据库. 我们在开发过程中, 主要需要对 Prisma 进行对应配置和代码编写.

在项目中集成 Prisma

  1. 添加 Prisma 客户端生成所需的 cli 工具: yarn add prisma --dev, 之后可以通过此工具生成客户端.

  2. 添加 Prisma client 运行依赖: yarn add @prisma/client @prisma/adapter-d1 # prisma 连接器和客户端

  3. 可以使用这个命令生成初始 schema 结构: npx prisma init --datasource-provider sqlite

  4. 确认生成的 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")
    }
  5. 通过 schema 生成客户端代码: npx prisma generate (客户端代码在 node_modules/prisma 目录下), 此命令可自动查找 prisma/schema.prisma 文件.

  6. 在代码中使用客户端:

    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 数据库情况下, 迁移有两步操作:

  1. 生成迁移文件
  2. 将迁移应用到 D1 数据库

详细步骤如下:

  1. 生成初始迁移文件(此时文件为空): npx wrangler d1 migrations create __YOUR_DATABASE_NAME__ create_user_table, 执行后, 会在 migration 目录下生成一个 0001_create_user_table.sql 文件.

  2. 生成初始迁移文件内容: 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");
  3. 执行迁移: 本地开发迁移和远程迁移分别使用如下命令, 执行后, 即可将 schema 同步到本地开发/远程生产数据库中

    • 本地: npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --local
    • 远程: npx wrangler d1 migrations apply __YOUR_DATABASE_NAME__ --remote
  4. 下面再看第二次迁移, 首先在 schema 文件中添加另外一张表定义:

    model Post {
    id Int @id @default(autoincrement())
    title String
    author User @relation(fields: [authorId], references: [id])
    authorId Int
    }
  5. 创建下一个迁移文件: npx wrangler d1 migrations create __YOUR_DATABASE_NAME__ create_post_table

  6. 再生成迁移文件内容: 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
    );
  7. 再次将迁移应用到本地 & 远程:

    • 本地: npx wrangler d1 migrations apply YOUR_DATABASE_NAME --local
    • 远程: npx wrangler d1 migrations apply YOUR_DATABASE_NAME --remote

使用 prisma 进行 CRUD 的注意事项

再来回顾一下 prisma 在代码中的使用步骤:

  1. 执行 npx prisma generate 生成 prisma 客户端.

  2. 代码中使用即可:

    // 已创建的 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)

注意:

  1. 在 serverless 环境下同样需要保证一个实例, 同时可以考虑依赖注入, 参考这个链接:
    1. 主要思路是在 Env 中放入变量, 然后使用中间件去初始化连接器(如果没有的情况下)
    2. 或者也可以使用一个全局变量来处理 prisma 的客户端创建
    3. https://github.com/kristianfreeman/cloudflare-d1-prisma-honox-starter/blob/main/app/routes/_middleware.ts
  2. 原因:
    1. https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/instantiate-prisma-client#the-number-of-prismaclient-instances-matters
    2. https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/databases-connections#prismaclient-in-serverless-environments
  3. 最佳实践: https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices