Cloudflare 服务设计

依托目前 Cloudflare 的生态, 可以直接无需单独购买服务器和数据库搭建自己的云端服务

  • 视图页面(构建 - 计算 - Workers 和 Pages): 支持直接创建部署静态页面功能
  • 接口功能(构建 - 计算 - Workers 和 Pages): 支持创建一等公民的对外服务(目前一等公民语言为 Rust 和 NodeJS)
  • 数据库功能(存储和数据库 - Postgres & MySQL 数据库): 官方提供的 PlanetScale 云服务器(注意做好额度报警,避免超额)

注意: PlanetScale 数据库不支持中国境内使用

以上功能都有免费额度, 只要不超过额度一般不需要收费, 所以可以用于个人简单使用

这里将先依托官方的 workers-rs 构建简单的接口服务用于处理自定义接口服务, 后面将会围绕这部分扩展出具体的模块开发

workers-rs 文档: https://developers.cloudflare.com/workers/languages/rust/

建议业务抽离成单独模块出来, 因为 Cloudflare 目前方案是基于编译 wasm 运行, 而其他云服务器商会采用静态二进制运行

服务搭建

这里默认本地服务已经搭建好依赖的服务

1
2
3
4
5
rustc --version
# rustc 1.96.0 (ac68faa20 2026-05-25)

npm --version
# 11.11.0

注意: 官方采用 wasm32 技术, 所以需要追加 wasm32 编译模块

1
2
3
4
5
# 追加 wasm32 编译器
rustup target add wasm32-unknown-unknown

# 追加代码生成器
cargo install cargo-generate

完成之后就开始初始化项目, 进入自己电脑的工程目录, 按照以下方式生成项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 项目代码生成
cargo generate cloudflare/workers-rs
# 这里会选择几个模板来选择
# - templates\axum: 接入标准 Axum Web 完整框架
# - templates\hello-world: 极简最小示例, 无 HTTP 封装
# - templates\hello-world-http: workers 内置原生 Router, 官方简易路由用于展示测试样例
# - templates\leptos: Leptos 全栈 SSR 前端框架 和 Cloudflare Worker 后端
#
# 这几个模板可以先导出 hello-world-http 看看 HTTP 服务整体运行逻辑
# hello-world 模板可以测试下内部代码业务
# leptos 是一套完整前后端 SSR 架构全栈框架(依赖很重的框架), 这部分有兴趣可以去了解
# 如果需要跨编译器处理建议采用 axum 原生框架编写, 所以我采用这部分开发
#
# 生成项目名为 nova-workers 的项目工程
# 之后会提示启动 `panic=unwind` 错误响应, 这里 panic=unwind 会让 `panic!` 崩溃时展开调用栈并输出完整堆栈日志
# 虽然输出堆栈异常信息可以高效精准获取到异常错误, 但是会导致 wasm 产物体积变大且不会理解抛出异常终端
# 所以建议保持默认设置的 false, 避免 wasm 编译产物膨胀和异常中断的问题
#
# 完成之后就可以进入工程目录并且进行 git 代码控制托管

我这边采用 IDEA + Rust插件 来启动工程项目, 其他开发工具应该也是差不多, 出问题的时候参照官方文档来处理即可

这里说明下生成项目下的文件和目录具体功能:

  • Cargo.toml: Rust 的 Cargo 包管理器的标准项目配置文件

  • wrangler.toml: Cloudflare 的 Wasm 打包配置信息

  • src/: 核心的业务源代码目录

等待完成编译结束, 可以执行以下命令来直接启动本地测试服务器

1
2
3
4
5
6
7
8
# 启动本地测试服务端
npx wrangler dev
# 这里首次启动可能比较慢, 并且会提示你按照 wrangler@xxx 依赖包, 输入 `y` 即可
# 等待自动安装所有依赖, 可以直接访问 http://localhost:8787 确认服务是否启动
# 如果一直卡在 [custom build] Running: cargo install 只能等待依赖包全部下载完成
# 依赖下载完成会提示 [wrangler:info] Ready on http://127.0.0.1:8787
# 可以直接点击这个地址直接访问, 会提示 `Hello Axum!`

