CloudFlare 运营着一个庞大的全球网络,在全球拥有超过 330 个数据中心。这种广泛的覆盖范围使得其服务距离大多数互联网用户仅有毫秒之遥。除了其基础的内容分发网络(CDN)和 DDoS 保护、Web 应用程序防火墙(WAF)等安全服务外,CloudFlare 还显著扩展了其“开发者服务”。这些服务现在包括无服务器计算(Workers)、对象存储(R2)、键值存储(KV)和无服务器 SQL 数据库(D1),以及针对 AI(AI Gateway、Workers AI)、图像和实时应用程序的专业服务。选择 CloudFlare 作为一站式开发平台,其优势在于性能、安全性、可拓展性、成本效益和开发体验的全面提升。

所涉及技术栈

  • Workers:构建无服务器应用程序并立即在全球范围内部署,以实现卓越的性能、可靠性和规模。
  • Pages:创建可立即部署到 CloudFlare 全球网络的全栈应用程序。
  • Wrangler:CloudFlare 开发平台的 CLI 工具。
  • D1:CloudFlare 原生的无服务数据库(SQLite)。
  • R2:CloudFlare 的对象存储。
  • KV:创建一个全局的、低延迟的键值数据存储。
  • Workers AI:在 CloudFlare 的全球网络上运行由无服务器 GPU 提供支持的机器学习模型。
  • Bun:新的 JavaScript 运行时,内置原生捆绑器、转译器、任务运行器和 npm 客户端。
  • Hono:构建于 Web 标准的快速、轻量的 Web 应用框架。支持任意 JavaScript 运行时。
  • Drizzle ORM:注重开发体验的轻量、高性能 TypeScript ORM。

Bun

基础

  • 安装 Bun:curl -fsSL https://bun.sh/install | bash
  • 更新 Bun:bun upgrade
  • 初始化空白项目:bun init [-y]
    • 添加 TypeScript 类型支持:bun add -d @types/bun
  • 基于模板创建项目:bun create <template> [<destination>]
    • 基于 create-template 格式的 npm 包、GitHub 仓库或者本地模版创建新项目;
    • 也可以使用 bunx create-template 创建,bunx 等同于 npx,如:
    1
    2
    bun create remix
    bunx create-remix
  • 执行文件/指令:bun [--watch] run [--bun] <filename.[js|jst|ts|tsx] | script-defined-in-package-dot-json>
    • --watch:以观察模式运行文件或指令,必须 紧跟在 bun 后面;
    • --bun:指定以 bun 而不是 node 作为可执行文件(`#!/usr/bin/env node`)的运行时环境;
    • 每个在 package.json 文件中的 scripts 部分定义的指令(如:"scripts":{"dev": "bun server.ts"}),都会有两个对应的生命周期钩子(格式:pre<script>post<script>):predevpostdev,如果 predev 执行失败,Bun 不会继续执行 dev 指令。
  • 编译为可执行的单文件程序: bun build ./cli.ts —compile —outfile mycli && ./mycli
  • 自动安装:如果Bun 没在工作目录或者更高层级目录中找到 node_modules 目录,Bun 将使用其模块解决方案算法。
  • Bun 的行为配置文件 bunfig.toml,通常与项目的 package.json 文件同目录或者应用于全局的配置文件($HOME/.bunfig.toml)。

    bunfig.toml 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    # Reduce memory usage at the cost of performance
    smol = true

    # Set the log level. "debug" | "warn" | "error"
    logLevel = "debug"

    # permit to enable/disable the analytics records
    telemetry = false

    # Test runner
    [test]
    root = "./__tests__" # The root directory to run tests from. Default `.`
    preload = ["./setup.ts"] # Same as the top-level `preload` field, but only applies to `bun test`
    smol = true
    coverage = false # Enables coverage reporting. Default `false`. Use `--coverage` to override.
    coverageThreshold = 0.9 # to require 90% line-level and function-level coverage
    # coverageThreshold = { line = 0.7, function = 0.8, statement = 0.9 } # Different thresholds can be specified for line-wise, function-wise, and statement-wise coverage.
    coverageSkipTestFiles = false # Whether to skip test files when computing coverage statistics. Default `false`

    # Package manager: config the `bun install` behavior
    [install]
    optional = true # Whether to install optional dependencies, default `true`
    dev = true # Whether to install development dependencies. Default `true`
    peer = true # Whether to install peer dependencies. Default `true`
    production = false # Whether `bun install` will run in "production mode". Default `false`. In production mode, `"devDependencies"` are not installed. Override with `--production`
    exact = false # Whether to set an exact version in package.json. Default `false`.
    # The default registry is https://registry.npmjs.org/. This can be globally configured in bunfig.toml
    registry = "https://registry.npmjs.org" # set default registry as a string
    registry = { url = "https://registry.npmjs.org", token = "123456" } # set a token
    registry = "https://username:password@registry.npmjs.org" # set a username/password

    # To configure a registry for a particular scope (e.g. `@myorg/<package>`) use `install.scopes`. You can reference environment variables with `$variable` notation.
    [install.scopes]
    # registry as string
    myorg = "https://username:password@registry.myorg.com/"

    # registry with username/password
    # you can reference environment variables
    myorg = { username = "myusername", password = "$npm_password", url = "https://registry.myorg.com/" }

    # registry with token
    myorg = { token = "$npm_token", url = "https://registry.myorg.com/" }


    [install.cache]
    # the directory to use for the cache
    dir = "~/.bun/install/cache"

    # when true, don't load from the global cache.
    # Bun may still write to node_modules/.cache
    disable = false

    # when true, always resolve the latest versions from the registry
    disableManifest = false

