大约两周前,我们启动了我们共同构建Gloo的努力,Gloo 是一个用于使用 Rust 和 Wasm 构建快速可靠的 Web 应用程序和库的模块化工具包。我们知道我们希望通过分离可重用、独立的库来明确培养 Rust 和 Wasm 库生态系统:这些库可以帮助你,无论你是用纯 Rust 编写一个全新的 Web 应用程序,构建自己的框架,还是将一些 Rust 生成的 Wasm 手术式地插入到现有的 JavaScript 项目中。我们仍然不清楚的是,我们还不知道的是,我们如何设计和公开这些可重用的部分。

洋葱层 API

我很高兴地告诉你,经过在问题线程中进行了一些协作讨论,我们想出了一个很有希望的 Gloo API 设计方法,并且我们已经在CONTRIBUTING.md中对其进行了形式化。我将这种方法称为“洋葱层”API 设计。

简而言之,我们希望在原始-sys绑定之上构建中级抽象库,在中级 API 之上构建 futures 和 streams 集成,并在所有这些之上构建高级 API。但是,至关重要的是,每一层都应该是公开的和可重用的。

虽然这种 API 设计方法当然不是新颖的,但我们希望非常刻意地遵循它,以便我们

  • 最大限度地提高对更大生态系统的可重用性,以及
  • 在构建高级 API 时使用我们的中级 API,以确保它们的通用性和适合性,使其成为坚实的基础。

当我们检查每一层时,我将使用the setTimeoutsetInterval Web API作为运行示例。

核心:wasm-bindgenjs-sysweb-sys

最内层是建立在wasm-bindgenjs-sysweb-sys之上的原始绑定。这些绑定速度快,代码大小占用空间小,并且与主机绑定提案向前兼容。

它们不是一直都非常符合人体工程学。直接使用原始web-sys绑定有时感觉像是进行原始libc调用,而不是利用 Rust 的不错的std抽象。

以下是使用原始web-sys绑定在 500 毫秒超时后执行某些操作的方法

use wasm_bindgen::{closure::Closure, JsCast};

// Create a Rust `FnOnce` closure that is exposed to JavaScript.
let closure = Closure::once(move || {
    do_some_operation();
});

// Get the JavaScript function that reflects our Rust closure.
let js_val = closure.as_ref();
let js_func = js_val.unchecked_ref::<js_sys::Function>();

// Finally, call the `window.setTimeout` API.
let timeout_id = web_sys::window()
    .expect("should have a `window`")
    .set_timeout_with_callback_and_timeout_and_arguments_0(js_func, 500)
    .expect("should set a timeout OK");

// Then, if we ever decide we want to cancel the timeout, we do this:
web_sys::window()
    .expect("should have a `window`")
    .clear_timeout_with_handle(timeout_id);

the callbacks

当我们查看原始web-sys用法时,会有一些类型转换噪音,一些不幸的方法名称,以及一些unwrap,用于忽略我们更愿意大声失败而不是跛行进行的边缘情况。我们可以使用我们的第一个“中级”API 层来清理所有这些内容,在计时器的情况下,它是gloo_timers crate 中的callbacks 模块(它也被从gloo 伞形 crate 中重新导出为gloo::timers)。

建立在-sys绑定之上的第一个“中级”API 公开了与 Web 相同的功能和相同的设计,但使用了正确的 Rust 类型。例如,在这一层,我们不是使用js_sys::Function 来获取无类型的 JavaScript 函数,而是获取任何F: FnOnce()。这一层本质上是对 Rust 的最不固执己见的直接 API 翻译。

use gloo::timers::callbacks::Timeout;
// Alternatively, we could use the `gloo_timers` crate without the rest of Gloo:
// use gloo_timers::callbacks::Timeout;

// Already, much nicer!
let timeout = Timeout::new(500, move || {
    do_some_operation();
});

// If we ever decide we want to cancel our delayed operation, all we do is drop
// the `timeout` now:
drop(timeout);

// Or if we never want to cancel, we can use `forget`:
timeout.forget();

在 Futures 和 Streams 上分层

要添加的下一层是与 Rust 生态系统中的流行特征和库集成,例如Futureserde。对于我们正在运行的gloo::timers示例,这意味着我们实现了一个由setTimeout支持的Future,以及一个由setInterval支持的Stream实现。

use futures::prelude::*;
use gloo::timers::futures::TimeoutFuture;