启动完成下一步就是准备编写自定义的业务功能代码, 这部分需要你有一定 Rust 代码基础

业务功能

这里首先定义事件接口

1
2
3
4
# 假设用以下 curl 接口 POST 请求
curl -X POST -d "username=meteorcat&password=123456" http://127.0.0.1:8787/event/authorize
# 这里就是模拟请求的业务接口

一般来说, 可以在 src/lib.rs 文件中找到 Worker 的入口点定义, 默认初始化文件如下

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
//! nova-workers 主服务入口
//!
//! 利用 Cloudflare + Axum 实现的边缘 API 网关, 目前包含以下模块
//! - /event/authorize: 简单授权模块
//! - /event/logout: 注销授权模块

use axum::{routing::get, Router};
use tower_service::Service;
use worker::*;

/// 路由功能
///
/// 将对应 PATH 绑定到指定业务函数的映射功能
fn router() -> Router {
Router::new().route("/", get(root))
}


/// GET 方法首页
///
/// 测试的业务功能入口函数
pub async fn root() -> &'static str {
"Hello Axum!"
}


/// 服务启动入口
///
/// 处理传入的 HTTP 请求事件, 并将其路由分发至对应的处理函数
#[event(fetch)]
async fn fetch(
req: HttpRequest,
_env: Env,
_ctx: Context,
) -> Result<axum::http::Response<axum::body::Body>> {
Ok(router().call(req).await?)
}

这里我调整了以下, 只需要 GET 方法的 / 路径直接返回当前 UTC 毫秒时间戳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
40
41
42
43
44
45
46
47
48
//! nova-workers 主服务入口
//!
//! 利用 Cloudflare + Axum 实现的边缘 API 网关, 目前包含以下模块
//! - /event/authorize: 简单授权模块
//! - /event/logout: 注销授权模块

use axum::{routing::get, response::Json, Router};
use axum::http::StatusCode;
use tower_service::Service;
use worker::*;
use serde::Serialize;

/// 路由功能
///
/// 将对应 PATH 绑定到指定业务函数的映射功能
fn router() -> Router {
Router::new().route("/", get(root))
}


/// 生成时间请求响应标识
#[derive(Debug, Serialize)]
pub struct TimeResponse {
time: u64,
}


/// GET 方法首页
///
/// 测试的业务功能入口函数
pub async fn root() -> (StatusCode, Json<TimeResponse>) {
let ts = Date::now().as_millis();
(StatusCode::OK, Json(TimeResponse { time: ts }))
}


/// 服务启动入口
///
/// 处理传入的 HTTP 请求事件, 并将其路由分发至对应的处理函数
#[event(fetch)]
async fn fetch(
req: HttpRequest,
_env: Env,
_ctx: Context,
) -> Result<axum::http::Response<axum::body::Body>> {
Ok(router().call(req).await?)
}

这里会提示 auxm:json 异常(这是正常的)

注意: 因为默认 axum 被设置为 default-features=false, 因此不会自动引入 json 依赖, 找到以下依赖修改成以下配置

1
2
3
axum = { version = "0.8", default-features = false, features = ["form","json", "http1", "macros",]}
serde = { version = "1.0.228", features = ["derive", "std"] }
serde_json = "1.0"

不要设置 default-features=true, 因为底层用的异步运行时是 Cloudflare 定制的, 而 axum 底层及其依赖 Tokio 运行时(
缺失依赖)

重新访问 http://127.0.0.1:8787 就会返回以下内容

1
2
3
{
"time": 1783675410365
}

现在回过头来可以把路由拆分出来写我们自定义业务功能, 拆分出来如下所示

1
2
3
4
5
6
7
8
9
10
11
src/
├── lib.rs # 程序入口, 总路由组装, #[event(fetch)]
├── model.rs # 所有序列化结构体: 请求表单, 返回JSON通用结构
├── handler/
│ ├── mod.rs # 统一导出所有处理函数
│ ├── root.rs # 首页/时间戳接口逻辑
│ └── event.rs # /event 功能组
└── route/
├── mod.rs # 统一导出所有路由分组
├── root_route.rs # 根路由分组
└── event_route.rs # /event 业务路由分组

这里拆分出来几个文件可以按照个人习惯区分即可, 这里仅仅作为参考

