跳到主要内容

72 篇博文 含有标签「Rust」

查看所有标签

深入 Dioxus 信号系统

1. 创建信号

在 Dioxus 中,可以使用 use_state 钩子创建状态:

let count = use_state(&cx, || 0);
  • 解释use_state 接受一个上下文引用 &cx 和一个初始化函数,返回一个状态和一个用于更新状态的函数。

2. 读取和更新信号

读取和更新状态的方式如下:

let current_value = *count; // 读取当前值
count.set(new_value); // 设置新值
  • 解释
    • *count:解引用以获取当前状态的值。
    • count.set(new_value):更新状态为 new_value,并触发组件重新渲染。

3. 状态更新的核心原理:基于 PartialEq 的变化检测

Dioxus 的信号系统通过底层实现的 PartialEq trait 来判断状态值的变化,从而决定是否触发组件的重新渲染。

原理讲解

  1. 信号变化检测

    • 每次调用 set 方法更新信号时,Dioxus 会通过调用值的 PartialEq 方法比较新值和旧值。
    • 如果 PartialEq::eq 返回 true(即值相等),Dioxus 会跳过重新渲染。
    • 如果 PartialEq::eq 返回 false(即值不相等),Dioxus 会将信号标记为“已变化”,并通知依赖此信号的组件重新渲染。
  2. 示例代码

    fn Counter(cx: Scope) -> Element {
    let count = use_state(&cx, || 0);

    cx.render(rsx! {
    button {
    onclick: move |_| count.set(*count + 1),
    "Count: {count}"
    }
    })
    }
    • count.set(*count + 1) 被调用时:
      1. count 的新值(*count + 1)与旧值(*count)通过 PartialEq 比较。
      2. 如果值不同,则触发组件重新渲染。
  3. 为什么使用 PartialEq

    • 性能优化:通过 PartialEq 判断是否需要重新渲染,避免不必要的性能开销。
    • 灵活性:支持自定义比较逻辑。开发者可以为自定义类型实现 PartialEq,仅比较关键字段或按需定义相等性规则。
  4. 自定义类型的 PartialEq 实现: 如果你有一个自定义数据类型,可以通过实现 PartialEq 控制变化检测逻辑。例如:

    #[derive(Clone)]
    struct CustomData {
    field1: i32,
    field2: String,
    }

    // 实现 PartialEq
    impl PartialEq for CustomData {
    fn eq(&self, other: &Self) -> bool {
    self.field1 == other.field1 // 仅比较 field1
    }
    }

原理优势

  • 避免冗余渲染:与 React 的 Virtual DOM diff 不同,Dioxus 在状态变化时直接通过 PartialEq 比较值,大幅减少无效计算。
  • 精细化控制:开发者可以通过自定义 PartialEq 提供更高的灵活性。
  • 内置优化:对基本类型如 i32StringPartialEq 比较成本极低。

4. 小结

Dioxus 的信号系统基于 PartialEq 实现细粒度的变化检测和状态更新机制。这种机制为开发者提供了:

  1. 高效的状态管理:减少不必要的重新渲染,提高性能。
  2. 灵活的比较逻辑:通过自定义 PartialEq 实现更复杂的状态变化判断。
  3. 简单的 API:通过自动追踪和更新依赖,简化组件开发。

这种自动化和精细化的状态管理方案,使得 Dioxus 在响应式编程中具有独特优势,为开发者带来更佳的开发体验。

鱼雪

引言

在现代前端开发中,状态管理是一个核心话题。React 通过 useStateuseReducer 等 Hooks 实现状态管理, 而 Dioxus 则采用了基于信号(Signal)的响应式方案。

本文将深入对比这两种方案的异同,帮助你更好地理解和使用 Dioxus 的状态管理系统。

基础概念对比

React 的状态管理

在 React 中,我们通常这样管理状态:

function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}

Dioxus 的信号系统

而在 Dioxus 中,使用信号来管理状态:

fn Counter(cx: Scope) -> Element {
let count = use_state(&cx, || 0);

cx.render(rsx! {
button {
onclick: move |_| count.set(*count + 1),
"Count: {count}"
}
})
}

核心区别

  1. 状态更新机制

    • React: 通过 setState 触发重渲染。
    • Dioxus: 通过信号的变化自动触发组件更新, 本质上通过 PartialEq 实现自动更新
  2. 响应式特性

    • React: 需要手动处理依赖关系(如在 useEffect 中指定依赖)。
    • Dioxus: 自动追踪依赖,实现细粒度更新。

深入 Dioxus 信号系统

1. 创建信号

在 Dioxus 中,可以使用 use_state 钩子创建状态:

let count = use_state(&cx, || 0);
  • 解释use_state 接受一个上下文引用 &cx 和一个初始化函数,返回一个状态和一个用于更新状态的函数。

2. 读取和更新信号

读取和更新状态的方式如下:

let current_value = *count; // 读取当前值
count.set(new_value); // 设置新值
  • 解释
    • *count:解引用以获取当前状态的值。
    • count.set(new_value):更新状态为 new_value,并触发组件重新渲染。

3. 派生状态

在 React 中:

function DoubleCounter() {
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => count * 2, [count]);

return <div>Double: {doubleCount}</div>;
}

在 Dioxus 中,可以使用 use_memo 创建派生状态:

fn DoubleCounter(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
let double_count = use_memo(&cx, move || *count * 2, (*count,));

cx.render(rsx! {
div { "Double: {double_count}" }
})
}
  • 解释
    • use_memo 接受上下文引用、计算函数和依赖元组,返回计算结果。当依赖发生变化时,重新计算。

4. 异步状态处理

Dioxus 提供了 use_future 用于处理异步状态:

fn AsyncCounter(cx: Scope) -> Element {
let count = use_state(&cx, || 0);

let async_value = use_future(&cx, (*count,), |&count| async move {
// 异步操作
gloo_timers::future::TimeoutFuture::new(100).await;
count * 2
});

cx.render(rsx! {
div { "Async value: {async_value.value().unwrap_or(&0)}" }
})
}
  • 解释
    • use_future 接受上下文引用、依赖元组和异步函数,返回一个包含异步结果的状态。当依赖变化时,重新执行异步函数。