// By using futures, we can use all the future combinator methods to build up a
// description of some asynchronous task.
let my_future = TimeoutFuture::new(500)
    .and_then(|_| {
        // Do some operation after 500 milliseconds...
        do_some_operation();

        // and then wait another 500 milliseconds...
        TimeoutFuture::new(500)
    })
    .map(|_| {
        // after which we do another operation!
        do_another_operation();
    })
    .map_err(|err| {
        handle_error(err);
    });

// Spawn our future to run it!
wasm_bindgen_futures::spawn_local(my_future);

请注意,我们目前使用futures 0.1,因为我们竭尽全力让 Wasm 生态系统在稳定的 Rust 上运行,但一旦新的std::future::Future设计稳定,我们计划切换过来。我们对async/await也感到非常兴奋!

更多层?

这些是我们在setTimeoutsetInterval API 中拥有的所有层。不同的 Web API 将具有不同的层集,这很好。并非每个 Web API 都使用回调,因此在每个 Gloo crate 中始终都有一个callbacks 模块没有意义。重要的是,我们正在积极识别层,使它们公开且可重用,并在较低层之上构建较高级层。

我们可能会在有意义的其他 Web API 中添加更高级的层。例如,文件 APIFileReader接口公开了在某些事件触发后你不应该调用的方法,任何早于此的调用尝试都会抛出异常。我们可以将其编码为基于状态机的Future,它甚至不会让你在相关事件触发并且状态机达到特定状态之前调用这些方法。利用编译时的类型来提高人体工程学和正确性!

另一个未来的方向是添加更多与更大 Rust crate 生态系统中的更多部分的集成层。例如,通过the futures-signals crate添加函数式响应式编程风格的层,该 crate 也被dominator框架使用。

事件

目前 Gloo 中正在进行的积极设计工作之一是如何设计我们的事件目标和监听器层。事件在大多数 Web API 中使用,因此我们必须正确设计它,因为它将位于我们许多其他 crate 的下方。虽然我们还没有 100% 确定设计,但我非常喜欢我们正在前进的方向。

web_sys::Eventweb_sys::EventTarget::add_event_listener_with_callback之上,我们正在构建一个用于添加和删除事件监听器并通过 RAII 风格的自动清理在删除时管理其生命周期的层。

我们可以使用此 API 来创建惯用的 Rust 类型,这些类型附加事件监听器,这些监听器在类型被删除时会自动从 DOM 中删除

use futures::sync::oneshot;
use gloo::events::EventListener;

// A prompt for the user.
pub struct Prompt {
    receiver: oneshot::Receiver<String>,

    // Automatically removed from the DOM on drop!
    listener: EventListener,
}

impl Prompt {
    pub fn new() -> Prompt {
        // Create an `<input>` to prompt the user for something and attach it to the DOM.
        let input: web_sys::HtmlInputElement = unimplemented!();

        // Create a oneshot channel for sending/receiving the user's input.
        let (sender, receiver) = oneshot::channel();

        // Attach an event listener to the input element.
        let listener = EventListener::new(&input, "input", move |_event: &web_sys::Event| {
            // Get the input element's value.
            let value = input.value();

            // Send the input value over the oneshot channel.
            sender.send(value)
                .expect_throw(
                    "receiver should not be dropped without first removing DOM listener"
                );
        });

        Prompt {
            receiver,
            listener,
        }
    }
}

// A `Prompt` is also a future, that resolves after the user input!
impl Future for Prompt {
    type Item = String;
    type Error = ();

    fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
        self.receiver
            .poll()
            .map_err(|_| {
                unreachable!(
                    "we don't drop the sender without either sending a value or dropping the whole Prompt"
                )
            })
    }
}

在该层之上,我们正在使用 Rust 的特征系统来设计一个更高级的、静态的事件 API,它将使事件转换安全且静态检查,并确保你在监听的事件类型中没有错别字

use gloo::events::{ClickEvent, on};

// Get an event target from somewhere.
let target: web_sys::EventTarget = unimplemented!();

// Listen to the "click" event, know that you didn't misspell the event as
// "clik", and also get a nicer event type!
let click_listener = on(&target, move |e: &ClickEvent| {
    // The `ClickEvent` type has nice getters for the `MouseEvent` that
    // `"click"` events are guaranteed to yield. No need to dynamically cast
    // an `Event` to a `MouseEvent`.
    let (x, y) = event.mouse_position();

    // ...
});

这些事件 API 仍在开发中,还有一些问题需要解决,但我对它们感到非常兴奋,我们希望在构建内部使用它们的 Gloo crate 时从中获得很多收益。

参与进来!

让我们一起构建 Gloo!想参与进来吗?