包管理器

  • 安装项目依赖:bun i
  • 安装全局依赖:bun i -g <package>
  • 添加项目依赖:bun add -d|--dev|--optional|-E|--exact|-g|--global <package-name | git@github.com:moment/moment.git>
    • -d | --dev:添加开发依赖
    • --optional:添加可选依赖
    • -E | --exact:固定依赖的具体版本
    • -g | --global:天津全局依赖,不会修改当前项目的 **package.json** 文件,与 `bun i -g` 相同
  • 移除依赖:bun remove <package>
  • 更新依赖:
    • 更新某个或者所有依赖:bun update [package]
    • 更新到最新版:bun update --latest
  • 清空 Bun 的全局模块缓存:bun pm cache rm
  • 列出所有全局安装的依赖:bun pm ls -g [--all]
  • 打包bun build ./index.tsx --outdir ./build

工作空间

Bun 支持在 package.json 中通过 workspaces 键配置工作空间。允许通过同一份代码管理多个独立的包。

示例目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tree
<root>
├── README.md
├── bun.lockb
├── package.json
├── tsconfig.json
└── packages
   ├── pkg-a
   │   ├── index.ts
   │   ├── package.json
   │   └── tsconfig.json
   ├── pkg-b
   │   ├── index.ts
   │   ├── package.json
   │   └── tsconfig.json
  ├── pkg-c
   │   ├── index.ts
   │   ├── package.json
   │   └── tsconfig.json
   └── pkg-d
   ├── index.ts
   ├── package.json
   └── tsconfig.json

根目录的package.json

1
2
3
4
5
6
7
8
9
10
11
{
"name": "my-project",
"version": "1.0.0",
"workspaces": ["packages/*"],
"devDependencies": {
"pck-a": "workspace:*",
"pck-b": "workspace:^",
"pck-c": "workspace:~",
"pck-d": "workspace:1.0.0"
}
}

在执行 bun i 时,会安装根项目以及全部的工作空间下的单体仓库的依赖,遇到重复的依赖,则会将其移至根目录的 node_modules 下。

如果只想安装工作空间下的某个单体仓库的依赖,可以使用 --filter 进行过滤:

1
2
3
4
5
# 安装除了 `pkg-c` 外的其他以 `pkg-` 开头的单体仓库的依赖
bun install --filter "pkg-*" --filter "!pkg-c"

# 也可以使用路径方式指明
bun install --filter "./packages/pkg-*" --filter "!pkg-c" # or --filter "!./packages/pkg-c"

测试

Bun 附带一个快速、内置、兼容 Jest 的测试运行器。测试使用 Bun 运行时执行,并支持以下功能。

  • TypeScript 和 JSX
  • 生命周期钩子
  • 快照测试
  • UI 和 DOM 测试
  • 使用 --watch 实现监视模式
  • 使用 --preload 实现脚本预加载

运行测试代码:bun test。递归地搜索工作目录下匹配模式的文件:

  • *.test.{js|jsx|ts|tsx}
  • *_test.{js|jsx|ts|tsx}
  • *.spec.{js|jsx|ts|tsx}
  • *_spec.{js|jsx|ts|tsx}

示例代码main.test.ts

1
2
3
4
5
import { expect, test } from "bun:test";

test("2 + 2", () => {
expect(2 + 2).toBe(4);
});

CI/CD

Hono

基础

  • 初始化项目:bunx create-hono my-app
  • 安装依赖:bun i
  • 启动开发模式:bun dev
  • 部署应用:bun deploy

Hello World

1
2
3
4
5
6
import { Hono } from 'hono'
const app = new Hono()

app.get('/', (c) => c.text('Hello Cloudflare Workers!'))

export default app

Hono 获取 Cloudflare Workers 的 KV、R2、D1 等绑定对象,只能通过上下文环境变量(c.env)获取(相关的绑定需要提前在 wrangler.toml 中配置):

示例index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Hono } from 'hono'

type Bindings = {
My_BUCKET: R2Bucket
USERNAME: string
PASSWORD: string
}

const app = new Hono<{Bindings: Bindings}>()
// 获取环境变量值
app.put('/upload/:key', async (c, next) => {
const key = c.req.param('key')
await c.env.MY_BUCKET.put(key, c.req.body)
return c.text(`Put ${key} successfully!`)
})

API

应用初始化选项

1
2
3
4
5
6
7
8
9
10
import { RegExpRouter } from 'hono/router/reg-exp-router'
type ENV = {
Bindings?: Record<string, unknown> // 环境绑定
Variables?: Record<string, unknown> // 请求上下文变量,可以通过 `c.set(key, value)`、`c.get(key)`来设置获取
}
const app = new Hono<Env>({
strict: false,
router: new RegExpRouter(),
getPath: (req) => ""
})

