31 个版本

1.0.0 2016年3月24日
0.3.4 2024年8月19日
0.3.1 2024年7月25日
0.0.24 2024年3月29日
0.0.7 2021年11月5日

22网络编程

Download history 6030/week @ 2024-05-03 8582/week @ 2024-05-10 15385/week @ 2024-05-17 13598/week @ 2024-05-24 9611/week @ 2024-05-31 14704/week @ 2024-06-07 9927/week @ 2024-06-14 8727/week @ 2024-06-21 11327/week @ 2024-06-28 7919/week @ 2024-07-05 5250/week @ 2024-07-12 7130/week @ 2024-07-19 7551/week @ 2024-07-26 7929/week @ 2024-08-02 12805/week @ 2024-08-09 9271/week @ 2024-08-16

每月 39,349 次下载
用于 23 个 crate

Apache-2.0

365KB
7.5K SLoC

workers-rs crates.io docs.rs

工作进展中 的 Rust 与 Cloudflare Workers 环境的易用绑定。使用 Rust 编写整个 Worker!

阅读 注释和常见问题解答

示例用法

use worker::*;

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
    console_log!(
        "{} {}, located at: {:?}, within: {}",
        req.method().to_string(),
        req.path(),
        req.cf().unwrap().coordinates().unwrap_or_default(),
        req.cf().unwrap().region().unwrap_or("unknown region".into())
    );

    if !matches!(req.method(), Method::Post) {
        return Response::error("Method Not Allowed", 405);
    }

    if let Some(file) = req.form_data().await?.get("file") {
        return match file {
            FormEntry::File(buf) => {
                Response::ok(&format!("size = {}", buf.bytes().await?.len()))
            }
            _ => Response::error("`file` part of POST form must be a file", 400),
        };
    }

    Response::error("Bad Request", 400)
}

入门指南

项目使用 wrangler 运行和发布您的 Worker。

使用 cargo generate 从模板开始

$ cargo generate cloudflare/workers-rs

有几种模板可供选择。您应该会看到一个带有 src/lib.rs 的新项目布局。从这里开始!使用任何本地或远程 crate 和模块(只要它们编译为 wasm32-unknown-unknown 目标)。

准备好运行您的项目后,在本地运行您的 Worker

npx wrangler dev

最后,上线

# configure your routes, zones & more in your worker's `wrangler.toml` file
npx wrangler deploy

如果您想在您的机器上安装 wrangler,请参阅 wrangler 仓库 中的说明。

http 功能

worker 0.0.21 引入了一个 http 功能标志,它开始用广泛使用的类型替换自定义类型,这些类型来自 http crate。

这使得使用如 axumhyper 这样的标准类型 crate 变得更加容易。

这目前做了一些事情

  1. 引入了 Body,它实现了 http_body::Body 并是对 web_sys::ReadableStream 的简单包装。
  2. 使用 [event] 宏时,req 参数变为 http::Request<worker::Body>
  3. fetch 处理器的预期返回类型为 http::Response<B>,其中 B 可以是任何 http_body::Body<Data=Bytes>
  4. Fetcher::fetch_request 的参数是 http::Request<worker::Body>
  5. Fetcher::fetch_request 的返回类型是 Result<http::Response<worker::Body>>

最终结果是能够直接使用像 axum 这样的框架(见 示例

pub async fn root() -> &'static str {
    "Hello Axum!"
}

fn router() -> Router {
    Router::new().route("/", get(root))
}

#[event(fetch)]
async fn fetch(
    req: HttpRequest,
    _env: Env,
    _ctx: Context,
) -> Result<http::Response<axum::body::Body>> {
    Ok(router().call(req).await?)
}

我们还实现了在 worker::Requesthttp::Request<worker::Body>,以及在 worker::Responsehttp::Response<worker::Body> 之间的 try_from。这使得您可以在代码与原始类型紧密耦合的情况下逐步转换代码。

或者使用 Router

参数化路由并从处理器中访问参数值。每个处理器函数都接受一个 Request 和一个 RouteContextRouteContext 包含共享数据、路由参数、Env 绑定等等。