状态共享方案对比

1. Props 传递

React:

function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} setCount={setCount} />;
}

Dioxus:

fn Parent(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
cx.render(rsx! {
Child { count: *count, set_count: count.setter() }
})
}
  • 解释:在 Dioxus 中,count.setter() 返回一个用于更新状态的函数,可以作为属性传递给子组件。

2. Context 使用

React:

const CountContext = createContext();

function Provider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
{children}
</CountContext.Provider>
);
}

Dioxus:

#[derive(Clone)]
struct CountState {
count: UseState<i32>,
}

fn Provider(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
cx.provide_context(CountState { count });

cx.render(rsx! {
// 子组件
})
}
  • 解释provide_context 用于在组件树中提供上下文,子组件可以使用 use_context 获取共享的状态。

3. 全局状态

Dioxus 支持全局状态管理:

static COUNT: AtomRef<i32> = |_| 0;

fn GlobalCounter(cx: Scope) -> Element {
let count = use_atom_ref(&cx, COUNT);
cx.render(rsx! {
button {
onclick: move |_| *count.write() += 1,
"Global count: {count.read()}"
}
})
}
  • 解释
    • AtomRef 定义了一个全局状态。
    • use_atom_ref 用于在组件中访问全局状态的引用,readwrite 方法用于读取和修改全局状态的值。

最佳实践建议

  1. 选择合适的状态范围

    • 局部状态:使用 use_state
    • 共享状态:使用 Context
    • 全局状态:使用 AtomRef
  2. 性能优化

    • 合理使用 use_memo 缓存派生状态。
    • 避免状态嵌套过深。
    • 尽量将状态分解为粒度更细的信号。
  3. 组件设计

    • 使用明确的属性接口。
    • 将复杂状态逻辑抽象为自定义 Hook。
    • 对于全局状态,尽量使用模块化设计避免耦合。

总结

Dioxus 的信号系统提供了一种更加自动化和细粒度的状态管理方案。相比 React 的手动依赖追踪,它能够:

  1. 自动追踪和更新依赖。
  2. 提供更简洁的 API。
  3. 实现更高效的细粒度更新。
  4. 更自然地处理异步状态。

虽然学习曲线可能稍陡,但掌握后能够带来更好的开发体验和运行时性能。

参考资料


本文首发于 https://yuxuetr.com,转载请注明出处。

鱼雪

发布日期:2025年1月1日

🎉 新年快乐!

我们很高兴地宣布发布 axum 0.8.0 版本。 axum 是一个基于 tokio、tower 和 hyper 构建的人性化且模块化的 Web 框架。

此次发布还包括 axum-coreaxum-extraaxum-macros 的新主要版本。

主要更新

以下是本次版本中最值得注意的一些变化:

路径参数语法变更