model.rs

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
//! 数据请求&响应模型

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use worker::Date;

/// 通用包装响应枚举
#[derive(Serialize)]
#[serde(untagged)]
#[warn(dead_code)]
pub enum Responses {
Time(TimeResponse),
EventAuthorize(EventAuthorizeResponse),
Error(ErrorResponse),
}

/// 通用成功响应
#[derive(Debug, Serialize)]
pub struct TimeResponse {
pub time: u64,
}

/// 派生当前时间响应功能
impl TimeResponse {
pub fn now() -> Self {
let ts = Date::now().as_millis();
TimeResponse { time: ts }
}
}

/// 通用异常响应
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub code: String,
pub messages: HashMap<String, String>,
}

/// 衍生简单的 messages:{ error: ""} 错误信息
impl ErrorResponse {
pub fn error(code: String, msg: String) -> Self {
let mut messages = HashMap::with_capacity(1);
messages.insert("error".to_string(), msg);
ErrorResponse { code, messages }
}
}

/// 请求参数: /event/authorize
#[derive(Debug, Deserialize)]
pub struct EventAuthorizeRequest {
pub username: String,
pub password: String,
}

// 响应参数: /event/authorize
#[derive(Debug, Serialize)]
pub struct EventAuthorizeResponse {
pub uid: u64,
pub username: String,
pub token: String,
}

有的会将响应数据嵌套一层数据, 从而形成 { status:200, message: "OK", data:{ 嵌套新的数据结构 } }

我这边直接每个响应自定义结构体返回, 客户端只需要识别 HttpStatus=200 就可以直接解析结构体即可

handler/root.rs

1
2
3
4
5
6
7
8
use crate::model::TimeResponse;
use axum::{http::StatusCode, response::Json};

/// GET 方法首页
pub async fn root() -> (StatusCode, Json<impl serde::Serialize>) {
(StatusCode::OK, Json(TimeResponse::now()))
}

这里采用 Json<impl serde::Serialize> 响应结构是为了方便多个通用响应体, 而响应必须确定结构对象, 按照以下方式会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 以下功能编译会报错, 因为只能识别单一的 serde::Serialize 解析
pub async fn resp() -> (StatusCode, Json<impl serde::Serialize>) {
let ts = Date::now().as_millis();
if ts % 2 == 1 {
(StatusCode::OK, Json(TimeResponse::now()))
} else {
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse::error(
"40001".to_string(),
"账号或密码错误".to_string(),
)),
)
}
}

Rust 编译器要求: 所有 return 分支, Json 里面包裹的必须是'同一个固定类型'

1
2
3
4
5
6
# 这里被 Rust 推断两个不同类型, impl serde::Serialize 只能识别到首次的 TimeResponse 类型
if 条件 {
Json(TimeResponse) // 类型A
} else {
Json(ErrorResponse) // 类型B
}

为了方便通用响应返回, 采用了 #[serde(untagged)] 枚举声明

1
2
3
4
5
#[serde(untagged)]
pub enum AnyOutput {
Time(TimeResponse),
Error(ErrorResponse),
}

如果不加 #[serde(untagged)], 他会默认将枚举自行解构, 也就是返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这里 Time 多套一层
{
"Time": {
"time": 123
}
}

// 这里 Error 多套一层
{
"Error": {
"code": "40001",
"messages": { "error": "xxx" }
}
}

而加上 #[serde(untagged)] 就会自动将枚举层级解构掉, 变成以下类型

lines
1
2
3
4
5
6
7
8
9
10
11
12
// 系统自动删除掉枚举名称
{
"time": 123
}

// 其他也是解构
{
"code": "40001",
"messages": {
"error": "xxx"
}
}

handler/event.rs

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
use crate::model::*;
use axum::{extract::Form, http::StatusCode, response::Json};

/// POST /event/authorize 登录校验
pub async fn authorize(
Form(form): Form<EventAuthorizeRequest>,
) -> (StatusCode, Json<impl serde::Serialize>) {
if form.username == "meteorcat" && form.password == "123456" {
return (
StatusCode::OK,
Json(Responses::EventAuthorize(EventAuthorizeResponse {
uid: 0,
username: "meteorcat".to_string(),
token: "".to_string(),
})),
);
}

(
StatusCode::UNAUTHORIZED,
Json(Responses::Error(ErrorResponse::error(
"40001".to_string(),
"账号或密码错误".to_string(),
))),
)
}