use serde::{Deserialize, Serialize};
use worker::*;

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {

    // Create an instance of the Router, which can use parameters (/user/:name) or wildcard values
    // (/file/*pathname). Alternatively, use `Router::with_data(D)` and pass in arbitrary data for
    // routes to access and share using the `ctx.data()` method.
    let router = Router::new();

    // useful for JSON APIs
    #[derive(Deserialize, Serialize)]
    struct Account {
        id: u64,
        // ...
    }
    router
        .get_async("/account/:id", |_req, ctx| async move {
            if let Some(id) = ctx.param("id") {
                let accounts = ctx.kv("ACCOUNTS")?;
                return match accounts.get(id).json::<Account>().await? {
                    Some(account) => Response::from_json(&account),
                    None => Response::error("Not found", 404),
                };
            }

            Response::error("Bad Request", 400)
        })
        // handle files and fields from multipart/form-data requests
        .post_async("/upload", |mut req, _ctx| async move {
            let form = req.form_data().await?;
            if let Some(entry) = form.get("file") {
                match entry {
                    FormEntry::File(file) => {
                        let bytes = file.bytes().await?;
                    }
                    FormEntry::Field(_) => return Response::error("Bad Request", 400),
                }
                // ...

                if let Some(permissions) = form.get("permissions") {
                    // permissions == "a,b,c,d"
                }
                // or call `form.get_all("permissions")` if using multiple entries per field
            }

            Response::error("Bad Request", 400)
        })
        // read/write binary data
        .post_async("/echo-bytes", |mut req, _ctx| async move {
            let data = req.bytes().await?;
            if data.len() < 1024 {
                return Response::error("Bad Request", 400);
            }

            Response::from_bytes(data)
        })
        .run(req, env).await
}

持久对象、KV、密钥 & 变量绑定

所有指向您的脚本(持久对象 & KV 命名空间、密钥、变量和版本)的“绑定”都可通过提供给入口点(此例中的 main)和路由处理器回调(在 ctx 参数中)的 env 参数访问,如果您使用的是来自 worker crate 的 Router

use worker::*;

#[event(fetch, respond_with_errors)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
    utils::set_panic_hook();

    let router = Router::new();

    router
        .on_async("/durable", |_req, ctx| async move {
            let namespace = ctx.durable_object("CHATROOM")?;
            let stub = namespace.id_from_name("A")?.get_stub()?;
            // `fetch_with_str` requires a valid Url to make request to DO. But we can make one up!
            stub.fetch_with_str("http://fake_url.com/messages").await
        })
        .get("/secret", |_req, ctx| {
            Response::ok(ctx.secret("CF_API_TOKEN")?.to_string())
        })
        .get("/var", |_req, ctx| {
            Response::ok(ctx.var("BUILD_NUMBER")?.to_string())
        })
        .post_async("/kv", |_req, ctx| async move {
            let kv = ctx.kv("SOME_NAMESPACE")?;

            kv.put("key", "value")?.execute().await?;

            Response::empty()
        })
        .run(req, env).await
}

有关如何配置这些绑定的更多信息,请参阅

持久对象

在 Rust 中定义持久对象

要使用 worker crate 定义持久对象,您需要在自己的结构体上实现 DurableObject 特性。此外,必须将 #[durable_object] 属性宏应用于结构体定义及其特性的 impl 块。

use worker::*;

#[durable_object]
pub struct Chatroom {
    users: Vec<User>,
    messages: Vec<Message>,
    state: State,
    env: Env, // access `Env` across requests, use inside `fetch`
}

#[durable_object]
impl DurableObject for Chatroom {
    fn new(state: State, env: Env) -> Self {
        Self {
            users: vec![],
            messages: vec![],
            state: state,
            env,
        }
    }

    async fn fetch(&mut self, _req: Request) -> Result<Response> {
        // do some work when a worker makes a request to this DO
        Response::ok(&format!("{} active users.", self.users.len()))
    }
}

在发布脚本时,您需要将脚本“迁移”以使其了解新的持久对象,并在您的 wrangler.toml 中包含一个绑定。

  • wrangler.toml 文件中包含持久对象绑定类型
# ...

[durable_objects]
bindings = [
  { name = "CHATROOM", class_name = "Chatroom" } # the `class_name` uses the Rust struct identifier name
]

[[migrations]]
tag = "v1" # Should be unique for each entry
new_classes = ["Chatroom"] # Array of new classes

队列

启用队列

由于队列处于测试阶段,您需要启用 queue 特性标志。

通过将其添加到你的 Cargo.toml 中的工作器依赖项来启用它

worker = {version = "...", features = ["queue"]}

示例工作器消费和产生消息

use worker::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone, Deserialize)]
pub struct MyType {
    foo: String,
    bar: u32,
}