App

  • app.HTTP_METHOD([path,] handler | middleware…):注册路由

  • app.basePath(path):统一设置路由前缀,如:/api

  • app.notFound(handler):自定义 404 响应

  • app.onError(err, handler):全局异常处理

  • app.mount(path, anotherApp):挂载其他应用(含其它 Web 标准编写的框架)到 Hono 应用上

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { Router as IttyRouter } from 'itty-router'
    import { Hono } from 'hono'

    // 创建 itty-router 应用
    const ittyRouter = IttyRouter()
    ittyRouter.get('/hello', () => new Response('Hello from itty-router'))

    // Hono 应用
    const app = new Hono()
    app.mount('/itty-router', ittyRouter.handle)

Routing

  • 基础路由配置:

    • app.get(path, handler)app.post(path, handler)app.put(path, handler)app.delete(path, handler)
    • 包含 * 的通配符:app.get('/wild/*/card', handler)
    • 任意 HTTP 方法:app.all(path, handler)
    • 自定义 HTTP 方法:app.on('PURGE', path, handler)
    • 同一路径配置多个请求方法(也可以使用链式路由):app.on(['PUT', 'DELETE'], path, handler)
    • 多路径同方法:app.on(METHOD, ['/hello', '/ja/hello'], handler)
  • 路由参数:

    • 定义:/:参数名?,冒号为参数的定义符号,参数后的问号标识该参数为可选参数,可选参数只能放在最末尾,否则问号会被当成参数名的一部分:
      可选参数的错误定义
      可选参数的错误定义
    • 正则表达式:app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => { const { date, title } = c.req.param() })
    • 包含斜线:app.get('/posts/:fileame{.+\\.png$}', handler)
    • 链式路由:app.get(path, handler).post(handler).delete(handler)
    • 分组路由:实例化多个 Hono,每个实例设置好路由后再通过 route 方法挂载到主实例上。

      示例代码index.ts

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      import { Hono } from 'hono'

      const book = new Hono()
      book.get('/book', (c) => c.text('List Books')) // GET /book
      book.post('/book', (c) => c.text('Create Book')) // POST /book

      const user = new Hono().basePath('/user')
      user.get('/', (c) => c.text('List Users')) // GET /user
      user.post('/', (c) => c.text('Create User')) // POST /user

      const app = new Hono()
      app.route('/', book) // Handle /book
      app.route('/', user) // Handle /user

      export default app
  • 路由优先级

    • 路由处理器、中间件会按照注册顺序执行;
    • 如果中间件需要应用于某些路由,则应将该中间件注册顺序排到所有这些路由前;
    • 使用通配路由(app.get('*', handler))作为兜底路由,并将其放在所有路由最末尾。

Context

上下文(Context)用于处理请求(req: HonoRequest)、响应(res: Response)对象。

  • Context.body(data, [status, headers]):返回 HTTP 响应。
  • Context.header(name, value [, options: {append?: boolean}]):设置响应头。
  • Context.status(code):设置响应状态码。
  • Context.text(data, [status, headers]):以 Content-Type: text/plain 返回文本。
  • Context.json(data, [status, headers]):以 Content-Type: application/json 返回数据。
  • Context.html(data, [status, headers]):以 Content-Type: text/html 返回数据。
  • Context.notFound():返回状态为 404 到响应。
  • Context.redirect(path, [statusCode]):重定向,默认状态码为 302(资源已找到,但暂时移动到重定向路径上),可设置 301(永久)、303(See Other)、307(临时)。
  • HonoRequest.param([key]):获取所有路由参数对象,指定键名则返回对应值。
  • HonoRequest.query([key]):解析获取所有查询参数,指定键名则返回对应值。
  • HonoRequest.queries([key]):返回到查询参数为数组,相同键名多个值。
  • HonoRequest.header([key]):解析返回请求头对象,指定键名则返回对应值。
  • HonoRequest.parseBody():解析请求体类型为multipart/form-data 或者 application/x-www-form-urlencoded 的数据。

    解析获取上传的多文件对象时,键名需带上 [] 后缀:(await c.req.parseBody())['files[]']

  • HonoRequest.json():解析请求体类型为 application/json 的数据。
  • HonoRequest.text():解析请求体类型为 text/plain 的数据。
  • HonoRequest.blob():将请求体数据转换成 Blob 对象。
  • HonoRequest.formData():将请求体数据转换成 FormData 对象。
  • HonoRequest.url:获取请求的 URL,包含协议、域名、端口、路径。
  • HonoRequest.raw:返回 Web 标准的 Request 对象。如果是 Cloudflare 运行环境,可以通过 c.req.raw.cf 获取到相关数据。

异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { HTTPException } from 'hono/http-exception'

// ...
// 在中间件中直接抛出异常
app.post('/auth', async (c, next) => {
// authentication
if (authorized === false) {
throw new HTTPException(401, { message: 'Custom error message' })
}
await next()
})