/// POST /event/logout 注销
pub async fn logout() -> (StatusCode, Json<impl serde::Serialize>) {
(StatusCode::OK, Json(TimeResponse::now()))
}

这部分没什么好说的, 大部分业务采用 async 做异步函数调用即可, 之后就是暴露给外部调用

handler/mod.rs

1
2
3
4
5
6
7
pub mod root;
pub mod event;

// 导出供路由直接使用
pub use root::root;
pub use event::{authorize, logout};

后面如果扩展功能, 可以在这个 mod.rs 文件追加上异步函数处理

route/root_route.rs

1
2
3
4
5
6
7
8
use crate::handler::root;
use axum::{routing::get, Router};

/// 主页根路由分组
pub fn root_router() -> Router {
Router::new().route("/", get(root))
}

route/event_route.rs

1
2
3
4
5
6
7
8
9
10
use crate::handler::{authorize, logout};
use axum::{routing::post, Router};

/// event 业务路由分组
pub fn event_router() -> Router {
Router::new()
.route("/authorize", post(authorize))
.route("/logout", post(logout))
}

后面业务扩展函数就在这里定义追加

route/mod.rs

1
2
3
pub mod event_route;
pub mod root_route;

这里 lib.rs 直接就能引用, 不需要通过 pub use 直接暴露

lib.rs

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
//! nova-workers 主服务入口
//!
//! 利用 Cloudflare + Axum 实现的边缘 API 网关, 目前包含以下模块
//! - /event/authorize: 简单授权模块
//! - /event/logout: 注销授权模块

mod handler;
mod model;
mod route;

use crate::route::event_route::event_router;
use crate::route::root_route::root_router;
use axum::Router;
use tower_service::Service;
use worker::*;

/// 路由功能
///
/// 将对应 PATH 绑定到指定业务函数的映射功能
fn router() -> Router {
root_router().nest("/event", event_router())
}

/// 服务启动入口
///
/// 处理传入的 HTTP 请求事件, 并将其路由分发至对应的处理函数
#[event(fetch)]
async fn fetch(
req: HttpRequest,
_env: Env,
_ctx: Context,
) -> Result<axum::http::Response<axum::body::Body>> {
Ok(router().call(req).await?)
}

如果想要扩展二级路由, 可以在 router 函数之中附加多层路由, 这样重新编译启动之后就可以看到效果

cloudflare worker

这里记得切换 Wasm 编译器, 才能生成专门的 Wasm 包

提交发布

在项目配置编写完成后, 可以将 Worker 部署到 *.workers.dev 子域名上; 或者如果已配置了自定义域名也可以将其部署到该自定义域名上

这里可以调用命令发布

1
npx wrangler deploy

启动之后会跳转 https://dash.cloudflare.com 来请求授权发布, 跳转之后点击授权之后, 等待跳转授权完成即可, 之后可以关闭画面

cloudflare wrangler

他会在命令行之中显示以下内容:

1
2
3
4
5
6
7
8
Successfully logged in.
Total Upload: 405.26 KiB / gzip: 141.65 KiB
Worker Startup Time: 2 ms
Uploaded nova-workers (2.65 sec)
Deployed nova-workers triggers (1.08 sec)
https://{xxx}-{yyy}.dev
Current Version ID: d1dedb32-{xxx}-{yyy}
Before you go, Wrangler detected AI coding agents that may not be best configured to work with Cloudflare: Codex, Junie. Would you like Wrangler to automatically install Cloudflare skills for the best experience?

这里最后会检测使用的 AI 工具, 会提示针对 Cloudflare 完成最优配置, 这里随便 Y/n 都可以

完成之后就可以在 构建 - 计算 - Workers 和 Pages 看到提交的 Rust 应用已经发布

cloudflare project

之后点击进入该 Worker 功能, 在菜单上面找到 之中选择 添加域名 添加二级域名即可, 这样就发布自己的 Rust 应用