// Consume messages from a queue
#[event(queue)]
pub async fn main(message_batch: MessageBatch<MyType>, env: Env, _ctx: Context) -> Result<()> {
    // Get a queue with the binding 'my_queue'
    let my_queue = env.queue("my_queue")?;

    // Deserialize the message batch
    let messages = message_batch.messages()?;

    // Loop through the messages
    for message in messages {
        // Log the message and meta data
        console_log!(
            "Got message {:?}, with id {} and timestamp: {}",
            message.body(),
            message.id(),
            message.timestamp().to_string()
        );

        // Send the message body to the other queue
        my_queue.send(message.body()).await?;

        // Ack individual message
        message.ack();

        // Retry individual message
        message.retry();
    }

    // Retry all messages
    message_batch.retry_all();
    // Ack all messages
    message_batch.ack_all();
    Ok(())
}

确保你已经在你的 wrangler.toml 中有正确的绑定

# ...
[[queues.consumers]]
queue = "myqueueotherqueue"
max_batch_size = 10
max_batch_timeout = 30


[[queues.producers]]
queue = "myqueue"
binding = "my_queue"

RPC 支持

workers-rsWorkers RPC 提供实验性支持。目前,这依赖于 JavaScript 绑定,可能需要一些手动使用 wasm-bindgen

目前尚未支持 RPC 的所有功能(或尚未测试),包括

  • 函数参数和返回值
  • 类实例
  • 存根转发

RPC 服务器

使用 workers-rs 编写 RPC 服务器相对简单。只需使用 wasm-bindgen 导出方法。这些方法将由 worker-build 自动检测并可供其他 Workers 使用。请参阅示例

RPC 客户端

创建用于调用另一个 Workers 的 RPC 方法的类型和绑定更为复杂。你需要编写更复杂的 wasm-bindgen 绑定和一些样板代码,以便与 RPC 方法进行更符合习惯的交互。请参阅示例

使用手动编写的绑定,应该可以使用 serde-wasm-bindgen 支持非原始参数和返回类型。

生成客户端绑定

有几种方法可以描述 RPC 接口。在底层,Workers RPC 使用 Cap'N Proto。一个可能的未来方向是让 Wasm 客户端包含 Cap'N Proto serde 支持,并直接与 RPC 协议通信,绕过 JavaScript。这可能涉及在 Cap'N Proto 架构中定义 RPC 接口,并从中生成 Rust 代码。

WebAssembly 社区中另一个流行的接口架构是 WIT。这是一个专为 WebAssembly 组件模型设计的轻量级格式。workers-rs 包含一个 实验性 代码生成器,允许你使用 WIT 描述你的 RPC 接口并生成 JavaScript 绑定,如rpc-client 示例所示。使用此代码生成器的最简单方法是使用构建脚本,如示例所示。此代码生成器处于预 alpha 阶段,没有支持保证,并且目前仅支持原始类型。

使用 Miniflare 进行测试

为了在本地测试你的 Rust 工作器,最佳方法是使用 Miniflare。然而,由于 Miniflare 是一个 Node 包,你需要在项目中使用 JavaScript 或 TypeScript 编写端到端测试。使用 Miniflare 编写测试的官方文档在此处可用。由于该文档专注于 JavaScript / TypeScript 代码库,你需要进行以下配置才能使其与基于 Rust、WASM 生成的 worker 一起工作

步骤 1:将 Wrangler 和 Miniflare 添加到你的 devDependencies

npm install --save-dev wrangler miniflare

步骤 2:在运行测试之前构建你的工作器

确保在测试链中调用以下命令以构建你的工作器,然后在运行测试之前

wrangler deploy --dry-run

默认情况下,这应该在项目的根目录下 ./build/ 目录中构建你的工作器。

步骤 3:在 JavaScript / TypeScript 测试中配置你的 Miniflare 实例

在测试中实例化 Miniflare 测试实例时,请确保配置其 scriptPath 选项为您的 JavaScript 工作线程入口生成的相对路径,并配置其 moduleRules 以便它能够解析从该 JavaScript 工作线程导入的 *.wasm 文件

// test.mjs
import assert from "node:assert";
import { Miniflare } from "miniflare";

const mf = new Miniflare({
  scriptPath: "./build/worker/shim.mjs",
  modules: true,
  modulesRules: [
    { type: "CompiledWasm", include: ["**/*.wasm"], fallthrough: true }
  ]
});

const res = await mf.dispatchFetch("http://127.0.0.1");
assert(res.ok);
assert.strictEqual(await res.text(), "Hello, World!");

D1 数据库

启用 D1 数据库

