[2023.09.15]: Yew SSR模式下的条件编译问题

昨天才写了Rust的条件编译,没想到这个问题还没完。
昨天我还为它的强大而赞叹不已,自以为对它了解了八九成,然而今天我才猛然意识到,这个里面的深度远超我的想象。我估计,我现在只了解其中的冰山一角吧。

故事从客户端post数据的后端api说起。

习以为常的思维影响着我解决问题的方式,对于这种问题,我通常会寻找一个库来处理后端的 API 交互问题。因此,我在互联网上四处搜寻与 Yew SSR 开发相关的示例,并发现大部分示例都使用了 reqwest 库。但是,当时我并没有注意到一个细节,这些示例都只调用了 get 方法,却没有涉及到 post 方法的应用场景。

Yew官方给出的例子如下:

#[cfg(feature = "ssr")]
async fn fetch_uuid() -> Uuid {
    // reqwest works for both non-wasm and wasm targets.
    let resp = reqwest::get("https://httpbin.org/uuid").await.unwrap();
    let uuid_resp = resp.json::<UuidResponse>().await.unwrap();

    uuid_resp.uuid
}

#[function_component]
fn Content() -> HtmlResult {
    let uuid = use_prepared_state!((), async move |_| -> Uuid { fetch_uuid().await })?.unwrap();

    Ok(html! {
        <div>{"Random UUID: "}{uuid}</div>
    })
}

因此,我就顺着这个例子给出的reqwest,来解决我要post数据到后端api的问题。

让我们细看一下官方给出的例子代码,fetch_uuid带有条件编译#[cfg(feature="ssr")],也就是说这个函数只会在编译服务器端的target文件时才会编译进去,然后看一下调用fetch_uuid的地方,用的是use_prepared_state!宏,它也会作用于服务器端。连起来理解,就是在服务器端调用fetch_uuid函数来获取数据,并将数据“发送”到客户端。这里的“发送”实际上就是wasm的加载过程,也就是说在服务器端和客户端都可以看到这个值。理解了这段例子代码之后,我们会发现这里的过程都发生在服务器端,而不是客户端。我们要把数据post到后端api,这个场景发生在wasm加载到浏览器之后,由用户在UI上的操作发起的。

显然,这段例子代码没有涉及到我的需求,但是reqwest库中也有post的功能,能不能通过reqwest库来实现我要把前端的数据post到后端api的这个需求呢?

答案是NO。

首先聊一下SSR模式,即服务器端生成,在这个模式中,针对后端api的访问就有两种情况

  1. 在服务器的运行环境中来访问后端api;
  2. 在浏览器端的wasm环境中来访问后端api;
    明白了这两种情况后,我们就很好理解在Cargo.toml中的这段配置了。
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4"
wasm-logger = "0.2"
log = "0.4"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.29.0", features = ["full"] }
warp = "0.3"
clap = { version = "4", features = ["derive"] }

这个配置,说明wasm-bindgen-futures, wasm-logger, log这3个依赖只有在编译成wasm时才会有效,而tokio, warp, clap这个3个依赖,只有在编译成服务端代码时才会有效。
而reqwest依赖于tokio库,因此reqwest只能用于服务器端的api处理,不能用于浏览器端wasm环境中的api处理。当然,你完全不用担心如果你在开发这类需求时,会不会在这个地方配置出错,完全不用担心这个问题,因为编译器会把问题报给你。这一点总算是给Rust挽回了一点面子。

故事到这里还没有完,彩蛋还在后面。

按照上面的Cargo.toml,我最初在运行cargo clippy时总是报错,说wasm_bindgen_futures::spawn_local不存在,但后来发现运行trunk build index.html时,这个错误又没了,所以我就怀疑上了条件编译这一块。说明在运行trunk build index.html时,它产生的目标是wasm。算了吧,能够完成build也不错,先把流程走通。但是后来再运行cargo run --features=ssr --bin=ssr_server -- --dir dist时,又报wasm_bindgen_futures::spawn_local不存在的错误。我想这里编译的是服务端代码,它的编译目标应该是非wasm的。而wasm-bindgen-futures又是声明在wasm下面,因此找不到wasm_bindgen_futures。所以,我尝试把wasm-bindgen-futures一到[dependencies]下,这下子都不报错了。

因此,我觉得这里应该折射出一个问题,即Yew的SSR开发模式中,没有严格区分哪些代码要被编译成服务器端的target,哪些代码要编译成客户端wasm的target。

当然,最后的结局编译成功,运行时也没有什么问题。

总结一下,在Yew的SSR开发中,虽然代码都在一个项目之内,但是要注意区分代码和功能是哪些运行在服务器环境,哪些运行在浏览器的wasm环境。可以通过指定特定目标架构的依赖项([target.'cfg()'.dependencies]),配合编译器来帮助我们发现错误。

说了这么多,最终的代码如下:
Cargo.toml

reqwest = { version = "0.11.18", features = ["json"] }
gloo-net = { version="0.4.0", features=["json", "http"]}
wasm-bindgen-futures = "0.4"

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-logger = "0.2"
log = "0.4"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.29.0", features = ["full"] }
warp = "0.3"

客户端发起post请求的代码:

    let on_save = {
        Callback::from(move |editorData: EditorData| {
            spawn_local(async move {
                if let Ok(req) = Request::post("/notes").json(&editorData) {
                    let _ = req.send().await;
                } else {
                    log!("invalid json");
                }
            })
        })
    };

今天的故事就讲到这里,里面有不对的地方,还望大家多多指教。(终于要到后端的开发了)

你可能感兴趣的:(rust,开发语言,前端)