// 返回异常引起的原因
app.post('/auth', async (c, next) => {
try {
authorize(c)
} catch (e) {
throw new HTTPException(401, { message, cause: e })
}
await next()
})

中间件

中间件作用于路由处理器之前或者之后。我们可以在请求(Request)派发之前获取到它,也可以在响应(Response)派发之后操作它。

中间件与路由处理器的区别:

  • 路由处理器:必须返回 Response 对象,且只有一个处理器被调用。
  • 中间件:无返回,需执行 await next() 处理下一个中间件。

用法

1
2
3
4
5
6
// 匹配所有 HTTP 请求方法、所有路由
app.use(logger())
// 作用于指定路径
app.use('/posts/*', cors())
// 作用于指定方法和路径
app.post('/posts/*', basicAuth())

自定义中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.use('/message/*', async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
c.header('x-message', 'This is middleware')
})

// 使用工厂模式创建中间件,常用于单独模块编写中间件且保持类型注解不丢失。

import { createMiddleware } from 'hono/factory'

const logger = createMiddleware<{Bindings: {}}>(async (c, next) => {
console.log(`[${c.req.method}] ${c.req.url}`)
await next()
c.header('x-message', 'This is middleware')
})

一些有用的中间件

Basic Authentication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 import { Hono } from 'hono'
import { basicAuth } from 'hono/basic-auth'
import { users } from './users'

const app = new Hono()

// 简单配置,单个或多个用户
app.use('/auth/*', basicAuth({
username: 'someone',
password: 'a-strong-password'
}, ...users))

// 自定义校验方法
app.use(basicAuth({
verifyUser: (username, password, c) => {
return username === 'dynamic-user' && password === 'strong-pwd'
}
}))

// 自定义密码校验哈希方法
// const credential = Buffer.from(username + ':' + password).toString('base64')
// const req = new Request('http://localhost/auth/a')
// req.headers.set('Authorization', `Basic ${credential}`)
app.use('/auth/*', basicAuth({
username: username,
password: password,
hashFunction: (data: string) => SHA256(data).toString(),
}))

CORS(跨域)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('/api/*', cors())
app.use(
'/api2/*',
cors({
origin: 'http://example.com', // 单一来源
origin: ['http://example.com', 'http://example2.com'], // 多个来源
origin: (origin, c) => { // 通过函数返回结果
return origin.endsWith('.example.com') ? origin : 'http://example.com'
}
allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'],
allowMethods: ['POST', 'GET', 'OPTIONS'],
exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
maxAge: 600,
credentials: true,
})
)

app.all('/api/abc', (c) => {
return c.json({ success: true })
})
app.all('/api2/abc', (c) => {
return c.json({ success: true })
})

JWT

如果未配置 cookie 字段,该 JWT 中间件将会使用请求头的 Authorization 项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import type { JwtVariables } from 'hono/jwt'

// 提供变量类型可提供类型推导 `c.get('jwtPayload')`:
type Variables = JwtVariables

const app = new Hono<{ Variables: Variables }>()

app.use('/private/*', jwt({secret: 'hard-code-secret'}))

// 如果从环境变量中获取 secret,则须额外封装因为 jwt 本身就是中间件。
app.use('/auth/*', (c, next) => {
const jwtMiddleware = jwt({
secret: c.env.JWT_SECRET, // 必填参数
cookie: undefined,
alg: 'HS256'
})
return jwtMiddleware(c, next)
})

Logger

1
2
3
4
5
6
7
8
9
10
11
12
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const app = new Hono()

export const customLogger = (message: string, ...rest: string[]) => {
console.log(message, ...rest)
}

app.use(logger(customLogger)) // 自定义打印函数

app.use(logger())
app.get('/', (c) => c.text('Hello Hono!'))

Secure Headers Middleware

简化了安全 headers 的设置过程,允许您控制特定安全 headers 的启用和禁用。参考

1
2
3
4
import { Hono } from 'hono'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
app.use(secureHeaders())

Hono Rate Limiter

- 安装依赖:
1
bun add hono-rate-limiter
- Demo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.ts
import { rateLimiter } from "hono-rate-limiter";
import { WorkersKVStore } from "@hono-rate-limiter/cloudflare";
import { Context, Next } from "hono";
import { KVNamespace } from "cloudflare:worker";

// Add this in Hono app
interface Env {
CACHE: KVNamespace;
// ... other binding types
}

const limiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: "draft-6", // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "", // Method to generate custom identifiers for clients.
// store: ... , // Redis, MemoryStore, etc. See below.
store: new WorkersKVStore({ namespace: c.env.CACHE }), // Here CACHE is your WorkersKV Binding.
});

// Apply the rate limiting middleware to all requests.
app.use(limiter);
- `wrangler.toml` 配置:
1
2
3
4
# wrangler.toml
[[kv_namespaces]]
binding = "CACHE"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
`keyGenerator`函数决定如何限制请求,它应该代表用户或用户类的唯一特征,用于实施速率限制。好的选择包括授权头中的

API 密钥、URL 路径或路由、应用程序使用的特定查询参数和/或用户 ID。

