本地搭建 Hono + D1 + Vue3 + TailwindCSS 并发布到 Cloudflare Workers
wxk1991 Lv5

本地搭建 Hono + D1 + Vue3 + TailwindCSS 并发布到 Cloudflare Workers

本文从零开始搭一个小项目:

1
2
3
4
Vue3 + TailwindCSS 前端
Hono API
Cloudflare D1 数据库
Cloudflare Workers 部署

并且从:

1
pnpm i

开始讲。

Hono + D1 + Vue3 + TailwindCSS + Workers 工作流


一、项目最终效果

我们做一个最简单的 Todo 项目。

功能:

  • 查看 Todo
  • 新增 Todo
  • 切换完成状态
  • 删除 Todo

接口:

1
2
3
4
GET    /api/todos
POST /api/todos
PATCH /api/todos/:id/toggle
DELETE /api/todos/:id

前端:

1
Vue3 + TailwindCSS

后端:

1
Hono + D1

部署:

1
Cloudflare Workers

Cloudflare 官方 D1 文档里也明确说明,D1 是 Cloudflare 原生 serverless SQL 数据库,可以通过 Worker binding 在 Workers 里查询。Hono 官方文档也提供了 Cloudflare Workers 模板,并推荐用 Wrangler 本地开发和发布。


二、创建项目

先创建目录:

1
2
3
mkdir hono-d1-vue-todo
cd hono-d1-vue-todo
pnpm 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 migrations
touch src/client/main.ts src/client/App.vue src/client/style.css
touch src/server/index.ts
touch 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

1
@import "tailwindcss";

五、配置 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"
}
}

本地前端开发:

1
pnpm dev

Worker 本地开发:

1
pnpm worker:dev

部署:

1
pnpm deploy

十、发布到 Cloudflare Workers

发布前确认:

1
pnpm run build

确认远程 D1 已初始化:

1
pnpm exec wrangler d1 execute todo-db --remote --command="SELECT * FROM todos"

部署:

1
pnpm run deploy

部署成功后,会输出 workers.dev 地址。

访问后应该能看到 Vue 页面,并且 Todo 数据来自 D1。


十一、常见问题

1. 前端能打开,但 API 404

检查 Worker 是否有:

1
app.get('/api/todos', ...)

并且 app.get('*') 是否放在 API 路由之后。

2. D1 查询报错

检查 wrangler.jsonc

1
"binding": "DB"

代码里也必须是:

1
c.env.DB

3. 线上没有数据

你可能只初始化了本地 D1。

需要执行:

1
pnpm exec wrangler d1 execute todo-db --remote --file=./migrations/0001_init.sql

4. Tailwind 没生效

检查:

1
import './style.css'

以及 style.css

1
@import "tailwindcss";

总结

这套组合很适合小型全栈项目:

1
2
3
4
5
6
Vue3 负责界面
TailwindCSS 负责样式
Hono 负责 API
D1 负责数据
Workers 负责部署
Wrangler 负责本地开发和发布

优点是:

  • 项目轻
  • 部署快
  • 不需要传统服务器
  • API 和前端可以放在一个 Worker
  • D1 用 SQL,上手成本低

如果只是做工具站、小后台、个人项目、轻量业务,这套技术栈非常顺手。


参考资料