基于cloudflare的全站项目创建及联调
基于cloudflare的全站项目创建及联调
对于个人开发者,做全栈项目难免要考虑很多:买服务器、域名、选数据库和服务端技术栈、配代理…而使用cloudflare的一条龙服务显然是更简洁的方式。但是这一套流程和涉及的工具比较复杂,每次重新开始一个项目我都要花时间回想之前是怎么做的。所以今天写一篇文章来一劳永逸地记录一下完整过程。
初始化并部署monorepo项目
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目。可以用它把前后端放在一起进行版本控制。这种方式和multirepo相比自然有很多劣势,但是作为个人开发者做小项目用它绝对是利大于弊。
工作区配置
新建项目文件夹,pnpm init初始化。在根目录下创建 pnpm-workspace.yaml,用于声明前后端两个包:
packages:
- "frontend"
- "backend"更新根 package.json,写入一些针对前后端的便捷脚本:
{
"name": "example-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev:frontend": "pnpm -C frontend dev",
"build:frontend": "pnpm -C frontend build",
"dev:backend": "pnpm -C backend dev",
"deploy:backend": "pnpm -C backend deploy",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"private": true,
"packageManager": "[email protected]"
}通用工具安装
如果已经全局安装过就不用再装:
pnpm i -g wrangler
pnpm i -g whistle后端
后端选用hono框架。它支持运行时、比较轻量、支持ts、中间件多,天然适合部署为cloudflare worker。新建后端文件夹,并手动创建如下结构:
backend/
wrangler.toml
tsconfig.json
src/index.ts
migrations/0001_init.sql其中wrangler.toml是cloudflare worker的配置文件,tsconfig.json用来配置ts规则,src/index.ts是源码目录和入口文件,migrations/0001_init.sql是对数据库的操作。
之后初始化pnpm并安装必要的包:
pnpm init
pnpm -C backend add hono
pnpm -C backend add -D wrangler @cloudflare/workers-types写backend/tsconfig.json,照抄即可:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "WebWorker"],
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true,
"allowJs": false,
"isolatedModules": true
},
"include": ["src/**/*.ts"]
}写backend/wrangler.toml,注意把占位符改成cloudflare上的真实数据库信息:
name = "pictorial-backend"
main = "src/index.ts"
compatibility_date = "2024-10-01"
[[d1_databases]]
binding = "DB"
database_name = "<YOUR_D1_DB_NAME>"
database_id = "<YOUR_D1_DB_ID>"
migrations_dir = "migrations"在backend/src/index.ts中,先简单写两个接口来测试:
import { Hono } from 'hono';
type Env = {
Bindings: {
DB: D1Database;
};
};
const app = new Hono<Env>();
app.get('/api/health', (c) => {
return c.json({ ok: true });
});
app.get('/api/notes', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT id, content, created_at FROM notes ORDER BY id DESC'
).all();
return c.json({ notes: results ?? [] });
});
app.post('/api/notes', async (c) => {
let content: string | undefined;
try {
const body = await c.req.json();
content = body?.content;
} catch {
// ignore parse error
}
if (!content || typeof content !== 'string' || !content.trim()) {
return c.json({ error: 'content is required' }, 400);
}
await c.env.DB.prepare('INSERT INTO notes (content) VALUES (?)').bind(content).run();
return c.json({ ok: true });
});
export default app;然后在backend/migrations/0001_init.sql中创建表并插入数据:
-- D1 migration: initialize notes table
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- seed example data
INSERT INTO notes (content) VALUES ('Hello from D1!');最后,可以在package.json中也加入一些便捷指令(注意替换占位符):
{
"name": "backend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "wrangler dev --remote",
"dev:remote": "wrangler dev --remote",
"deploy": "wrangler deploy",
"db:migrate:remote": "wrangler d1 migrations apply <YOUR_D1_DB_NAME> --remote",
"db:execute:remote": "wrangler d1 execute <YOUR_D1_DB_NAME> --remote --file=./migrations/0001_init.sql"
},
"dependencies": {
"hono": "^4.10.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"wrangler": "^4.45.3"
}
}现在后端所需的文件都已经配置完成。接下来把这个后端部署到cloudflare:
wrangler login # 会打开浏览器登录
wrangler deploy # 部署时cloudflare会根据配置文件自动连接数据库
pnpm run db:execute:remote # 执行数据库迁移脚本做完这些后,用postman或浏览器往接口发请求应该就能拿到数据了。
前端
前端一般就用react-ts,根目录初始化项目:
pnpm create vite@latest frontend -- --template react-ts初始化时可以写一个按钮来调后端接口测试能不能打通:
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
const [response, setResponse] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchHealthData = async () => {
// 增加计数器
setCount(prev => prev + 1)
// 重置状态
setLoading(true)
setError(null)
setResponse(null)
try {
const response = await fetch('https://backstage-backend.fuufhjn.link/api/notes')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.text()
setResponse(data)
} catch (err) {
setError(err instanceof Error ? err.message : '请求失败')
} finally {
setLoading(false)
}
}
return (
<>
<div>
<a href="https://vite.dev" target="_blank" rel="noopener noreferrer">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noopener noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={fetchHealthData}>
点击发送请求 (已点击: {count})
</button>
{/* 显示请求状态 */}
{loading && <p className="loading">请求中...</p>}
{/* 显示错误信息 */}
{error && (
<div className="error">
<p>请求失败:</p>
<code>{error}</code>
</div>
)}
{/* 显示API响应 */}
{response && (
<div className="response">
<p>API响应:</p>
<code>{response}</code>
</div>
)}
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App;配置反向代理用于联调
为了避免同源策略的影响,要把前后端的请求代理到同一源下。浏览器在进行反向代理时只能够到前端,所以最佳方式是起一个代理服务,让这个代理服务同时处理前后端的反向代理,浏览器只需要选择这个服务的规则即可。
whistle代理服务
上面已经全局安装过了,使用w2 start启动。注意检查一下前端目录中frontend/vite.config.ts里的内容应该有:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:8787'
}
}
})这样前端发的请求都会被代理到8787端口。启动服务后,访问localhost:8899,新建一条规则并保存、激活。
如果你希望同时使用本地的前后端,可以这样写whistle代理规则:
<backend domain>.fuufhjn.link/api/ http://localhost:8787/api/ resCors://*
<frontend domain>.fuufhjn.link/ http://localhost:5173/ resCors://*在开发中,经常会先调好后端(因为cloudflare被墙经常打不开远程调试),只在本地调试前端,后端就用真实部署的数据,这种情况下应该取消后端的代理:
backstage-backend.fuufhjn.link/api/ resCors://*
# backstage-backend.fuufhjn.link/api/ http://localhost:8787/api/ resCors://*
backstage-management.fuufhjn.link/ http://localhost:5173/ resCors://*写好规则后点击上面的https安装证书,安装后一定记得在设置——自定义证书中引入并信任,不然装了也没用!
SwitchyOmega插件
直接在插件商店安装即可,之后添加一条自定义规则,通过http代理到localhost:8899。此时,激活浏览器代理,启动前后端的本地服务,在浏览器输入域名就会发现访问到的是本地的前端服务,尝试发送请求拿到的也是本地后端的数据。
后续开发routine
开始联调
从上面的流程可以看出,联调时需要启动三个服务:前端、后端(同时调前后端时才需要)和whistle代理。分开写命令是:
pnpm dev:backend
pnpm dev:frontend
w2 start然后再打开浏览器代理(注意根据后端调试方式不同在8899端口改变代理规则)。
结束
结束调试时在终端关闭前后端服务并w2 stop,把浏览器代理改为direct即可。