不建议使用IP地址(因为在许多有效情况下,许多用户可以共享这些地址)或位置(同上),因为您可能会发现自己意外地限制了比您预想的更广泛的用户群体。

Zod OpenAPI

Zod OpenAPI Hono 是扩展了 Hono 的一个类,它支持 OpenAPI 协议。使用它可以结合 Zod 来验证值和类型,并生成 OpenAPI 的 Swagger 文档。
安装:

1
bun add zod @hono/zod-openapi @scalar/hono-api-reference

参考:


Drizzle ORM

基础

  • 安装: bun add drizzle-orm && bun add -d drizzle-kit

    • 对应的数据库驱动器:
      • Neon Postgres: bun add @neondatabase/serverless
      • Xata: bun add @xata.io/client
      • Vercel Postgres: bun add @vercel/postgres
      • Turso:bun add @libsql/client
      • Cloudflare D1
  • 配置文件:项目根目录下新建的 drizzle.config.[ts|js|json] 文件。

    示例代码drizzle.config.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
     import { defineConfig } from 'drizzle-kit'

    export default defineConfig({
    dialect: "sqlite", // "mysql" | "sqlite" | "postgresql"
    schema: "./src/schema.ts", // "./src/**/schema.ts" | ["./src/user/schema.ts", "./src/posts/schema.ts"] | "./src/schema/*"
    out: "./drizzle",

    // https://orm.drizzle.team/kit-docs/config-reference#driver
    // driver: 'd1-http', // 'aws-data-api' | 'd1-http' | 'expo' | 'turso'
    dbCredentials: {
    url: process.env.DB_URL,
    // or
    //user: "postgres",
    //password: process.env.DATABASE_PASSWORD,
    //host: "127.0.0.1",
    //port: 5432,
    //database: "db",

    // for Cloudlfare D1
    //accountId: "",
    //databaseId: "",
    //token: "",
    },

    // multi-project schema, multi-project in one database
    // `const pgTable = pgTableCreator((name) => `project1_${name}`);`
    tablesFilter: ["project1_*"],
    })
  • Drizzle Kit 操作命令:

    • 生成数据库 migrations:drizzle-kit generate
    • 应用 migrations:drizzle-kit migrate
    • 同步 schema 到数据库:drizzle-kit push
    • 删除上一次生成的 migrations:drizzle-kit drop
    • 多人协作时,检查 schema 的冲突:drizzle-kit check
    • 打开浏览器进行可视化操作:drizzle-kit studio

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    import { sql } from "drizzle-orm";
    import { text, integer, sqliteTable } from "drizzle-orm/sqlite-core";

    const users = sqliteTable('users', {
    id: text('id'),
    textModifiers: text('text_modifiers').notNull().default(sql`CURRENT_TIMESTAMP`),
    intModifiers: integer('int_modifiers', { mode: 'boolean' }).notNull().default(false),
    });

数据类型(以 SQLite 为例)

基于官方的 SQLite 文档,每个存储在 SQLite 数据库中(或由数据库引擎操作)的值都具有以下一种存储类型:NULLINTEGERREALTEXTBLOB

Drizzle ORM 原生支持所有这些类型,并可以自由创建自定义类型。

  • Integer:有符号整型,根据数值大小,使用 0、1、2、3、4、6 或 8 个字节存储。
    • integer('id') :默认模式整型。
    • integer('count', {mode: 'number' | 'boolean' | 'timestamp' | 'timestamp_ms'}):自定义整型模式,调整其字节存储。
  • Real:8字节 IEEE 浮点数,real('interest')
  • Text:文本字符串,使用数据库编码存储(UTF-8、UTF-16BE 或 UTF-16LE)。
    • text('title')
    • text('json', {mode: 'json'}).$type<{foo: string}>():text 数据类型设置为 JSON 类型,并通过 $type<T>() 提供类型推导。
    • text('status', { enum: ["value1", "value2"] }):设置为枚举类型,类型为:"value1" | "value2" | null。只提供类型推导,不会再运行时进行校验。
  • Blob:二进制数据类型,输入什么存储什么。
    • blob('image')
    • blob('blob', { mode: 'buffer' | 'bigint' | 'json' })
  • Boolean:SQLite 不支持原生 boolean 类型,可以通过设置 integermodeboolean 来实现,值为 01
    • integer('complete', { mode: 'boolean' })
  • Bigint:SQLite 的数据类型中没有 bigint,但可以通过设置 blobmodebigint 来实现。
  • 自定义数据类型:可以通过 .$type() 来实现自定义列数据类型。

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     type UserId = number & {__brand: 'user_id'}
    type Data = {
    foo: string;
    bar: number
    }

    const users = sqliteTable('users', {
    id: integer('id').$type<UserId>().primaryKey(),
    jsonField: blob('json_field').$type<Data>(),
    })

索引/约束

