依托目前 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 模板可以测试下内部代码业务 # leptos 是一套完整前后端 SSR 架构全栈框架(依赖很重的框架), 这部分有兴趣可以去了解 # 如果需要跨编译器处理建议采用 axum 原生框架编写, 所以我采用这部分开发 # # 之后会提示启动 `panic=unwind` 错误响应, 这里 panic=unwind 会让 `panic!` 崩溃时展开调用栈并输出完整堆栈日志 # 虽然输出堆栈异常信息可以高效精准获取到异常错误, 但是会导致 wasm 产物体积变大且不会理解抛出异常终端 # 所以建议保持默认设置的 false , 避免 wasm 编译产物膨胀和异常中断的问题 #
我这边采用 IDEA + Rust插件 来启动工程项目, 其他开发工具应该也是差不多, 出问题的时候参照官方文档来处理即可
这里说明下生成项目下的文件和目录具体功能:
等待完成编译结束, 可以执行以下命令来直接启动本地测试服务器
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 use axum::{routing::get, Router};use tower_service::Service;use worker::*;fn router () -> Router { Router::new ().route ("/" , get (root)) } pub async fn root () -> &'static str { "Hello Axum!" } #[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 use axum::{routing::get, response::Json, Router};use axum::http::StatusCode;use tower_service::Service;use worker::*;use serde::Serialize;fn router () -> Router { Router::new ().route ("/" , get (root)) } #[derive(Debug, Serialize)] pub struct TimeResponse { time: u64 , } pub async fn root () -> (StatusCode, Json<TimeResponse>) { let ts = Date::now ().as_millis (); (StatusCode::OK, Json (TimeResponse { time: ts })) } #[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 业务路由分组
这里拆分出来几个文件可以按照个人习惯区分即可, 这里仅仅作为参考
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 >, } 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 } } } #[derive(Debug, Deserialize)] pub struct EventAuthorizeRequest { pub username: String , pub password: String , } #[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};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 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};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 (), ))), ) } 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};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 直接暴露
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 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::*;fn router () -> Router { root_router ().nest ("/event" , event_router ()) } #[event(fetch)] async fn fetch ( req: HttpRequest, _env: Env, _ctx: Context, ) -> Result <axum::http::Response<axum::body::Body>> { Ok (router ().call (req).await ?) }
如果想要扩展二级路由, 可以在 router 函数之中附加多层路由, 这样重新编译启动之后就可以看到效果
这里记得切换 Wasm 编译器, 才能生成专门的 Wasm 包
提交发布
在项目配置编写完成后, 可以将 Worker 部署到 *.workers.dev 子域名上; 或者如果已配置了自定义域名也可以将其部署到该自定义域名上
这里可以调用命令发布
启动之后会跳转 https://dash.cloudflare.com 来请求授权发布, 跳转之后点击授权之后, 等待跳转授权完成即可, 之后可以关闭画面
他会在命令行之中显示以下内容:
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 应用已经发布
之后点击进入该 Worker 功能, 在菜单上面找到 域 之中选择 添加域名 添加二级域名即可, 这样就发布自己的 Rust 应用