路径参数的语法已从 /:single/*many 更改为 /{single}/{*many}

这一更改有多个原因,其中最重要的是旧语法不允许在路由定义中使用前导 :* 字符。新的语法是在升级到 matchit 0.8 后引入的,类似于 format!() 宏的格式,并且也是 OpenAPI 描述中使用的语法。转义使用双大括号 {},因此如果你想匹配字面上的 {} 字符,可以写成 {{}}

我们理解这对几乎所有 axum 用户来说都是一次破坏性更新,但我们认为现在进行更改比以后在更多用户依赖旧语法时再改更为合适。迁移路径也相对简单,希望这一变化不会给您带来太大困扰。

更多信息和迁移示例可在相应的 Pull Request 中找到。 感谢 David Mládek 在 axum 中的实现,以及 Ibraheem Ahmed 在 matchit 上的持续工作。

Option<T> 作为提取器

Option<T> 作为提取器的使用方式已发生变化。之前,任何来自 T 提取器的拒绝都会被简单地忽略并转换为 None

现在,Option<T> 作为提取器需要 T 实现新的 trait OptionalFromRequestParts(或 OptionalFromRequest)。

这使得处理来自 T 提取器的拒绝并将其转换为错误响应成为可能,同时仍然允许提取器是可选的。

例如,假设你有一个需要请求中存在有效令牌的 AuthenticatedUser 提取器,但在某些情况下认证是可选的。 现在你可以使用 Option<AuthenticatedUser> 作为提取器,而不会失去在令牌无效或数据库连接失败时返回错误响应的能力。

感谢 Jonas Platte 提交的引入这一新功能的 Pull Request。

移除 #[async_trait]

在 2023 年末,Rust 团队使得在 trait 中使用 impl Future<Output = _> 成为可能。 这一特性称为 trait 中的位置返回 impl Trait,这意味着我们不再需要 #[async_trait] 宏来定义 trait 中的异步方法。

这一变化主要影响我们的 FromRequestPartsFromRequest traits,因为它们使用异步方法。 如果你有实现这些 traits 的自定义提取器,需要移除其中的 #[async_trait] 注解。

这一更改由郑力(Zheng Li)实现。感谢你的贡献!

详细变更日志

以下是 axum 0.8.0 版本的详细变更日志:

破坏性更改 (Breaking Changes)

  • 升级 matchit 到 0.8:路径参数语法从 /:single/*many 更改为 /{single}/{*many};旧语法会导致 panic 以避免行为的静默变化。#2645
  • 要求所有处理器和服务为 Sync:新增到 Router 和 MethodRouter 中的所有处理器和服务现在需要实现 Sync#2473
  • 元组路径提取器的参数检查:元组和元组结构的 Path 提取器反序列化器现在会严格检查参数数量是否与元组长度匹配。#2931
  • 移动 Host 提取器到 axum-extra#2956
  • 移除 WebSocket::close:用户需要显式发送关闭消息。#2974
  • 使 serve 泛化:针对监听器和 IO 类型进行泛化。#2941
  • 移除 Serve::tcp_nodelay 和 WithGracefulShutdown::tcp_nodelay:参见 serve::ListenerExt 以设置任意 TCP 流属性。#2941
  • Option<Path<T>> 行为变化:不再吞噬所有错误条件,而是在许多情况下拒绝请求;详见文档。#2475
  • WebSocket Message 类型变化axum::extract::ws::Message 现在使用 Bytes 代替 Vec<u8>,并引入了 Utf8Bytes 类型代替 String#3078

修复 (Fixes)

  • 跳过 SSE 不兼容字符:在 Event::json_data 中跳过 serde_json::RawValue 的不兼容字符。#2992
  • 路径段使用数组类型时避免 panic#3039
  • 避免在中间件前设置 content-length:允许中间件为请求添加主体而无需手动设置 content-length。#2897

变更 (Changes)

  • 更新最低 Rust 版本:提升到 Rust 1.75。#2943
  • 升级 tokio-tungstenite 到 0.26#3078
  • Query/Form 解析错误报告:使用 serde_path_to_error 报告解析失败的字段。#3081

新增功能 (Added)

  • 添加 method_not_allowed_fallback:在路径匹配但没有对应 HTTP 方法处理器时设置回退处理器。#2903
  • 添加 NoContent 快捷方式:用于 StatusCode::NO_CONTENT 的自描述快捷方式。#2978
  • 支持 HTTP/2 上的 WebSockets:通过将 get(ws_endpoint) 处理器更改为 any(ws_endpoint) 来启用。#2894
  • 添加 MethodFilter::CONNECT 和相关路由功能#2961
  • 扩展 FailedToDeserializePathParams::kind 枚举:新增 ErrorKind::DeserializeError 变体,以捕获命名路径参数解析错误的键、值和消息。#2720

查看完整更新日志

此次发布还有许多其他更改,包括新功能、错误修复以及不太明显的破坏性更改。我们鼓励您阅读更新日志以了解所有更改!

您可以在 axum-core 的更新日志 中找到更多相关的更改。

如果在更新过程中遇到问题,请在 GitHub 上发起讨论,或在 Discord 上提问。

最后,我们要感谢所有帮助实现此次发布的贡献者。感谢你们的辛勤工作!

参考链接

鱼雪

引言

在 2023 年 12 月,一件小奇迹发生了:Traits 中的 async fn 正式发布。

自 Rust 1.39 版本起,我们已经拥有了独立的异步函数:

pub async fn read_hosts() -> eyre::Result<Vec<u8>> {
// 等等
}

以及在 impl 块中的异步函数:

impl HostReader {
pub async fn read_hosts(&self) -> eyre::Result<Vec<u8>> {
// 等等
}
}

但我们仍然无法在 Traits 中使用异步函数:

use std::io;

trait AsyncRead {
async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}

当尝试编译时,会出现如下错误:

❯ cargo +1.74.0 check --quiet
error[E0706]: functions in traits cannot be declared `async`
--> src/main.rs:9:5
|
9 | async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
| -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| `async` because of this
|
= note: `async` trait functions are not currently supported
= note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
= note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information

For more information about this error, try `rustc --explain E0706`.
error: could not compile `sansioex` (bin "sansioex") due to previous error

Cool Bear 的热贴士

cargo +channel 语法有效

cargo +channel 语法有效,因为这里的 cargo 是由 rustup 提供的一个 shim。

有效的 channel 名称看起来像 x.y.zstablebetanightly 等——与 rust-toolchain.toml 文件或任何其他 toolchain override 中遇到的名称相同。

使用 async-trait crate

长期以来,推荐使用 async-trait crate 来在 Traits 中使用异步函数:

use std::io;

#[async_trait::async_trait]
trait AsyncRead {
async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}

这种方法有效,但它改变了 Trait 定义(以及任何实现),使其返回已固定、盒装的 futures。

盒装 futures?

是的!这些是分配在堆上的 futures

为什么?

因为 futures——异步函数返回的值——可能具有不同的大小!

局部变量的大小

下面这个函数返回的 future 大小:

async fn foo() {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
println!("done");
}

与这个函数相比:

async fn bar() {
let mut a = [0u8; 72];
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
for _ in 0..10 {
a[0] += 1;
}
println!("done");
}

bar 函数的 future 大小更大,因为它包含了更多的状态需要跟踪:

❯ cargo run --quiet
foo: 128
bar: 200

因为 bar 函数中的数组在异步睡眠期间没有被释放——所有内容都存储在 future 中。

这就是一个问题,因为通常,当我们调用一个函数时,我们希望知道应该为返回值保留多少空间:我们说返回值是“有尺寸的”。

而在这里,“我们”实际上指的是编译器——为局部变量在栈上保留空间是函数被调用时的第一步之一。

在反汇编中可以看到:

❯ cargo asm sansioex::main --quiet --simplify --color | head -5

sansioex::main:
Lfunc_begin45:
sub sp, sp, #256
stp x20, x19, [sp, #224]
stp x29, x30, [sp, #240]

这里的 sub 指令总共保留了 256 字节。

Cool Bear 的热贴士

cargo asm 子命令

这里展示的 cargo asm 子命令来自 cargo-show-asm,可以通过以下命令安装:

cargo install --locked --all-features cargo-show-asm

原始的 cargo-asm crate 仍然有效,但功能较少,自 2018 年以来没有更新。

总结

在 Rust 中,Traits 中的异步函数的引入是一个重要的进展,但也带来了新的挑战, 如 async-trait crate 带来的 boxed futures 问题。 这一变化背后的原因主要是由于异步函数返回的 futures 大小不固定,编译器需要在编译时为函数返回值保留固定大小的空间。

通过理解这些技术细节,Rust 开发者可以更好地利用 Rust 的异步编程特性,同时避免潜在的性能问题和复杂性。

参考

鱼雪

在开发名为 Textpod(用 Rust 编写的笔记应用)时,我需要自动化地在 GitHub 上完成构建和发布。 以下内容(以及相应的 YAML 配置文件)演示了整套自动化流程的配置步骤,包括:

  1. 为 Windows、Linux、macOS (Intel + ARM) 构建二进制
  2. 将这些构建产物及校验文件上传到最新的 GitHub Release
  3. 发布到 crates.io
  4. 构建面向 amd64 和 arm64 的精简 Docker 镜像,并推送到 Docker Hub

一、触发条件:GitHub Release

首先,我们以 release 事件作为执行触发器。YAML 配置大致如下:

on:
release:
types:
- created

当我们在 GitHub 中创建一个新的 Release 时,这些后续的构建及发布任务就会自动执行。


二、在 Linux 环境中构建 Linux 与 Windows 二进制

可以在同一个 Linux 环境中通过 rustup 对不同的目标进行交叉编译(例如 Windows 和 Linux)。

jobs:
linux_windows:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v2

- name: Install Linux and Windows Cross Compilers
run: sudo apt-get install --yes --no-install-recommends musl-tools gcc-mingw-w64-x86-64-win32

- name: Install rustup targets
run: rustup target add x86_64-unknown-linux-musl x86_64-pc-windows-gnu

- name: Build the executable
run: cargo build --release --target x86_64-unknown-linux-musl --target x86_64-pc-windows-gnu

- name: Tar x86_64 binary
run: tar -czvf textpod-gnu-linux-x86_64.tar.gz -C target/x86_64-unknown-linux-musl/release textpod

- name: Zip windows binary
run: zip -j textpod-windows.zip target/x86_64-pc-windows-gnu/release/textpod.exe

- name: Generate SHA256 checksums
run: |
shasum -a 256 textpod-gnu-linux-x86_64.tar.gz > textpod-gnu-linux-x86_64.tar.gz.sha256
shasum -a 256 textpod-windows.zip > textpod-windows.zip.sha256

- name: Upload release binaries
uses: alexellis/upload-assets@0.4.0
env:
GITHUB_TOKEN: ${{ github.token }}
with:
asset_paths: '["textpod-gnu-linux-x86_64.tar.gz", "textpod-windows.zip", "textpod-gnu-linux-x86_64.tar.gz.sha256", "textpod-windows.zip.sha256"]'

构建说明

  1. Step 4 (cargo build) 一次性构建两个目标:
    • x86_64-unknown-linux-musl
    • x86_64-pc-windows-gnu
  2. 后续分别将 Linux 产物打包为 tar.gz 文件、Windows 产物打包为 zip 文件。
  3. 生成各自的 SHA256 校验文件:textpod-gnu-linux-x86_64.tar.gz.sha256textpod-windows.zip.sha256
  4. 使用 alexellis/upload-assets@0.4.0 Action 将这四个文件一并上传到 GitHub Release。

注意${{ github.token }} 是 GitHub 自动提供的内置 Token,不需要你额外创建或配置。


三、在 macOS 环境中构建 x86 和 ARM 二进制

对 macOS 的构建与上述类似,但需要指定在 macOS 平台下(runs-on: macos-latest)进行,并添加两个目标:Intel 和 ARM。

rustup target add x86_64-apple-darwin aarch64-apple-darwin
cargo build --release --target=x86_64-apple-darwin --target=aarch64-apple-darwin

同理,你可以将打包、校验、上传至 Release 等步骤与 Linux/Windows 的做法相结合,为 macOS 平台生成并上传相应产物。


四、发布到 crates.io

在完成了所有平台的构建(Linux、Windows、macOS)后,可以通过以下方式将包发布到 crates.io:

  crates:
runs-on: ubuntu-latest
needs: [linux_windows, macos]
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
  • needs: [linux_windows, macos] 表示这个 job 必须等前面的两个构建都成功后才会执行。
  • 你需要在 crates.io 创建一个 API Token,并将其作为机密变量(CARGO_REGISTRY_TOKEN)添加到 GitHub 的 secrets 中。

五、构建并推送 Docker 镜像至 Docker Hub

最后,可以构建面向 amd64 和 arm64 平台的 Docker 镜像,然后推送到 Docker Hub:

  docker:
runs-on: ubuntu-latest
needs: crates
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: freetonik/textpod:latest

说明

  • docker/build-push-action@v6 可以自动构建多架构镜像。
  • 在构建之前,先使用 setup-qemu-actionsetup-buildx-action 来启用对多平台(amd64、arm64)的支持。
  • 将 Docker Hub 的认证信息 (DOCKERHUB_USERNAME, DOCKERHUB_TOKEN) 配置为 GitHub secrets。
  • 构建完成后,镜像会被推送到 freetonik/textpod:latest,你可以换成自己的 Docker Hub 命名空间和仓库名称。

需要注意,多平台构建常常比较耗时,可能需要 20-40 分钟左右。


六、最终效果

  • GitHub Release:创建新的 Release 后,一开始只会显示源代码链接;几分钟后,构建完成并将二进制文件(以及校验文件)上传到 Release 页。
  • Docker 镜像:我们使用了 rust:alpine 作为基础镜像(参见项目中的 Dockerfile),通常可获得体积只有约 10 MB 的容器镜像,已发布在 Docker Hub。

以下是项目目录示例,可在对应仓库的 .github/workflows/ 中查看 YAML 文件的历史版本。
整个流程可以保证在一次 Release 后,自动完成所有平台的编译、打包、上传与发布。

小结:这样就轻松搞定了多平台二进制 + Docker 镜像 + crates.io 同步发布的完整 CI/CD。


参考链接

鱼雪

引言

在当今数字化时代,实时数据处理已成为企业的核心需求。 Fluvio作为一个精简的分布式流处理引擎,专为边缘到核心的流处理而设计, 提供了性能、可扩展性和可编程性的完美结合。

本文将深入探讨Fluvio的核心特性、技术优势及其实践应用。

核心特性

云原生设计

Fluvio的设计理念完全遵循云原生原则,具备以下特点:

  • 声明式管理:降低管理负担
  • Kubernetes原生:无缝集成K8s环境
  • 水平扩展:满足数据弹性需求
  • 自我修复:无需人工干预即可从故障中恢复

边缘计算优化

Fluvio在边缘计算场景下表现出色:

  • 轻量级:仅37MB的单一二进制文件,完美支持ARM64 IoT设备
  • 事件驱动:采用异步架构,支持大规模I/O操作
  • 多线程架构:充分利用多核CPU性能
  • 高性能:内部组件处理延迟达到纳秒级

AI原生支持

为AI应用开发提供强大支持:

  • 自定义数据生命周期管理
  • 支持长期运行的数据和AI管道
  • 提供声明式API用于流处理和数据物化

技术亮点

1. Rust驱动

采用Rust语言开发,确保了:

  • 高性能执行
  • 内存安全
  • 跨平台兼容性

2. WebAssembly集成

通过WebAssembly提供:

  • 安全的沙箱执行环境
  • 高性能的自定义流处理逻辑
  • 语言无关的开发能力

3. 云原生控制平面

借鉴Kubernetes的设计理念:

  • 采用声明式编程
  • 实现最终一致性
  • 简化运维管理

实践应用

快速入门

# 安装Fluvio版本管理器
curl -fsS https://hub.infinyon.cloud/install/install.sh | bash

# 启动本地集群
fluvio cluster start

# 创建主题
fluvio topic create quickstart-topic

# 生产数据
fluvio produce quickstart-topic

# 消费数据
fluvio consume quickstart-topic

SmartModule转换示例

# transforms.yml
transforms:
- uses: infinyon/jolt@0.4.1
with:
spec:
- operation: shift
spec:
quote: ""

Fluvio vs 主流流处理解决方案对比

为了帮助读者更好地理解Fluvio的优势,我们将其与市场上主流的流处理解决方案进行对比:

功能对比表

特性FluvioApache KafkaApache FlinkApache Spark Streaming
部署大小37MB180MB+290MB+300MB+
边缘计算支持✅ 原生支持❌ 需要额外组件⚠️ 有限支持❌ 不适用
WebAssembly支持✅ 内置❌ 不支持❌ 不支持❌ 不支持
开发语言RustJava/ScalaJavaScala/Java
资源消耗极低中等较高
启动时间秒级分钟级分钟级分钟级
延迟性能纳秒级毫秒级毫秒级秒级
学习曲线平缓中等陡峭陡峭

Fluvio的独特优势

  1. 轻量级架构

    • 相比其他解决方案动辄数百MB的部署体积,Fluvio仅需37MB
    • 更适合边缘计算和IoT场景
    • 启动速度更快,资源占用更少
  2. 现代化技术栈

    • 采用Rust语言开发,提供内存安全保证
    • 原生WebAssembly支持,实现安全的自定义处理逻辑
    • 完全的云原生设计,更好地适应现代架构需求
  3. 简化的运维

    • 单一二进制文件部署
    • 自动化的集群管理
    • 声明式配置,降低运维复杂度
  4. 性能优势

    • 纳秒级延迟,优于传统毫秒级解决方案
    • 高效的资源利用,降低运营成本
    • 优化的多线程架构,提供更好的并发性能

适用场景对比

场景最佳选择原因
边缘计算Fluvio轻量级、低延迟、资源消耗小
大规模数据处理Apache Spark成熟的生态系统、强大的批处理能力
实时流处理Fluvio/FlinkFluvio适合边缘场景,Flink适合大规模集中式处理
消息队列Kafka成熟的消息队列系统,高可靠性
AI/ML管道Fluvio原生AI支持、WebAssembly集成、低延迟

迁移建议

如果您正在考虑采用Fluvio,以下场景特别适合:

  1. 边缘计算项目

    • IoT设备数据处理
    • 边缘AI推理
    • 实时数据过滤和转换
  2. 云原生应用

    • Kubernetes环境部署
    • 微服务架构集成
    • 实时数据管道
  3. AI/ML工作负载

    • 实时特征工程
    • 模型推理管道
    • 数据预处理流程

成本效益分析

相比传统解决方案,Fluvio可以带来显著的成本节省:

  • 硬件成本: 由于更低的资源消耗,可减少50-70%的硬件投入
  • 运维成本: 简化的部署和管理流程可降低30-50%的运维工作量
  • 开发成本: WebAssembly支持和现代化API可提升40%的开发效率
  • 能源成本: 得益于高效的资源利用,可节省40-60%的能源消耗

总结

Fluvio通过其独特的设计和强大的功能,为现代数据处理提供了一个强有力的解决方案。 它不仅满足了云原生环境的需求,还特别适合边缘计算场景,同时为AI应用开发提供了便利的工具和接口。

参考资源

鱼雪

摘要

本文通过现实案例展示了 Windsurf AI 在 Rust 开发工作流中的颠覆性影响,工作效率提高至以前的三倍,同时仍能保证高质量的代码输出。 文章着重分析了上下文限制(Context Window)带来的挑战,并提出了行之有效的解决方案,例如明确化需求、范围化执行,以及对单个任务开启独立会话等。

文章还总结了在使用 AI 进行软件开发时的重要经验:良好的前期准备和精心组织的工作流是提升 AI 效用的关键。 文末的建议给出了如何有效利用 Windsurf AI 的可行步骤,从编写需求文档到管理复杂任务。即使存在一些限制,该方法仍在实际开发中展 现出了高效和准确的特性。


背景事件:四周内三倍增产

在采用 Windsurf AI 的短短四周内,大约有 3 万行 Rust 代码被写入主仓库;而在此之前,我每月平均只产出约 1 万行代码,这代表了 三倍的生产效率提升。不过,要想持续产出高质量结果,也需要不断优化使用方法与开发流程。


问题:最初的混乱与可行的上下文限制

与许多开发者相似,我在开始使用 AI 生成代码时遇到了不少问题——经常会发生编译错误、不符合需求或产生严重的破坏性变更,需要我回退 Git 提交来撤销 AI 写入的“错误代码”。

后来我发现,这些问题并非源自 AI 算法本身的缺陷,而是在于模型的 上下文窗口(Context Window) 存在可行范围远小于理论最大值的问题。本次使用的是 Claude Sonnet 3.5,其标称拥有 200K tokens 的上下文容量。然而在实际操作中,系统提示词、重复提交的历史消息以及当前输入等都会占用大量上下文,导致“真正可用”的上下文容量大打折扣。

示例:可行上下文限制的影响

以 500 行 Rust 单元测试文件为例:

  • 该文件总字符数约为 17741,估计约为 4244 个 tokens。
  • 按照 OpenAI 提供的估算方法,Rust 代码的平均 token 占用往往还要稍高一些。

如果可行上下文限制(workable context limit)仅剩 10 万 tokens,那么 1.5 万行 代码就足以触及该上限。而在实际开发中,仓库规模往往远大于此。与此同时,随着对话的进行,历史消息也会被重复提交,一并占用可行上下文。这意味着必须精心管理上下文,才不会让 AI 失效或输出错误。


解决方案:重构开发流程

鉴于上述难题,我通过以下方法重新组织编程工作流,借此大幅提高了 Windsurf AI 的实用性和稳定性。

1. 明确需求(Requirement Definition)

先写出一个简短的(通常只需一页)需求文档,包含要完成的任务、目标、测试方式、文档要求等。

  • 有时我会手动编写初稿;
  • 也会借助其他 AI(如 ChatGPT)来生成大纲或迭代草稿。
  • 每份需求文档的结尾往往附带具体的操作清单(例如:实现某模块,增加并运行测试,编写文档等)。

2. 范围化执行(Scoped Execution)

  • 开启一个新对话:针对每个新任务,我都会在 AI 界面开启新的会话,将上下文定向到对应模块的根目录。
  • 简要前言:例如,“本次目标是根据以下需求在 [module/folder name] 中实现功能。”
  • 粘贴需求文档:将整理好的需求文档粘贴给 AI,随后执行相应 Prompt。
  • 快速修复:一般第一版结果就可行,如出现小问题(比如导入路径有误)就再次调整 Prompt 并让 AI 修复。
  • 最终产出:确保任务完成后,生成 README 或日志(changelog)方便后续阅读或复用。

3. 避免上下文过载(Context Overload)

  • 不要在一个会话里做过多无关或过长流程,以防上下文被占满;
  • 如果需求不明确,AI 往往会输出错误或无效代码。保持需求具体且完整,为 AI 提供可遵循的路线图。
  • 在需要长时间对话或大型改动时,也可拆分成多个独立任务来处理。

经验教训(Lessons Learned)

在使用 AI 的第一个月,我得出以下关键结论:

  1. 可行上下文限制
    这是当前模型最主要的瓶颈。通过为大型任务开启新的对话、让 AI 生成 changelog 并保留必要的学习内容,可以相对缓解局限。大规模代码库更需要有意识地拆分任务,直到出现上下文足够大的模型才可真正彻底解决。

  2. 明确且结构化的需求
    需求越清晰,AI 输出的质量越高,且能节省大量沟通成本。模糊需求容易导致无效代码、浪费上下文。

  3. 示例的重要性
    提供一个你想要的输出示例——例如特定的宏用法、trait 的定义方式等——会显著提高 AI 的准确度。让 AI 看到实例,它就能更好推断出正确的实现与语法。


建议(Recommendations)

  1. 需求文档
    在每项工作开始前准备好详尽的需求文档,并将大型任务拆分为若干更易管理的子任务。你可以利用 ChatGPT(免费版即可)将用户故事转化为更结构化的需求文档。

    注意:在这个阶段的调整并不会消耗同一个 Windsurf AI 会话的上下文。

  2. 针对每个任务开独立对话

    • 在新的对话中只提供与该任务相关的上下文与需求文档,以保留可行上下文空间。
    • 对于已有的内部依赖,可以粘贴相关 crate 的部分内容给 AI 参考。
    • 列出完成任务需要的标准操作(比如测试、文档、README 更新等)。
  3. 标准任务清单
    在对话中列出实施、编译、测试、文档、README 更新等任务。Windsurf AI 能在一定安全权限下自动执行命令,但务必将所有可执行命令列入白名单。

这种方法对于各种类型的任务都有效,如复杂的集成测试、Rust 宏(proc_macro)的编写、高级数据结构与复杂编解码算法等。随着每次迭代,需求文档质量与代码生成质量会同步提升。


仍待解决的挑战

  1. 复杂重构
    当涉及带有生命周期的高级泛型时,模型很难一次性给出完整的、正确的重构方案。这类问题对于经验丰富的 Rust 工程师都具备挑战性,意味着未来需要更具深度的 AI 推理能力。

  2. 高度耦合的代码库
    拥有大量相互依赖模块的大型仓库非常容易触及上下文极限。仍需通过分割任务来应对,直到出现上下文规模大得多的新模型。

  3. 软件架构与设计
    AI 虽然能胜任特定任务级别的实现,但在更高层次的架构、设计决策和权衡中,仍需人类工程师把控全局思路。未来若能出现 AI 协同设计和 AI 生成代码的多 Agent 协作模式,或可进一步缩小这一差距。


总结

Windsurf AI 切实提升了我在 Rust 领域的开发效率,并让我把更多注意力从日常编码转向更高层次的设计与架构。这在一定程度上印证了我早在 2018 年的预判——借由人工智能实现自动化,开发者会更多地从事创造性、设计性、系统性思考等价值更高的工作。

随着 AI 设计AI 代码生成 的不断融合,“AI 协同设计(AI co-design)” 很可能成为下一片蓝海——AI 彼此协作,既能理解和优化项目需求,又能生成、测试、文档化、优化代码。无论如何,目前的 Windsurf AI 工作流已足以证明,AI 工具正在迅速改变软件开发者的工作方式,让我们在更少时间内完成更大规模的目标。


相关资源

作为 Rust 项目的包管理与构建工具,Cargo 提供了众多子命令(subcommand)用于完成依赖管理、构建、测试、发布、文档生成、升级等各种任务。 本篇文章将通过对常用子命令的详细解读,带你系统、深入地掌握 Cargo 的使用。 同时,文中会着重介绍与 features(特性)相关的用法及注意事项。

目录

  1. 初识 Cargo 与常见工作流
  2. 特性 features 与 Feature Unification
  3. 常用子命令详解
  1. features 相关的命令及注意事项
  2. 总结

初识 Cargo 与常见工作流

  • Cargo.toml:项目的主配置文件,里面包含项目名、版本、依赖以及特性等信息。
  • Cargo.lock:记录当前项目锁定的依赖版本信息,用于保证构建的可重现性。
  • 常见流程
    1. cargo new myprojectcargo init:初始化一个新的 Rust 项目。
    2. cargo build / cargo run / cargo check:进行构建、运行或仅做语义检查。
    3. cargo test:运行测试用例。
    4. cargo doc:生成文档。
    5. cargo publish:发布到 crates.io(或私有 Registry)。

特性 features 与 Feature Unification

features 是 Cargo 中的一种可选依赖机制,用于按需启用或禁用代码的某些部分。

  • 默认特性default feature 会在没有指定其他 feature 时被自动启用。
  • 依赖特性:有时一个 Crate 内部的某些特性在另一个 Crate 里会被依赖并合并启用(也称为特性统一,Feature Unification)。简单来说,只要有一个地方启用了某个特性,最终编译时就会被全部启用。

在使用 features 时需要注意:

  1. 有时不同依赖会对同一个 Crate 启用不同特性,最终编译时将把所有特性汇合。
  2. 可以通过命令行参数 --features--no-default-features--all-features 等来控制启用哪些特性。

常用子命令详解

1. cargo initcargo new

  • 作用:用来初始化或创建全新的 Rust 项目目录。
  • 区别
    • cargo init: 在一个已存在的空目录里初始化一个 Cargo 项目(生成 Cargo.tomlsrc 目录)。
    • cargo new: 在一个不存在或为空的目录中新建项目。

常用参数

  • --bin:创建可执行项目(包含 src/main.rs)。
  • --lib:创建类库项目(包含 src/lib.rs)。
  • --edition <year>:指定 Rust Edition,如 2015, 2018, 2021, 2024 等。

示例

# 在当前目录初始化项目
cargo init

# 新建一个名为 my_cli 的二进制项目
cargo new my_cli --bin

# 新建一个库项目,并指定为 2021 edition
cargo new my_lib --lib --edition 2021

2. cargo add

  • 作用:向当前项目的 Cargo.toml 中添加或修改依赖。该命令在 nightly 之外也通常通过 cargo-edit 插件提供。
  • 常用参数
    • cargo add crate_name:添加一个依赖(默认加到 [dependencies])。
    • --dev: 添加到 [dev-dependencies] 中。
    • --build: 添加到 [build-dependencies] 中。
    • --path <path>:以本地路径添加依赖。
    • --git <url>:从 git 仓库添加依赖。
    • --features / -F:指定要启用的 feature;可以用 crate_name/feature_name 启用子特性。
    • --no-default-features:关闭默认特性。

示例

# 添加 regex 到 dependencies
cargo add regex

# 添加 trybuild 到 dev-dependencies
cargo add --dev trybuild

# 添加 serde & serde_json 并启用 serde/derive 特性
cargo add serde serde_json -F serde/derive

# 从 git 仓库添加依赖
cargo add --git https://github.com/user/repo crate_name

3. cargo tree

  • 作用:可视化查看依赖关系树,帮助分析依赖层级、重复依赖和特性启用情况等。
  • 常用参数
    • cargo tree:查看默认依赖树。
    • cargo tree --no-dedupe:不去重,重复出现的依赖也全部显示。
    • cargo tree -d / --duplicates:只显示被多次依赖、可能存在重复版本的依赖。
    • cargo tree -i <package> / --invert <package>:查看哪些包依赖了 <package>
    • cargo tree -e features:可视化显示各依赖所启用的特性。
    • cargo tree -e features -i <crate>:倒置依赖树并查看该包及其特性被如何启用。

示例

# 基础用法
cargo tree

# 查看启用的 features
cargo tree -e features

# 查看 crate "syn" 被谁依赖、启用了哪些特性
cargo tree -e features -i syn

4. cargo update

  • 作用:更新 Cargo.lock 中的依赖到最新的可用版本。如果没有 Cargo.lock 文件,则会创建一个新的锁文件。
  • 常用参数
    • cargo update:更新所有依赖到兼容的最新版本。
    • cargo update <crate1> <crate2>:仅更新指定的依赖包。
    • --precise <version>:将指定的依赖更新到一个明确版本(或 git SHA)。
    • --dry-run:只显示将要更新的结果,不实际写入 Cargo.lock

示例

# 更新全部依赖
cargo update

# 更新特定依赖
cargo update foo bar

# 更新 foo 到指定版本
cargo update foo --precise 1.2.3

5. cargo check

  • 作用:仅做语义和类型检查,不会生成最终的可执行文件或库文件,速度比 cargo build 更快。
  • 常用参数
    • cargo check:检查当前包。
    • --all-targets:检查所有目标(包括测试、bench 等)。
    • -p <package> / --package <package>:指定要检查的包。
    • --features <features> / --all-features / --no-default-features:选择启用的特性。

示例

# 对当前包进行语义和类型检查
cargo check

# 检查所有目标(包含测试目标、bench等)
cargo check --all-targets --profile test

6. cargo searchcargo info

  • cargo search

    • 作用:在 registry(默认 crates.io)上搜索 crate。
    • --limit <n>:限制搜索结果数。
    • --registry <name>:指定自定义 registry 进行搜索。

    示例

    # 在 crates.io 搜索 "serde"
    cargo search serde

    # 限制返回结果数量
    cargo search serde --limit 5
  • cargo info

    • 作用:查看(本地或远程) crate 的详细信息,如版本、依赖、README 等。
    • 可以结合 --registry 或直接查看本地 Cargo.toml 里指定的版本信息。

    示例

    # 查看 crates.io 上的最新 serde 的信息
    cargo info serde

    # 查看特定版本
    cargo info serde@1.0.0

7. cargo installcargo uninstall

  • cargo install

    • 作用:安装用 cargo build 构建的二进制包。
    • 常用参数
      • cargo install <crate_name>:安装对应的二进制包。
      • --bin <bin_name>:只安装特定的可执行文件。
  • cargo uninstall

    • 作用:卸载用 cargo install 安装的二进制包。
    • 常用参数
      • cargo uninstall <crate_name>:卸载对应的二进制包。
    • --bin <bin_name>:只卸载特定的可执行文件。

示例

# 安装 ripgrep
cargo install ripgrep

# 卸载 ripgrep
cargo uninstall ripgrep

# 只卸载 ripgrep 的可执行文件
cargo uninstall --bin rg

8. cargo fix

  • 作用:自动根据编译器输出的建议修改源代码,通常用于修复警告,或进行 edition 升级等自动化操作。
  • 常用场景
    • 升级 edition 时(比如从 2018 升到 2021):cargo fix --edition
    • 修复所有特性或风格相关的警告:cargo fix --edition-idioms
  • 常用参数
    • --broken-code:即使源代码本身有编译错误也尝试修复。
    • --allow-dirty / --allow-staged:在工作区不是干净状态或已经有 staged 修改时仍允许执行 fix。
    • --features / --all-features:若想对特定 feature 下的代码进行 fix,需要手动指定特性启用。

示例

# 在本项目中应用修复
cargo fix

# 帮助切换到下一版 edition(不会自动修改Cargo.toml)
cargo fix --edition

# 修复edition风格,并启用所有特性
cargo fix --edition-idioms --all-features

9. cargo doc

  • 作用:生成当前项目及其依赖的文档,默认输出到 target/doc
  • 常用参数
    • --open:生成完文档后自动在浏览器中打开。
    • --no-deps:只生成当前项目的文档,不包含依赖。
    • --document-private-items:包含非公有(private)项的文档(默认对二进制目标是开启的)。

示例

# 生成当前项目及依赖的文档
cargo doc

# 生成完文档立即打开
cargo doc --open

# 只生成当前项目自身文档
cargo doc --no-deps

10. cargo run

  • 作用:构建并运行当前包或指定包的二进制。
  • 常用参数
    • --bin <bin_name>:若有多个二进制目标,通过此选项指定要运行的目标。
    • --example <example_name>:运行 example 目标。
    • --release:使用 release 配置来构建并运行,启用优化。
    • --features <features> / --no-default-features / --all-features:在运行时启用或禁用特性。

示例

# 运行当前项目的主二进制
cargo run

# 运行名为server的可执行目标,并传递参数给它
cargo run --bin server -- --port 8080 --debug

# 运行 example 中的 exname
cargo run --example exname -- --exoption exarg1 exarg2

11. cargo rustc

  • 作用:编译当前包并向最终的 rustc 编译器传递额外参数。适合需要传递额外编译器选项或对编译过程做更精细控制。
  • 使用限制:只能针对单一目标使用传递参数,若需要对多目标同时编译时请使用全局编译参数(如 RUSTFLAGS 环境变量)。
  • 常用参数
    • cargo rustc --lib -- -D warnings:编译 library 并把所有 warning 当作错误处理。
    • --bin <bin_name>:编译特定二进制目标。
    • --crate-type <types>:手动指定 crate 类型(如 lib,cdylib)。
    • 其余与 cargo build 类似的参数,如 --release--features--all-features 也通用。

示例

# 对lib进行编译,并将所有warning视为错误
cargo rustc --lib -- -D warnings

# 覆盖 Cargo.toml 中 crate-type
cargo rustc --lib --crate-type lib,cdylib

# 使用 nightly 中的实验性命令行选项
cargo rustc --lib -- -Z print-type-sizes

  1. 启用或禁用默认特性

    • --no-default-features:完全禁用默认特性。
    • --features="foo,bar":显式启用某些特性。
    • --all-features:启用所有可用特性。
  2. 常见在命令中的使用

    • cargo build --features="foo"
    • cargo check --no-default-features --features="bar"
    • cargo run --features="server_mode,logging"
    • cargo tree -e features:可视化依赖树中的特性启用情况。
    • cargo fix --features mycrate/async --edition:自动修复在特定 feature 下的代码并进行 edition 修正。
  3. Feature Unification 特性合并
    当多个依赖对同一个 crate 启用了不同的特性,Cargo 会将这些特性统一在一起,也就是说只要有一个依赖启用了某 feature,就会对编译起作用。若对依赖项的特性启用不一致,需要注意这可能引入更多的编译代码,甚至在极端情况下导致依赖冲突。

  4. 排查特性冲突
    如果想知道某个 crate 的哪些特性是被哪些包或者自身项目启用的,可通过

    cargo tree -e features -i <crate_name>

    加上 --no-dedupe 进一步查看。


总结

在 Rust 的生态系统中,Cargo 几乎可以说是“万金油”般的存在——它负责管理项目结构、依赖、文档、编译、测试、发布的方方面面。 熟练掌握各个常用子命令,尤其是明白 features 如何运作与合并,对于提升开发效率、优化项目结构而言至关重要。

  • 日常操作cargo build / cargo run / cargo check / cargo test 构成了主要工作流。
  • 特性管理是 Cargo 中的高级功能,需要对 “Feature Unification” 有所了解,以防止出现不一致或冲突。
  • 通过 cargo tree, cargo update, cargo add 以及 cargo doc 等命令,能够更好地管理依赖、生成文档、快速定位问题。
  • 遇到需要自动修复或 edition 升级时,cargo fix 也能帮我们省力不少。

希望这篇文章能够帮助你在项目中轻松地掌握并运用 Cargo 的各种命令与特性,从而让 Rust 的开发体验更上一层楼!

鱼雪