SQL 索引是作用于数据库列的规则,防止无效数据插入数据库中。

  • Not Null - 非空约束:.notNull() ,即数据结果不能为空 NULL

  • Default - 默认值:所有数据类型的默认值都是 NULL,可以通过 .default() 或者 .$defaultFn(),也可以通过 .$onUpdate(() => null) / .$onUpdateFn() 来更新默认值。

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { sql } from 'drizzle-orm'
    import { text, sqliteTable } from 'drizzle-orm/sqlite-core'
    import { createId } from '@paralleldrive/cuid2';

    const table = sqliteTable("table", {
    id: text('id').$defaultFn(() => createId()),
    time: text("time").default(sql`(CURRENT_TIME)`), // 默认值中的一些特殊关键词:当前时间
    date: text("date").default(sql`(CURRENT_DATE)`), // 当前日前
    timestamp: text("timestamp").default(sql`(CURRENT_TIMESTAMP)`), // 当前时间戳
    updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`).$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
    });
  • Unique - 唯一约束:确保列中的所有值都不同,主键拥有同样的效果,但是一张表只能有一个主键,唯一约束却可以有多个。

    • int('id').unique()
    • int('id').unique('custom_name') :自定义唯一约束名。
    • sqliteTable('composite', { id: int('id'), name: text('name') }, (table) => { unq: unique().on(table.id, table.name) }) :组合键唯一约束。
  • Check:限制值在某个范围内,Drizzle ORM 暂未实现。

  • Primary Key - 主键:主键具有唯一非空性,一张表中只能有一个主键,主键可以包含一个或多个字段(column)

    • integer('id').primaryKey()
    • integer('id').primaryKey({ autoIncrement: true }) :自增主键
    • 组合主键:

      示例代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      import { integer, text, primaryKey, sqliteTable} from "drizzle-orm/sqlite-core";

      export const user = sqliteTable("user", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      name: text("name"),
      });

      export const book = sqliteTable("book", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      name: text("name"),
      });

      export const bookToAuthor = sqliteTable("book_to_author", {
      authorId: integer("author_id"),
      bookId: integer("book_id"),
      }, (table) => {
      return {
      pk: primaryKey({ columns: [table.bookId, table.authorId] }),
      pkWithCustomName: primaryKey({ name: 'custom_name', columns: [table.bookId, table.authorId] }),
      };
      });

  • Foreign Key - 外键:外键约束通常用于防止破坏表间的连接操作。外键可以是一张表中的一个或多个字段指向其它表的主键。包含外键的表叫子表,包含指向对应主键的表叫父表 / 指向表。

    • .references(() => table.pk) :指向主键字段。

      示例代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      import { integer, text, sqliteTable } from "drizzle-orm/sqlite-core";
      export const user = sqliteTable("user", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      name: text("name"),
      });
      export const book = sqliteTable("book", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      name: text("name"),
      authorId: integer("author_id").references(() => user.id)
      });

      // 自引用
      import { integer, text, foreignKey, sqliteTable, AnySQLiteColumn } from "drizzle-orm/sqlite-core";
      export const user = sqliteTable("user", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      name: text("name"),
      parentId: integer("parent_id").references((): AnySQLiteColumn => user.id)
      });
      //or
      export const user = sqliteTable("user", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      name: text("name"),
      parentId: integer("parent_id"),
      }, (table) => {
      return {
      parentReference: foreignKey({
      columns: [table.parentId],
      foreignColumns: [table.id],
      name: "custom_fk"
      }),
      };
      });

      // 组合外键
      import { integer, text, primaryKey, foreignKey, sqliteTable, AnySQLiteColumn } from "drizzle-orm/sqlite-core";
      export const user = sqliteTable("user", {
      firstName: text("firstName"),
      lastName: text("lastName"),
      }, (table) => {
      return {
      pk: primaryKey({ columns: [table.firstName, table.lastName]}),
      };
      });
      export const profile = sqliteTable("profile", {
      id: integer("id").primaryKey({ autoIncrement: true }),
      userFirstName: text("user_first_name"),
      userLastName: text("user_last_name"),
      }, (table) => {
      return {
      userReference: foreignKey(() => ({
      columns: [table.userFirstName, table.userLastName],
      foreignColumns: [user.firstName, user.lastName],
      name: "custom_name"
      }))
      }
      });
  • Indexes - 索引:Drizzle ORM 提供 indexunique index 声明。

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { integer, text, index, uniqueIndex, sqliteTable } from "drizzle-orm/sqlite-core";
    export const user = sqliteTable("user", {
    id: integer("id").primaryKey({ autoIncrement: true }),
    name: text("name"),
    email: text("email"),
    }, (table) => {
    return {
    nameIdx: index("name_idx").on(table.name),
    emailIdx: uniqueIndex("email_idx").on(table.email),
    };
    });

视图

-- 摘自 Microsoft Learn

视图是一个虚拟表,其内容由查询定义。同表一样,视图包含一系列带有名称的列和行数据。视图在数据库中并不是以数据值存储集形式存在,除非是索引视图。 行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成。

对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其他数据库的一个或多个表,或者其他视图。

视图通常用来集中、简化和自定义每个用户对数据库的不同认识。 视图可用作安全机制,让用户通过视图访问数据,而不授予用户直接访问视图基础表的权限。 视图可用于提供向后兼容接口来模拟曾经存在但其架构已更改的表。

  • 声明视图:

    1
    2
    3
    4
    5
    import { sqliteView } from "drizzle-orm/sqlite-core";

    export const userView = sqliteView("user_view").as((qb) => qb.select().from(user));
    export const userView = sqliteView("user_view").as((qb) => qb.select({id: user.id, name: user.name}).from(user)); // 指定显示的字段
    export const customersView = sqliteView("customers_view").as((qb) => qb.select().from(user).where(eq(user.role, "customer"))); // 筛选
  • 声明已存在的视图:

    1
    2
    3
    4
    5
    export const trimmedUser = sqliteView("trimmed_user", {
    id: serial("id"),
    name: text("name"),
    email: text("email"),
    }).existing();

声明关系

一对一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'

// 自引用
export const users = sqliteTable('users', {
id: integer('id').primaryKey({autoIncrement: true}),
name: text('name'),
invitedBy: integer('invited_by'),
})

export const usersRelations = relations(users, ({one}) => ({
invitee: one(users, {
fields: [users.invitedBy],
references: [users.id]
})
}))

// 应用其他表
export const users = sqliteTable('users', {
id: integer('id').primaryKey({autoIncrement: true}),
name: text('name'),
invitedBy: integer('invited_by'),
})

export const usersRelations = relations(users, ({one}) => ({
profileInfo: one(profileInfo),
}))

export const profileInfo = sqliteTable('profile_info', {
id: integer('id').primaryKey({autoIncrement: true}),
userId: integer('user_id').references(() => users.id),
metadata: text('metadata', {mode: 'json'})
})

一对多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'

export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name')
})

export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}))

export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({autoIncrement: true}),
content: text('content'),
authorId: integer('author_id'),
})

export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id]
}),
comments: many(comments)
}))

export const comments = sqliteTable('comments', {
id: integer('id').primaryKey({autoIncrement: true}),
text: text('text'),
authorId: integer('author_id'),
postId: integer('post_id'),
})

export const commentsRelations = relations(comments, ({one}) => ({
post: one(posts, {
fields: [comments.postId],
references: [posts.id]
})
}))

多对多

多对多关系使用一张显式定义的中间表(junction/join table)连接相关联的表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { sqliteTable, integer, text, primaryKey } from 'drizzle-orm/sqlite-core'
import { relations } from 'drizzle-orm'

export const users = sqliteTable('users', {
id: integer('id').primaryKey({autoIncrement: true}),
name: text('name'),
})

export const usersRelations = relations(users, ({many}) => ({
usersToGroups: many(usersToGroups),
}))

export const groups = sqliteTable('groups', {
id: integer('id').primaryKey({autoIncrement: true}),
name: text('name'),
})

export const groupsRelations = relations(groups, ({many}) => ({
usersToGroups: many(usersToGroups),
}))

export const usersToGroups = sqliteTable('users_to_groups',
{
userId: integer('user_id').notNull().references(() => users.id),
groupId: integer('group_id').notNull().references(() => groups.id)
},
(table) => ({
pk: primaryKey({columns: [table.groupId, table.userId]})
})
)

export const usersToGroupsRelations = relations(usersToGroups, ({one}) => ({
group: one(groups, {
fields: [usersToGroups.groupId],
references: [groups.id]
}),
user: one(users, {
fields: [usersToGroups.userId],
references: [users.id],
}),
}))

数据操作

数据查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { Database } from 'bun:sqlite'
import * as schema from './schema'

const sqlite = new Database('sqlite.db')
const db = drizzle(sqlite, {schema})

// `[TableName]` 即为 schema 中定义的表名,支持 `findMany`、`findFirst`(limit=1)。
const result = db.query.[TableName].findMany({
// 包含或忽略数据查询结果中的某些字段
columns: {
id: true,
},
// 将来自多个相关表的的数据进行组合,并能适当地对查询结果进行聚合运算。
with: {
// [relations]: boolean | { [colName]: boolean }
},
// 对自定义字段进行检索并对其应用附加功能。
extras: (table, { sql }) => ({ nameLength: (sql<number>`length(${table.name})`).as('name_length') }),
// 过滤条件
where: (users, {eq}) => eq(users.id, 1),
offset: 1,
orderBy: (posts, {asc}) => [asc(posts.id)],
limit: 2,
})

数据插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { drizzle } from 'drizzle-orm/bun-sqlite'
import { Database } from 'bun:sqlite'
import * as schema from './schema'
import { sql } from 'drizzle-orm'

const sqlite = new Database('sqlite.db')
const db = drizzle(sqlite, {schema})

type NewUser = typeof schema.users.$inferInsert; // 定义创建 users 表数据所需要的字段类型,有对应的结果类型:`$inferSelect`
const insertUser = async (user: NewUser) => {
return db
.insert(schema.users)
.values(user) // 可以传入数组批量插入:.values([user1, user2])
.onConflictDoNothing() // 如果插入的数据产生冲突,则取消该次插入操作
// 如果插入的数据指定字段值发生冲突,则将就数据中的指定字段值更新为新值。
.onConflictDoUpdate({
// 指定发生冲突的字段,如果是复合索引、复合主键,则使用数组形式
target: schema.users.id,
// 过滤条件
targetWhere: sql`name <> 'New Name'`,
// 设置新值
set: {name: 'New Name'},
setWhere: sql`name <> 'New Name'`,
})
// 插入成功后返回新对象,如果指定字段则只返回指定的字段
.returning({userName: schema.users.name});
}

更新数据

1
2
3
4
await db.update(users)
.set({name: 'Someone'})
.where(eq(users.name, 'Dan'))
.returning();

删除数据

1
2
3
await db.delete(users)
.where(eq(users.name, 'Dan'))
.returning();

Wrangler

Wrangler 是开发 CloudFlare 相关应用的命令行工具。

新版 Wrangler 不再由 Rust 编写,而是使用 TypeScript 实现。

基础

  • 初始化项目:bunx create-cloudflare@latest
  • 根据绑定和配置模块规则生成类型(TypeScript):bun wrangler types
  • 本地开发:bun wrangler dev [--env dev] [--ip 0.0.0.0] [--port 12345]
  • 部署项目到 Cloudflare:bun wrangler deploy [--env prodction]
  • 查看登陆身份:bun wrangler whoami
  • 登陆 Cloudflare:bun wrangler login
  • 查看部署版本:bun wrangler deployments list
  • 版本回滚:bun wrangler rollback [<DEPLOYMENT_ID>]
  • Pages 相关命令:
    • 本地开发:bun wrangler pages dev [--local | --ip | --port | --binding | --kv | --r2 | --do | --live-reload | --compatibility-date]
    • 项目管理:bun wrangler pages project <list | create | delete>
    • 部署管理:bun wrangler pages deployment <list | tail>
    • 部署:bun wrangler pages deploy

配置文件

Wrangler 使用一个可选的 wrangler.toml 文件作为配置文件。

  • 只能作为顶层的键
    • keep_vars:可选,boolean 类型。部署时,是否保持控制面板中配置的变量。
    • send_metrics:可选,boolean 类型。是否将该项目的使用指标发送给 Cloudflare。
    • site: 建议使用 Cloudflare Pages,可选,对象。用于配置静态文件。
  • 可继承的键:配置于顶层的键,可被指定环境继承或覆盖。
    • name:必填,string 类型。Worker 名称。
    • main:必填,string 类型。执行 Worker 的入口文件。
    • compatibility_date:必填,string 类型,格式 yyyy-mm-dd。指定 Workers 运行时使用的版本。
    • account_id:可选,string 类型。
    • workers_dev:可选,boolean 类型,默认值 true。部署到 Cloudflare 时使用 *.worders.dev 子域名。
    • route / routes:可选,Route / Route[]类型。部署 Worker 到路由。
    • minify:可选,boolean 类型。上传时是否精简代码。
    • node_compat:可选,boolean 类型。为 Node.js 的内置模块、全局添加 polyfills。
    • logpush:可选,boolean 类型,默认值 false。是否为 Workers 启用 Workers Trace Events Logpush。
    • limits:可选,Limits 类型。
  • 非继承键:定义于顶层的键,不可被继承,只能在各个环境中配置。
    • define:可选,Record<string, string> 类型。可替换的值。
    • vars:可选,object 类型。一组环境变量。
    • durable_objects:绑定的 Durable Objects。
    • kv_namespaces:绑定的 KV 命名空间。
    • r2_buckets:绑定的 R2 桶。
    • vectorize:绑定的 Vectorize 索引。
    • services:绑定的服务。
    • tail_consumers:Worker 发送数据的目的地。

示例代码wrangler.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# Top-level configuration
name = "my-worker"
main = "src/index.js"
compatibility_date = "2024-07-12"

workers_dev = false
routes = [
"example.com/foo/*",
"example.com/bar/*"
]
vars = { ENVIRONMENT = "production" }

kv_namespaces = [
{ binding = "<MY_NAMESPACE>", id = "<KV_ID>" }
]

[dev]
ip = "192.168.1.1"
port = 8080
local_protocol = "http"

[vars]
API_HOST = "example.com"
API_ACCOUNT_ID = "example_user"
SERVICE_X_DATA = { URL = "service-x-api.dev.example", MY_ID = 123 }

[ai]
binding = "AI" # available in your Worker code on `env.AI`

[triggers]
crons = ["* * * * *"]

[limits]
cpu_ms = 100

[browser]
binding = "<BINDING_NAME>"

[env.staging]
name = "my-worker-staging"
vars = { ENVIRONMENT = "staging" }
route = { pattern = "staging.example.org/*", zone_name = "example.org" }

[[kv_namespaces]]
binding = "<MY_NAMESPACE>"
id = "<KV_ID>"

[[r2_buckets]]
binding = "<BINDING_NAME1>"
bucket_name = "<BUCKET_NAME1>"

[[r2_buckets]]
binding = "<BINDING_NAME2>"
bucket_name = "<BUCKET_NAME2>"


[env.dev]
vars = { ENVIRONMENT = "dev" }
route = "dev.example.com/*"
r2_buckets = [
{ binding = "<BINDING_NAME1>", bucket_name = "<BUCKET_NAME1>"},
{ binding = "<BINDING_NAME2>", bucket_name = "<BUCKET_NAME2>"}
]