由于 D1 数据库处于 alpha 版本,您需要在 worker crate 中启用 d1 功能。

worker = { version = "x.y.z", features = ["d1"] }

示例用法

use worker::*;

#[derive(Deserialize)]
struct Thing {
	thing_id: String,
	desc: String,
	num: u32,
}

#[event(fetch, respond_with_errors)]
pub async fn main(request: Request, env: Env, _ctx: Context) -> Result<Response> {
	Router::new()
		.get_async("/:id", |_, ctx| async move {
			let id = ctx.param("id").unwrap()?;
			let d1 = ctx.env.d1("things-db")?;
			let statement = d1.prepare("SELECT * FROM things WHERE thing_id = ?1");
			let query = statement.bind(&[id])?;
			let result = query.first::<Thing>(None).await?;
			match result {
				Some(thing) => Response::from_json(&thing),
				None => Response::error("Not found", 404),
			}
		})
		.run(request, env)
		.await
}

注意事项和常见问题解答

看到这样一个框架能实现多少可能性非常令人兴奋,因为它扩大了开发者基于 Workers 平台构建时的选择。然而,还有很多工作要做。请期待一些粗糙的边缘,一些未实现的 API,以及可能的一些错误。这里要强调的是,一些可能在您的 Rust 代码中工作良好的功能可能在这里不起作用——最终它都是 WebAssembly,如果您的代码或第三方库没有针对 wasm32-unknown-unknown 进行优化,它们就不能在 Workers 上使用。此外,您必须在家放弃线程化的异步运行时;这意味着没有 Tokio 或 async_std 支持。但是,当您使用 worker crate 时,async/await 语法仍然可用且得到原生支持。

我们完全打算支持这个 crate 并继续构建其缺失的功能,但您的帮助和反馈是必不可少的。我们不希望在真空中构建,并且我们非常幸运地拥有像您这样的卓越客户,他们可以帮助我们打造更好的产品。

所以请试试看,留下一些反馈,并星标这个仓库,以鼓励我们投入更多的时间和资源来支持这类项目。

如果您对此感兴趣并想帮忙,我们很乐意让外部贡献者开始。我们知道有很多改进可以做到,比如与流行的 Rust HTTP 生态系统类型兼容(如果您想实现,我们有一个针对 Headers 的示例转换),实现额外的 Web API,实用 crate,等等。事实上,我们一直在寻找优秀的工程师,并且正在招聘许多开放职位——请 查看

常见问题解答

  1. 我能否部署使用 tokioasync_std 运行时的 Worker?
  • 目前不行。您的 Worker 项目的所有 crate 都必须编译为 wasm32-unknown-unknown 目标,这在某些方面比 x86 和 ARM64 的目标更有限。
  1. worker crate 没有 X!为什么?
  • 很可能应该有,但我们还没有时间完全实现它或添加一个包装 FFI 的库。请通过 提交一个 issue 告诉我们您需要这个功能。
  1. 我的包大小超过了 Workers 的大小限制,我该怎么办?

⚠️ 注意事项

  1. 将 worker 包升级到版本 0.0.18 或更高版本
  • 当将您的worker升级到版本 0.0.18 时,可能会出现错误 "error[E0432]: unresolved import crate::sys::IoSourceState"。在这种情况下,将 package.editionwrangler.toml 中升级到 edition = "2021"
[package]
edition = "2021"

发布

  1. 触发 一个工作流来创建发布PR。
  2. 审查版本更改并合并PR。
  3. 将创建一个GitHub发布草稿。编写发布说明并在准备好时发布。
  4. Crates(《worker-sys》、《worker-macros》、《worker》)将自动发布。

贡献

欢迎并感谢您的反馈!请使用问题跟踪器来讨论潜在的实现或提出功能请求。如果您对提交PR感兴趣,我们建议尽早提出一个关于您想做出的更改的问题。

项目内容

  • worker:面向用户的crate,通过封装和方便库在Rust<->JS/WebAssembly互操作中提供Rust熟悉的抽象。
  • worker-sys:与Workers JS运行时FFI兼容的Rust extern "C"定义。
  • worker-macros:导出 eventdurable_object 宏,用于将Rust入口点封装在ES模块的 fetch 方法中,并生成代码以创建和交互Durable Objects。
  • worker-sandbox:用于测试功能和用户体验的Cloudflare Worker。
  • worker-build:用于基于 workers-rs 的项目的跨平台构建命令。

依赖关系

~13–26MB
~390K SLoC