
在 Cloudflare Workers 上用 fetch-router 与 D1 构建全栈应用从本地开发到生产部署实战指南【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix导读本指南以仓库中的 Cloudflare Workers 示例 为主线完整演示如何用remix-run/fetch-router在 Cloudflare Workers 边缘运行时上搭建一个以 D1 数据库为存储的「简易博客」应用。你将学会如何声明类型安全的路由表、如何组合中间件处理会话与表单、如何用 D1 的 Prepared Statement 读写数据以及如何通过 Wrangler 完成从本地迁移到云端部署的全流程最终在任意 Worker 环境中复用同一套 Fetch API 编程模型。一、示例概览一个跑在边缘的「全栈」博客该示例位于仓库的 packages/fetch-router/demos/cf-workers 目录是一个完全面向 Cloudflare Workers 运行时编写的应用路由由remix-run/fetch-router负责它是一个基于标准 WebFetch API与route-pattern构建的极简可组合路由器可在 Node.js、Bun、Deno、Cloudflare Workers 甚至浏览器中运行存储使用 Cloudflare D1基于 SQLite 的边缘数据库持久化博客文章交互具备登录/登出、发表文章、文章列表与详情页等完整 CRUD 流程渲染通过html-template的html模板标签与createHtmlResponse直接生成 HTML 响应无需额外前端框架。从目录结构可以看出它的组织思路入口 worker.ts 只负责把请求转交给路由器业务代码按「路由声明routes— 路由装配router— 数据访问data」三个文件拆分迁移脚本独立放在migrations/下cf-workers/ ├── app/ │ ├── data.ts # D1 数据访问层 │ ├── router.ts # 中间件与路由处理器装配 │ └── routes.ts # 类型安全的路由表声明 ├── migrations/ │ └── 0001_initial.sql ├── worker.ts # Worker 入口 ├── wrangler.jsonc # Wrangler 配置含 D1 绑定 ├── package.json └── tsconfig.json二、本地开发环境搭建原文档给出的三条命令即可完成从零到本地可访问的全过程下面结合仓库配置文件逐条说明其实际行为。1. 安装依赖pnpm install仓库使用 pnpm workspace 管理示例中的remix-run/*系列包fetch-router、cookie、data-schema、form-data-middleware、html-template、logger-middleware、response、session、session-middleware均以workspace:*版本关联安装时直接链接到本地仓库源码便于调试与跟进最新实现。开发依赖中唯一的运行时工具是wrangler^4.90.1见 package.json。2. 应用数据库迁移pnpm run db:migrate该命令实际执行wrangler d1 migrations apply fetch-router-blog针对名为fetch-router-blog的本地 D1 数据库应用 migrations/0001_initial.sql 中的迁移CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, author TEXT NOT NULL, created_at INTEGER NOT NULL ); -- Insert initial demo data INSERT INTO posts (title, content, author, created_at) VALUES (Welcome to the Blog, This is a simple blog demo built with fetch-router on Cloudflare Workers., Admin, strftime(%s, 2025-01-01)), (Getting Started with fetch-router, fetch-router is a minimal, composable router built on the web Fetch API., Admin, strftime(%s, 2025-01-02));迁移采用「版本化 SQL 文件」的方式0001_initial.sql既是建表语句也顺带插入了两条演示数据。注意created_at以 Unix 时间戳INTEGER存储与后文 data.ts 中的转换逻辑一一对应。3. 启动开发服务器pnpm run dev该命令执行wrangler dev --port 44100以本地模式启动 Worker并自动接入wrangler.jsonc中声明的 D1 绑定本地模式下 Wrangler 会创建本地 SQLite 实例。应用随即在http://localhost:44100上可用访问/可以看到迁移脚本预置的两篇文章点击Login输入任意用户名即可登录示例刻意不校验密码用于演示会话中间件登录后可发布新文章并在首页与详情页查看。三、配置解读Wrangler 与 D1 绑定wranger.jsonc 是理解「Worker 如何拿到数据库」的关键{ $schema: node_modules/wrangler/config-schema.json, name: fetch-router-cf-workers-demo, main: worker.ts, compatibility_date: 2025-11-18, observability: { enabled: true, }, d1_databases: [ { binding: DB, database_name: fetch-router-blog, database_id: local, migrations_dir: migrations, }, ], }逐项说明配置项值作用namefetch-router-cf-workers-demoWorker 名称wrangler deploy时作为部署标识mainworker.tsWorker 入口文件compatibility_date2025-11-18Wrangler 兼容性日期决定运行时行为集合observability.enabledtrue开启可观测性日志与追踪d1_databases[0].bindingDBD1 数据库在Env中的绑定名代码里通过env.DB访问d1_databases[0].database_namefetch-router-blogD1 数据库名称d1_databases[0].database_idlocal部署前的占位值生产环境必须替换为wrangler d1 create返回的真实 IDd1_databases[0].migrations_dirmigrations迁移脚本目录db:migrate默认读取此处绑定声明好之后运行pnpm run typegen即wrangler types会生成 worker-configuration.d.ts其中定义了interface Env { DB: D1Database; }也就是说DB是D1Database 类型的强类型绑定。tsconfig.json通过types: [./worker-configuration.d.ts, node]将其纳入编译app/data.ts 中所有函数的db: D1Database参数即来源于此全链路类型安全。四、路由声明与请求分发1. 声明式路由表app/routes.ts 只有短短几行却集中演示了 fetch-router 的三种路由声明方式import { route, form, resources } from remix-run/fetch-router/routes export const routes route({ home: /, login: form(/login), logout: { method: POST, pattern: /logout }, posts: resources(posts, { only: [new, create, show] }), })home: /是普通路径声明接受任意请求方法类型为RouteANY, /form(/login)是表单路由简写会展开为login.indexGET /login展示登录表单与login.actionPOST /login处理表单提交两条路由logout: { method: POST, pattern: /logout }直接把请求方法编码进路由router.map()会只为此方法注册处理器resources(posts, { only: [new, create, show] })是 RESTful 资源路由简写默认会生成index/new/show/create/edit/update/destroy七条路由这里通过only只保留三组GET /posts/new、POST /posts、GET /posts/:id。2. Worker 入口与路由分发worker.ts 是 Worker 的唯一入口它把整个应用的请求处理委托给路由器import { router } from ./app/router.ts export default { async fetch(request): PromiseResponse { try { return await router.fetch(request) } catch (error) { console.error(error) return new Response(Internal Server Error, { status: 500 }) } }, } satisfies ExportedHandlerEnv要点satisfies ExportedHandlerEnv让Env含DB: D1Database参与类型检查用try/catch包裹router.fetch()与 fetch-router README 中「wraprouter.fetch()in a try/catch to handle errors」的建议一致任何处理器抛出的异常都会被捕获统一返回 500 而非让 Worker 直接崩溃router.fetch()接收标准Request、返回标准Response与 Cloudflare Workers 的fetch事件模型完全同构这也是 fetch-router「built on the web Fetch API」的设计核心。3. 中间件栈装配app/router.ts 是应用的心脏它先创建路由器并注册全局中间件再逐条映射路由处理器export const router createRouter({ middleware: [logger(), formData(), session(sessionCookie, sessionStorage), loadDatabase()], })中间件按声明顺序执行中间件来源职责logger()remix-run/logger-middleware记录每个请求的方法、URL、状态码与耗时formData()remix-run/form-data-middleware解析请求体为FormData供表单 action 读取session(...)remix-run/session-middleware基于 Cookie 会话存储加载/保存会话loadDatabase()示例自实现把 D1 数据库实例注入请求上下文其中loadDatabase展示了 fetch-router 的类型化请求上下文用法const Database createContextKeyD1Database() function loadDatabase(): Middleware{ key: typeof Database; value: D1Database; property: db } { return (context, next) { context.set(Database, env.DB, { property: db }) return next() } }createContextKeyD1Database()创建一个带类型的上下文键context.set(Database, env.DB, { property: db })把绑定写入上下文同时通过property: db暴露为context.db之后所有处理器如router.map(routes.home, async ({ db, session }) ...)都能直接解构出db和session全程类型安全无需任何断言。requireAuth则是 action 级中间件的例子——它为「新建文章」页面提供登录保护function requireAuth(): Middleware { return (context, next) { let session context.get(Session) if (session null) { throw new Error(Expected session() middleware before requireAuth()) } let username session.get(username) if (!username) { return redirect(routes.login.index.href()) } return next() } }它先校验会话中间件是否已运行未运行则抛出明确错误未登录用户直接重定向到登录页。注册时挂载在 action 的middleware数组上new: { middleware: [requireAuth()], handler() { /* ... */ }, },这正体现了 fetch-router 的三级中间件模型router 级所有请求、controller 级控制器内全部 action、action 级单个 action按需选用。4. 处理器与类型安全的链接路由处理器既可以是函数自动绑定到对应方法也可以是带actions的对象。以登录为例form(/login)展开出的两个 action 各司其职router.map(routes.login, { actions: { index({ session }) { let username session.get(username) as string | undefined if (username) { return redirect(routes.home.href()) } return createHtmlResponse(html ...登录表单... ) }, async action({ formData, session }) { let { username } s.parse(loginSchema, formData) if (username) { session.set(username, username) return redirect(routes.home.href()) } return redirect(routes.login.index.href()) }, }, })值得关注的是类型安全链接模板里所有href、表单action都通过routes.xxx.href()生成例如routes.posts.show.href({ id: post.id })生成/posts/:id且id参数有类型约束routes.logout.href()生成/logoutroutes.posts.new.href()生成/posts/new。这意味着当路由表路径、参数名发生调整时所有引用点会在编译期报错杜绝「字符串手拼 URL」带来的漂移问题。表单提交的数据校验由remix-run/data-schema完成路由文件顶部声明了两个 schemaconst textField f.field(s.defaulted(s.string(), )) const loginSchema f.object({ username: textField }) const postSchema f.object({ title: textField, content: textField })defaulted(s.string(), )表示「字符串字段缺失时回退为空串」配合s.parse()实现声明式的表单数据解析。五、D1 数据访问层app/data.ts 把所有 SQL 收敛在一个文件里统一处理「数据库行 → 领域对象」的转换interface PostRow { id: number title: string content: string author: string created_at: number } function rowToPost(row: PostRow): Post { return { id: String(row.id), title: row.title, content: row.content, author: row.author, createdAt: new Date(row.created_at * 1000), } }三个函数对应博客的三类查询查询全部文章倒序export async function getPosts(db: D1Database): PromisePost[] { let result await db.prepare(SELECT * FROM posts ORDER BY created_at DESC).allPostRow() return result.results.map(rowToPost) }按 ID 查询单篇文章export async function getPost(db: D1Database, id: string): PromisePost | null { let result await db.prepare(SELECT * FROM posts WHERE id ?).bind(id).firstPostRow() return result ? rowToPost(result) : null }插入新文章并返回完整对象export async function createPost(db: D1Database, title: string, content: string, author: string): PromisePost { let createdAt Math.floor(Date.now() / 1000) let result await db .prepare(INSERT INTO posts (title, content, author, created_at) VALUES (?, ?, ?, ?)) .bind(title, content, author, createdAt) .run() let id result.meta.last_row_id return { id: String(id), title, content, author, createdAt: new Date(createdAt * 1000) } }这组代码是 D1 API 的标准用法prepare()创建预编译语句 →bind()绑定参数有效防止 SQL 注入→all()/first()/run()分别对应「多行查询」「单行查询」「写入」。rowToPost负责把 SQLite 的created_at时间戳还原为Date对象处理器层因此可以直接调用post.createdAt.toLocaleDateString()渲染日期。在处理器中数据库通过请求上下文注入无需手动传递连接router.map(routes.home, async ({ db, session }) { let posts await data.getPosts(db) // ...渲染文章列表 })六、部署到 Cloudflare 生产环境本地验证通过后按以下四步发布到云端。与本地开发的关键区别在于生产环境需要先创建真实的 D1 数据库并把其 ID 写回配置。1. 创建 D1 数据库如尚未创建npx wrangler d1 create fetch-router-blog命令执行成功后Wrangler 会输出一个形如 UUID 的database_id这是生产数据库的唯一标识。2. 更新wrangler.jsonc把 wranger.jsonc 中d1_databases[0].database_id的占位值local替换为上一步获得的真实 IDd1_databases: [ { binding: DB, database_name: fetch-router-blog, database_id: 你的真实 database_id, migrations_dir: migrations } ]本地开发阶段保持local即可让 Wrangler 使用本地 SQLite一旦部署到云端database_id必须是真实的远程库 ID否则 Worker 无法解析绑定。3. 将迁移应用到远程数据库pnpm run db:migrate --remote--remote标志让wrangler d1 migrations apply把migrations/下的 SQL 应用到云端数据库不带该标志默认作用于本地。此时posts表与两条演示数据会在生产库中建立。4. 部署 Workerpnpm run deploy即wrangler deployWrangler 依据wrangler.jsonc入口worker.ts、D1 绑定、可观测性配置打包并发布 Worker。部署完成后你的*.workers.dev域名或自定义域即可对外提供完整服务。七、类型检查与工程化配套示例还提供了两个值得沿用的工程化脚本见 package.jsonscripts: { deploy: wrangler deploy, dev: wrangler dev --port 44100, typecheck: tsc --noEmit, typegen: wrangler types, db:migrate: wrangler d1 migrations apply fetch-router-blog }pnpm run typegen由 Wrangler 根据wrangler.jsonc生成worker-configuration.d.ts含Env与全部运行时类型。改动 D1 绑定后应重新生成保持类型同步pnpm run typecheck基于 tsconfig.json 做全量类型检查。该配置启用了strict、module: NodeNext、allowImportingTsExtensions等并引入生成的 Worker 类型可在提交前拦截路由表、上下文属性或 D1 调用中的类型错误。结合 fetch-router 主文档 可以看到本示例完整覆盖了该库的核心能力声明式路由表route/form/resources、方法感知的路由{ method, pattern }、类型安全链接href()、三级中间件router/controller/action、类型化请求上下文createContextKeyproperty以及「直接用标准fetch()测试路由」的可测性——这些能力在不同运行时之间完全一致本示例正是其在 Cloudflare Workers D1 上的一次完整落地。小结通过这个「简易博客」示例一条清晰的链路已经成型wrangler.jsonc声明 D1 绑定 →worker.ts把请求交给router.fetch()→routes.ts以类型安全的方式声明路由 →router.ts用中间件装配会话、日志、表单解析与数据库上下文 →data.ts通过 Prepared Statement 操作 D1 → 本地wrangler dev开发验证后用d1 create 迁移 deploy一键上云。整个过程中fetch-router 始终基于标准 Fetch API让同一套路由与中间件代码可以无缝迁移到 Node.js、Bun、Deno 等其他运行时这也是在边缘计算时代保持应用可移植性的务实选择。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考