本地搭建 Hono + D1 + Vue3 + TailwindCSS 并发布到 Cloudflare Workers
本地搭建 Hono + D1 + Vue3 + TailwindCSS 并发布到 Cloudflare Workers 本文从零开始搭一个小项目:
1 2 3 4 Vue3 + TailwindCSS 前端 Hono API Cloudflare D1 数据库 Cloudflare Workers 部署
并且从:
开始讲。
一、项目最终效果 我们做一个最简单的 Todo 项目。
功能:
查看 Todo
新增 Todo
切换完成状态
删除 Todo
接口:
1 2 3 4 GET /api/todos POST /api/todos PATCH /api/todos/:id/toggle DELETE /api/todos/:id
前端:
后端:
部署:
Cloudflare 官方 D1 文档里也明确说明,D1 是 Cloudflare 原生 serverless SQL 数据库,可以通过 Worker binding 在 Workers 里查询。Hono 官方文档也提供了 Cloudflare Workers 模板,并推荐用 Wrangler 本地开发和发布。
二、创建项目 先创建目录:
1 2 3 mkdir hono-d1-vue-todocd hono-d1-vue-todopnpm init
安装依赖:
1 2 3 pnpm i hono @hono/vite-cloudflare-pages pnpm i vue @vitejs/plugin-vue pnpm i -D vite typescript wrangler tailwindcss @tailwindcss/vite
如果你还没有登录 Cloudflare:
1 pnpm exec wrangler login
检查登录状态:
1 pnpm exec wrangler whoami
三、目录结构 创建目录:
1 2 3 4 mkdir -p src/client src/server migrationstouch src/client/main.ts src/client/App.vue src/client/style.csstouch src/server/index.tstouch index.html vite.config.ts wrangler.jsonc migrations/0001_init.sql
目录结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 hono-d1-vue-todo/ ├── index.html ├── vite.config.ts ├── wrangler.jsonc ├── migrations/ │ └── 0001_init.sql └── src/ ├── client/ │ ├── App.vue │ ├── main.ts │ └── style.css └── server/ └── index.ts
四、配置 Vite 写 vite.config.ts:
1 2 3 4 5 6 7 8 9 10 import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' export default defineConfig ({ plugins : [vue (), tailwindcss ()], build : { outDir : 'dist' , }, })
写 index.html:
1 2 3 4 5 6 7 8 9 10 11 12 <!doctype html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > Hono D1 Vue Todo</title > </head > <body > <div id ="app" > </div > <script type ="module" src ="/src/client/main.ts" > </script > </body > </html >
写 src/client/style.css:
五、配置 D1 数据库 先创建 D1:
1 pnpm exec wrangler d1 create todo-db
命令执行后会输出类似:
1 2 3 4 5 6 7 8 9 { "d1_databases" : [ { "binding" : "DB" , "database_name" : "todo-db" , "database_id" : "xxxx-xxxx-xxxx" } ] }
把它写进 wrangler.jsonc。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "$schema" : "./node_modules/wrangler/config-schema.json" , "name" : "hono-d1-vue-todo" , "main" : "src/server/index.ts" , "compatibility_date" : "2026-06-03" , "assets" : { "directory" : "./dist" , "binding" : "ASSETS" } , "d1_databases" : [ { "binding" : "DB" , "database_name" : "todo-db" , "database_id" : "替换成你的 database_id" } ] }
Cloudflare D1 官方文档也强调,D1 通过 binding 暴露给 Worker,并在代码里通过 env.<BINDING_NAME> 访问。
六、创建数据表 写 migrations/0001_init.sql:
1 2 3 4 5 6 7 8 9 10 11 12 13 DROP TABLE IF EXISTS todos;CREATE TABLE todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL , completed INTEGER NOT NULL DEFAULT 0 , created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); INSERT INTO todos (title, completed) VALUES ('学习 Hono' , 0 ), ('连接 D1 数据库' , 0 ), ('发布到 Cloudflare Workers' , 0 );
先初始化本地 D1:
1 pnpm exec wrangler d1 execute todo-db --local --file=./migrations/0001_init.sql
查询一下:
1 pnpm exec wrangler d1 execute todo-db --local --command ="SELECT * FROM todos"
再初始化远程 D1:
1 pnpm exec wrangler d1 execute todo-db --remote --file=./migrations/0001_init.sql
注意:
1 2 --local 是本地开发数据库 --remote 是 Cloudflare 远程数据库
七、编写 Hono API 写 src/server/index.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 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 import { Hono } from 'hono' type Bindings = { DB : D1Database ASSETS : Fetcher } const app = new Hono <{ Bindings : Bindings }>()app.get ('/api/todos' , async (c) => { const { results } = await c.env .DB .prepare ( 'SELECT * FROM todos ORDER BY id DESC' ).all () return c.json (results) }) app.post ('/api/todos' , async (c) => { const body = await c.req .json <{ title : string }>() const title = body.title ?.trim () if (!title) { return c.json ({ message : 'title is required' }, 400 ) } await c.env .DB .prepare ( 'INSERT INTO todos (title, completed) VALUES (?, 0)' ).bind (title).run () return c.json ({ ok : true }) }) app.patch ('/api/todos/:id/toggle' , async (c) => { const id = Number (c.req .param ('id' )) await c.env .DB .prepare ( 'UPDATE todos SET completed = CASE completed WHEN 1 THEN 0 ELSE 1 END WHERE id = ?' ).bind (id).run () return c.json ({ ok : true }) }) app.delete ('/api/todos/:id' , async (c) => { const id = Number (c.req .param ('id' )) await c.env .DB .prepare ('DELETE FROM todos WHERE id = ?' ) .bind (id) .run () return c.json ({ ok : true }) }) app.get ('*' , async (c) => { return c.env .ASSETS .fetch (c.req .raw ) }) export default app
这里所有 SQL 都用 prepare().bind()。
不要拼字符串。
八、编写 Vue3 页面 写 src/client/main.ts:
1 2 3 4 5 import { createApp } from 'vue' import App from './App.vue' import './style.css' createApp (App ).mount ('#app' )
写 src/client/App.vue:
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 <script setup lang="ts"> import { onMounted, ref } from 'vue' type Todo = { id: number title: string completed: number created_at: string } const todos = ref<Todo[]>([]) const title = ref('') async function loadTodos() { todos.value = await fetch('/api/todos').then((res) => res.json()) } async function addTodo() { const value = title.value.trim() if (!value) return await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: value }), }) title.value = '' await loadTodos() } async function toggleTodo(id: number) { await fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' }) await loadTodos() } async function removeTodo(id: number) { await fetch(`/api/todos/${id}`, { method: 'DELETE' }) await loadTodos() } onMounted(loadTodos) </script> <template> <main class="min-h-screen bg-slate-950 px-6 py-10 text-slate-100"> <section class="mx-auto max-w-2xl"> <h1 class="text-3xl font-bold">Hono + D1 + Vue Todo</h1> <p class="mt-2 text-slate-400">一个部署在 Cloudflare Workers 上的小项目。</p> <form class="mt-8 flex gap-3" @submit.prevent="addTodo"> <input v-model="title" class="flex-1 rounded-lg border border-slate-700 bg-slate-900 px-4 py-3 outline-none focus:border-teal-400" placeholder="输入一个任务" /> <button class="rounded-lg bg-teal-500 px-5 py-3 font-semibold text-slate-950"> 添加 </button> </form> <ul class="mt-8 space-y-3"> <li v-for="todo in todos" :key="todo.id" class="flex items-center justify-between rounded-lg border border-slate-800 bg-slate-900 px-4 py-3" > <button class="text-left" :class="todo.completed ? 'text-slate-500 line-through' : 'text-slate-100'" @click="toggleTodo(todo.id)" > {{ todo.title }} </button> <button class="text-sm text-rose-300" @click="removeTodo(todo.id)"> 删除 </button> </li> </ul> </section> </main> </template>
九、配置 package.json 修改 package.json:
1 2 3 4 5 6 7 8 { "scripts" : { "dev" : "vite --host 0.0.0.0" , "build" : "vite build" , "worker:dev" : "wrangler dev" , "deploy" : "vite build && wrangler deploy" } }
本地前端开发:
Worker 本地开发:
部署:
十、发布到 Cloudflare Workers 发布前确认:
确认远程 D1 已初始化:
1 pnpm exec wrangler d1 execute todo-db --remote --command ="SELECT * FROM todos"
部署:
部署成功后,会输出 workers.dev 地址。
访问后应该能看到 Vue 页面,并且 Todo 数据来自 D1。
十一、常见问题 1. 前端能打开,但 API 404 检查 Worker 是否有:
1 app.get ('/api/todos' , ...)
并且 app.get('*') 是否放在 API 路由之后。
2. D1 查询报错 检查 wrangler.jsonc:
代码里也必须是:
3. 线上没有数据 你可能只初始化了本地 D1。
需要执行:
1 pnpm exec wrangler d1 execute todo-db --remote --file=./migrations/0001_init.sql
4. Tailwind 没生效 检查:
以及 style.css:
总结 这套组合很适合小型全栈项目:
1 2 3 4 5 6 Vue3 负责界面 TailwindCSS 负责样式 Hono 负责 API D1 负责数据 Workers 负责部署 Wrangler 负责本地开发和发布
优点是:
项目轻
部署快
不需要传统服务器
API 和前端可以放在一个 Worker
D1 用 SQL,上手成本低
如果只是做工具站、小后台、个人项目、轻量业务,这套技术栈非常顺手。
参考资料