├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── Cargo.toml ├── README.md ├── src ├── browser-animation-frame.rs ├── browser-dom-element.rs ├── browser-history.rs ├── interval.rs ├── lib.rs ├── option.rs └── timeout.rs ├── tests ├── case4dom_event.rs ├── case4history.rs ├── case4interval.rs ├── case4request_animation_frame.rs ├── case4timeout.rs ├── suite4browser.rs └── suite4nodejs.rs └── webdriver.json /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.procMacro.enable": true, 3 | "rust-analyzer.cargo.target": "wasm32-unknown-unknown", 4 | "rust-analyzer.check.targets": "wasm32-unknown-unknown", 5 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { // cargo check 7 | "label": "check", 8 | "command": "cargo", 9 | "args": [ 10 | "check" 11 | ], 12 | "options": { 13 | "cwd": "${workspaceFolder}" 14 | }, 15 | "presentation": { 16 | "echo": true, 17 | "reveal": "always", 18 | "focus": false, 19 | "panel": "shared" 20 | }, 21 | "group": { 22 | "kind": "build", 23 | "isDefault": true 24 | }, 25 | "isBackground": true, 26 | "problemMatcher": { 27 | "owner": "cargo-check", 28 | "background": { 29 | "activeOnStart": true, 30 | "beginsPattern": "Checking for the Wasm target\\.\\.\\.", 31 | "endsPattern": "Your wasm pkg is ready to publish at" 32 | }, 33 | "pattern": [{ 34 | "regexp": "^\\s*(\\S+)\\s*$", 35 | "file": 1 36 | }, { 37 | "regexp": "^\\s+(\\d+):(\\d+)\\s+(\\S+)\\s+(.*)\\s\\s+(.*)\\s*$", 38 | "line": 1, 39 | "column": 2, 40 | "severity": 3, 41 | "message": 4, 42 | "code": 5 43 | }] 44 | } 45 | }, 46 | { // cargo fix 47 | "label": "fix", 48 | "command": "cargo", 49 | "args": [ 50 | "fix", 51 | "--allow-staged", 52 | "--broken-code", 53 | "--edition-idioms" 54 | ], 55 | "options": { 56 | "cwd": "${workspaceFolder}" 57 | }, 58 | "presentation": { 59 | "echo": true, 60 | "reveal": "always", 61 | "focus": false, 62 | "panel": "shared" 63 | }, 64 | "group": { 65 | "kind": "build", 66 | "isDefault": true 67 | }, 68 | "isBackground": true, 69 | "problemMatcher": { 70 | "owner": "cargo-check", 71 | "background": { 72 | "activeOnStart": true, 73 | "beginsPattern": "Checking nonce_generator", 74 | "endsPattern": "Finished dev" 75 | }, 76 | "pattern": [{ 77 | "regexp": "^\\s*(\\S+)\\s*$", 78 | "file": 1 79 | }, { 80 | "regexp": "^\\s+(\\d+):(\\d+)\\s+(\\S+)\\s+(.*)\\s\\s+(.*)\\s*$", 81 | "line": 1, 82 | "column": 2, 83 | "severity": 3, 84 | "message": 4, 85 | "code": 5 86 | }] 87 | } 88 | }, 89 | { // cargo build 90 | "label": "build", 91 | "command": "cargo", 92 | "args": [ 93 | "build" 94 | ], 95 | "presentation": { 96 | "echo": true, 97 | "reveal": "always", 98 | "focus": false, 99 | "panel": "shared" 100 | }, 101 | "group": { 102 | "kind": "build", 103 | "isDefault": true 104 | }, 105 | "isBackground": true, 106 | "problemMatcher": {} 107 | }, 108 | { // cargo clean 109 | "label": "cargo clean", 110 | "hide": true, 111 | "command": "cargo", 112 | "args": [ 113 | "clean" 114 | ], 115 | "presentation": { 116 | "echo": true, 117 | "reveal": "always", 118 | "focus": false, 119 | "panel": "shared" 120 | }, 121 | "group": { 122 | "kind": "build", 123 | "isDefault": true 124 | }, 125 | "isBackground": true, 126 | "problemMatcher": {} 127 | }, 128 | { // wasm-pack test 129 | "label": "wasm-test", 130 | "type": "shell", 131 | "command": "wasm-pack test ${input:target} ${input:headless} ${input:test}", 132 | "presentation": { 133 | "echo": true, 134 | "reveal": "always", 135 | "focus": false, 136 | "panel": "shared" 137 | }, 138 | "group": { 139 | "kind": "build", 140 | "isDefault": true 141 | }, 142 | "isBackground": true, 143 | "problemMatcher": {} 144 | } 145 | ], 146 | "inputs": [{ 147 | "type": "pickString", 148 | "id": "headless", 149 | "description": "是否无头运行", 150 | "options": [ 151 | "--headless", 152 | "" 153 | ], 154 | "default": "--headless" 155 | }, { 156 | "type": "pickString", 157 | "id": "target", 158 | "description": "运行环境", 159 | "options": [ 160 | "--node --features=nodejs", 161 | // 从这(https://googlechromelabs.github.io/chrome-for-testing/)下载与本地 Chrome 版本匹配的 chrome_driver。 162 | "--chrome --chromedriver \"${env:LOCALAPPDATA}/Programs/chromedriver-118.0.5993.exe\"", 163 | "--firefox", 164 | "--safari" 165 | ], 166 | "default": "--chrome --chromedriver \"${env:LOCALAPPDATA}/Programs/chromedriver-118.0.5993.exe\"" 167 | }, { 168 | "type": "pickString", 169 | "id": "test", 170 | "description": "测试单元", 171 | "options": [ 172 | "--test=case4timeout", 173 | "--test=case4interval", 174 | "--test=case4request_animation_frame", 175 | "--test=case4dom_event", 176 | "--test=case4history", 177 | "--test=suite4browser", 178 | "--test=suite4nodejs" 179 | ], 180 | "default": "--test=browser_tests" 181 | }] 182 | } 183 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["stuart_zhang "] 3 | categories = ["wasm"] 4 | description = "二次封装`gloo crate`,将`Cpp - RAII`风格的`DOM`事件处理函数挂载方式封装为`Javascript - Angular`风格的`register / deregister`模式。" 5 | edition = "2021" 6 | keywords = ["wasm", "dom", "event", "event_listener"] 7 | license = "MIT" 8 | name = "wasm-gloo-dom-events" 9 | repository = "https://github.com/stuartZhang/wasm-gloo-dom-events" 10 | version = "0.2.0" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [features] 15 | nodejs = [] 16 | 17 | [dependencies] 18 | futures = "0.3.28" 19 | gloo = { version = "0.10.0", features = ["futures"] } 20 | serde = {version = "1.0.80", features = ["derive"]} 21 | serde-wasm-bindgen = "0.5" 22 | wasm-bindgen = {version = "0.2.87", features = ["serde-serialize"]} 23 | wasm-bindgen-futures = "0.4.37" 24 | web-sys = {version = "0.3.64", features = [ 25 | "CustomEvent", 26 | "CustomEventInit", 27 | "Event", 28 | ]} 29 | 30 | [dev-dependencies] 31 | deferred-future = {version = "0.1.4", features = ["local"]} 32 | gloo = { version = "0.10.0", features = ["futures", "history", "utils"] } 33 | futures = "0.3.28" 34 | wasm-bindgen = "0.2.87" 35 | wasm-bindgen-futures = "0.4.37" 36 | wasm-bindgen-test = "0.3.37" 37 | web-sys = {version = "0.3.64", features = [ 38 | "Document", 39 | "History", 40 | "HtmlBodyElement", 41 | "HtmlButtonElement", 42 | "PointerEvent" 43 | ]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-gloo-dom-events 2 | 3 | 二次封装[gloo crate](https://docs.rs/gloo/latest/gloo/index.html),将`Cpp - RAII`风格的`DOM`事件处理函数**挂载方式**封装变形为`Typescript - Angular`风格的`register / deregister`模式。 4 | 5 | ## 创作动机 6 | 7 | 就`DOM`事件处理函数的【挂/卸载】操作而言,`gloo crate`已经做了非常完善的`RAII with Guard`设计模式封装。这包括: 8 | 9 | 1. 将【调用端】提供的`Rust`事件处理闭包封装成`wasm_bindgen::closure::Closure`。再, 10 | 2. 将`wasm_bindgen::closure::Closure`类型转换为`js_sys::Function`。接着, 11 | 3. 将`js_sys::Function`注入`DOM`元素`web_sys::EventTarget::add_event_listener_with_callback(&self, ...)` — 至此,完成了`DOM`事件处理函数的挂载工作。然后, 12 | 4. 构造与返回一个“保活守卫 — `RAII Guard`实例”给【调用端】。这就 13 | 5. 将`DOM`事件处理函数的卸载工作委托给`rustc`的`Drop Checker`来完成。后续, 14 | 6. 只要(来自`#4`的)`RAII Guard`实例被释放,`RAII Guard`的析构函数`Drop::drop(self)`就会卸载在`#3`挂载的`DOM`事件处理函数。 15 | 16 | 很完美!它 17 | 18 | * 既将`DOM`事件处理函数的挂载操作委托给`RAII Guard`的构造器[EventListener::new(...)](https://docs.rs/gloo/latest/gloo/events/struct.EventListener.html#method.new);同时, 19 | * 又将同一个`DOM`事件处理函数的卸载操作委托给`RAII Guard`的析构器。这实在太`Thinking in Rust`了。 20 | 21 | 而且,能完全落实这套`RAII`编程范式的`Cpp`程序员也必定是老司机了。但, 22 | 23 | 1. `RAII Guard`是纯【系统编程】概念 24 | 2. `RAII Guard`实例是`WebAssembly`**线性内存**对象,却不在**`JS`堆**上 25 | 3. `RAII Guard`实例与`JS`事件循环没有直接的“物理”联系 26 | 27 | 所以,`RAII Guard`实例不会因为事件挂载操作而常驻内存(— 这是拥有`GC`加持的`js`程序才具备的“超能力”)。请看下面`js`代码片段: 28 | 29 | ```javascript 30 | (() => { 31 | let handle = event => console.log(event.type); 32 | button.addEventListener('click', handle); 33 | })(); 34 | // 至此,虽然函数执行结束,但`handle`闭包还驻留在内存中 — 这是事件循环的作用。 35 | // 所以,`button`的`click`事件依旧有响应 36 | ``` 37 | 38 | 相反,`RAII Guard`实例会随着【调用函数】的执行结束而被立即析构掉。进而,`Rust`端的`DOM`事件处理闭包也会被级联地释放掉。请看下面`rust`代码片段: 39 | 40 | ```rust 41 | fn main() { 42 | let handle = EventListener::new(&button, "click", move |event| { 43 | info!("按钮点击事件2", event); 44 | }); 45 | } 46 | // 在`Trunk`的入口函数`main()`执行结束之后,`button`的`click`处理函数 47 | // 就被立即卸载了。所以,从网页点击`button`组件将不会获得任何的响应。 48 | ``` 49 | 50 | 这明确不是我们想要的。我们想要是 51 | 52 | 1. `RAII Guard`实例常驻内存,和让`Rust - WASM`端的【`DOM`事件处理闭包】长期有效。**但又** 53 | 2. 禁止“人为刻意地”内存泄漏。比如,对`RAII Guard`实例**危险地**调用`std::mem::forget()` — 纯理论备选方案。**同时,也** 54 | 3. 避免使用`static mut`变量加`unsafe`块,全局缓存`RAII Guard`实例 — 这个法子是真管用,但**太外行**。请看下面代码片段: 55 | 56 | ```rust 57 | static mut HANDLE_CACHE: Option = None; 58 | fn main() { 59 | let handle = EventListener::new(&button, "click", move |event| { 60 | info!("按钮点击事件2", event); 61 | }); 62 | unsafe { // 我想吐槽:“能写出这样代码的‘货’也真没谁了!”。 63 | HANDLE_CACHE.replace(handle); 64 | } 65 | } 66 | ``` 67 | 68 | 归纳起来,我们期望由`DOM`事件挂载函数`gloo::events::EventListener::new(...)`返回的不是“保活守卫`Liveness Guard`”,而是“卸载函数`Deregistration Function`”。这样才和主流`UI`开发框架共同维系的编程习惯一致。目前,`register / deregister`事件挂载模式的经典用例就是`Angular`框架中的`$watch`监听器。比如, 69 | 70 | ```javascript 71 | let offHandle; 72 | vm.$onInit = () => { 73 | // 监听器挂载函数返回的是“卸载函数”。 74 | offHandle = $rootScope.$watch('some_property', () => {/* do something */}); 75 | }; 76 | vm.$onDestroy = () => { 77 | offHandle(); // 执行“卸载函数”注销掉监听器。 78 | }; 79 | ``` 80 | 81 | ## 工作原理 82 | 83 | 1. 将`DOM`监听器作为“消息源” 84 | 2. 借助“异步、无限(缓存)、多进单出”信道[futures::channel::mpsc::unbounded](https://docs.rs/futures/0.3.28/futures/channel/mpsc/fn.unbounded.html),将被触发的`DOM`**事件序列**转换成【异步流[futures::stream::Stream](https://docs.rs/futures/0.3.28/futures/stream/trait.Stream.html)】。 85 | 1. 异步流的迭代项就是`DOM`事件对象`web_sys::Event`自身。 86 | 3. 借助`wasm_bindgen_futures::spawn_local()`执行器,将【异步流】实例挂到`js vm`的事件循环上。进而,确保【异步流】实例在`WebAssembly`**线性内存**中的常驻,除非我们显式地卸载它。 87 | 4. 于是,【调用端】只要`futures::stream::StreamExt::for_each`(甚至,**并发**`for_each`)该【异步流】实例,就能在 88 | 1. 在`Trunk`的入口函数`main`执行结束之后, 89 | 2. 依旧持续监听与处理由`DOM`元素发起的事件了。 90 | 91 | 【异步编程】真是前端的技术关键路线,无论是`Typescript`前端,还是`WEB`汇编前端。 92 | 93 | ## 功能描述 94 | 95 | 首先,该`crate`分别对 96 | 97 | 1. `DOM`元素触发事件 98 | 2. 浏览器【历史栈】变更事件`window.addEventListener('popstate',...)` 99 | 3. 浏览器【帧渲染】事件`requestAnimationFrame()` 100 | 4. `setTimeout()` 101 | 5. `setInterval()` 102 | 103 | 的处理函数【挂/卸载】操作做了`register / deregister`封装。 104 | 105 | 其次,对非常活跃事件源的事件处理函数,基于【异步流】底层技术,提供两种执行方式: 106 | 107 | 1. 绝对地**串行**执行。无论事件处理函数是**同步**函数,还是**异步**函数,程序都会确保前一个事件处理函数被完全执行结果之后,才会开始执行后一个事件处理函数。 108 | 2. **并发**执行(注:不是**并行**执行,因为未涉及多线程,而是多协程)。一旦前一个事件处理函数进入了`.await`状态,剩余事件处理函数就立即开始执行或继续执行。 109 | 110 | 至于,如何传参配置执行方式,请见程序的【文档注释】。 111 | 112 | ## 安装 113 | 114 | ```shell 115 | cargo add wasm-gloo-dom-events 116 | ``` 117 | 118 | ## 调用套路详解 119 | 120 | 一共分成五个场景与五类套路 121 | 122 | ### 浏览器`DOM`元素响应事件 123 | 124 | ```rust 125 | use ::deferred_future::LocalDeferredFuture; 126 | use ::futures::future; 127 | use ::gloo::{timers::future::TimeoutFuture, utils}; 128 | use ::wasm_bindgen::{JsCast, UnwrapThrowExt}; 129 | use ::wasm_bindgen_test::*; 130 | use ::wasm_gloo_dom_events::{EventStream, Options}; 131 | use ::web_sys::{Document, HtmlBodyElement, HtmlButtonElement, PointerEvent}; 132 | wasm_bindgen_test_configure!(run_in_browser); 133 | #[wasm_bindgen_test] 134 | async fn dom_event() { 135 | // 136 | // 创建一个按钮`DOM`元素,和将其添加至文档`DOM`流中。 137 | // 138 | let document = utils::document(); 139 | let body = utils::body().dyn_into::().unwrap_throw(); 140 | let button = create_element::(&document, "button"); 141 | body.append_child(&button).unwrap_throw(); 142 | let deferred_future = LocalDeferredFuture::default(); 143 | let defer = deferred_future.defer(); 144 | // 145 | // 给按钮`DOM`元素挂载鼠标点击事件处理函数。 146 | // 1. 回调函数唯一形参是`DOM`事件自身的事件对象。 147 | // 148 | let off = EventStream::on(&button, "click", Options::enable_prevent_default(true), move |_event| { 149 | // 异步的事件处理函数 150 | defer.borrow_mut().complete("12".to_string()); 151 | future::ready(Ok(())) 152 | }); 153 | // 154 | // 模拟稍后点击按钮`DOM`元素。 155 | // 156 | wasm_bindgen_futures::spawn_local(async move { 157 | TimeoutFuture::new(500).await; 158 | let event = PointerEvent::new("click").unwrap_throw(); 159 | button.dispatch_event(&event).unwrap_throw(); 160 | }); 161 | let result = deferred_future.await; 162 | assert_eq!(result, "12"); 163 | // 164 | // 卸载事件处理函数 165 | // 166 | off(); 167 | } 168 | fn create_element(document: &Document, tag_name: &str) -> T { 169 | document.create_element(tag_name).unwrap_throw().dyn_into::().unwrap_throw() 170 | } 171 | ``` 172 | 173 | 从命令行,执行命令`wasm-pack test --chrome --headless --test=case4dom_event`可直接运行此例程。 174 | 175 | ### 浏览器【历史栈】变更事件 176 | 177 | ```rust 178 | use ::deferred_future::LocalDeferredFuture; 179 | use ::futures::future; 180 | use gloo::history::History; 181 | use ::gloo::{history::BrowserHistory, timers::future::TimeoutFuture}; 182 | use ::std::rc::Rc; 183 | use ::wasm_bindgen_test::*; 184 | use ::wasm_gloo_dom_events::EventStream; 185 | wasm_bindgen_test_configure!(run_in_browser); 186 | #[wasm_bindgen_test] 187 | async fn history() { 188 | // 189 | // 从主窗体拾取出`history`实例 190 | // 191 | let browser_history = Rc::new(BrowserHistory::new()); 192 | let deferred_future: LocalDeferredFuture = LocalDeferredFuture::default(); 193 | let defer = deferred_future.defer(); 194 | let off = { 195 | let browser_history = Rc::clone(&browser_history); 196 | // 197 | // 给`history`挂载历史栈更新事件处理函数。 198 | // 1. 回调函数第一个形参是`CustomEvent`。其`type`属性值呼应于`EventStream::on_history(..)`的第二个实参值。 199 | // 2. 回调函数第二个形参是`history`的最新状态数据。 200 | // 201 | EventStream::on_history(Rc::clone(&browser_history), "测试".to_string(), true, move |_event, state: Option>| { 202 | // 异步的事件处理函数 203 | defer.borrow_mut().complete(state.unwrap().to_string()); 204 | future::ready(Ok(())) 205 | }) 206 | }; 207 | { 208 | let browser_history = Rc::clone(&browser_history); 209 | // 210 | // 模拟稍后`TAB`签路由变更 — 浏览器地址栏内容发生变化。 211 | // 212 | wasm_bindgen_futures::spawn_local(async move { 213 | TimeoutFuture::new(500).await; 214 | // 修改地址栏`url`,和压栈新历史状态数据。在本例中, 215 | // 1. 修改浏览器地址栏为`/route1` 216 | // 2. 填入历史状态数据"12"字符串 217 | browser_history.push_with_state("route1", "12"); 218 | }); 219 | } 220 | let result = deferred_future.await; 221 | assert_eq!(result, "12"); 222 | // 223 | // 卸载事件处理函数 224 | // 225 | off(); 226 | } 227 | ``` 228 | 229 | 从命令行,执行命令`wasm-pack test --chrome --headless --test=case4history`可直接运行此例程。 230 | 231 | ### 浏览器【帧渲染】事件 232 | 233 | ```rust 234 | use ::deferred_future::LocalDeferredFuture; 235 | use ::futures::future; 236 | use ::wasm_bindgen_test::*; 237 | use ::wasm_gloo_dom_events::EventStream; 238 | wasm_bindgen_test_configure!(run_in_browser); 239 | #[wasm_bindgen_test] 240 | async fn request_animation_frame() { 241 | let deferred_future = LocalDeferredFuture::default(); 242 | let defer = deferred_future.defer(); 243 | // 244 | // 给浏览器【帧渲染】挂载事件。回调函数唯一形参是`CustomEvent`。 245 | // 1. 其`type`属性值呼应于`EventStream::on_request_animation_frame(..)`的第一个实参值。 246 | // 2. 其`detail.timestamp`属性值是`js - requestAnimationFrame(timestamp => {...})`中的`timestamp`回调函数实参值。 247 | // 248 | let off = EventStream::on_request_animation_frame("requestAnimationFrame".to_string(), true, move |_event| { 249 | // 异步的事件处理函数 250 | defer.borrow_mut().complete("12".to_string()); 251 | future::ready(Ok(())) 252 | }); 253 | let result = deferred_future.await; 254 | assert_eq!(result, "12"); 255 | // 256 | // 卸载事件处理函数 257 | // 258 | off(); 259 | } 260 | ``` 261 | 262 | 从命令行,执行命令`wasm-pack test --chrome --headless --test=case4request_animation_frame`可直接运行此例程。 263 | 264 | ### 单次计划任务 265 | 266 | ```rust 267 | use ::deferred_future::LocalDeferredFuture; 268 | use ::futures::future; 269 | use ::wasm_bindgen_test::*; 270 | use ::wasm_gloo_dom_events::EventStream; 271 | #[cfg(not(feature = "nodejs"))] 272 | wasm_bindgen_test_configure!(run_in_browser); 273 | #[wasm_bindgen_test] 274 | async fn timeout() { 275 | let deferred_future = LocalDeferredFuture::default(); 276 | let defer = deferred_future.defer(); 277 | // 278 | // 给`window.setTimeout()`挂载回调函数。回调函数唯一形参是`CustomEvent`。 279 | // 1. 其`type`属性值呼应于`EventStream::on_timeout(..)`的第一个实参值。 280 | // 281 | let off = EventStream::on_timeout("timeout".to_string(), 1000, move |_event| { 282 | // 异步的事件处理函数 283 | defer.borrow_mut().complete("12".to_string()); 284 | future::ready(Ok(())) 285 | }); 286 | let result = deferred_future.await; 287 | assert_eq!(result, "12"); 288 | // 289 | // 卸载事件处理函数 290 | // 291 | off(); 292 | } 293 | ``` 294 | 295 | 从命令行,执行命令可直接运行此例程 296 | 297 | * 浏览器:`wasm-pack test --chrome --headless --test=case4timeout` 298 | * `nodejs`:`wasm-pack test --node --features=nodejs --test=case4timeout` 299 | 300 | ### 周期多次计划任务 301 | 302 | ```rust 303 | use ::deferred_future::LocalDeferredFuture; 304 | use ::futures::future; 305 | use ::wasm_bindgen_test::*; 306 | use ::wasm_gloo_dom_events::EventStream; 307 | #[cfg(not(feature = "nodejs"))] 308 | wasm_bindgen_test_configure!(run_in_browser); 309 | #[wasm_bindgen_test] 310 | async fn timeout() { 311 | let deferred_future = LocalDeferredFuture::default(); 312 | let defer = deferred_future.defer(); 313 | let mut count = 0_u8; 314 | // 315 | // 给`window.setInterval()`挂载回调函数。回调函数唯一形参是`CustomEvent`。 316 | // 1. 其`type`属性值呼应于`EventStream::on_interval(..)`的第一个实参值。 317 | // 318 | let off = EventStream::on_interval("interval".to_string(), 1000, true, move |_event| { 319 | // 异步的事件处理函数 320 | count += 1; 321 | if count > 5 { 322 | defer.borrow_mut().complete("12".to_string()); 323 | } 324 | future::ready(Ok(())) 325 | }); 326 | let result = deferred_future.await; 327 | assert_eq!(result, "12"); 328 | // 329 | // 卸载事件处理函数 330 | // 331 | off(); 332 | } 333 | ``` 334 | 335 | 从命令行,执行命令可直接运行此例程 336 | 337 | * 浏览器:`wasm-pack test --chrome --headless --test=case4interval` 338 | * `nodejs`:`wasm-pack test --node --features=nodejs --test=case4interval` 339 | -------------------------------------------------------------------------------- /src/browser-animation-frame.rs: -------------------------------------------------------------------------------- 1 | use ::futures::{channel::mpsc, executor, FutureExt, SinkExt, StreamExt}; 2 | use ::gloo::render; 3 | use ::serde::{Deserialize, Serialize}; 4 | use ::std::{cell::RefCell, future::Future, rc::Rc}; 5 | use ::wasm_bindgen::prelude::*; 6 | use ::web_sys::{CustomEvent, CustomEventInit}; 7 | use super::{Event, EventStream, Listener}; 8 | 9 | impl EventStream { 10 | fn with_request_animation_frame() -> Self { 11 | let (sender, receiver) = mpsc::unbounded(); 12 | let sender = Rc::new(RefCell::new(sender)); 13 | let listener = { 14 | let sender = Rc::clone(&sender); 15 | render::request_animation_frame(move |timestamp| { 16 | sender.borrow().unbounded_send(Event::F64(timestamp)).unwrap_throw(); 17 | }) 18 | }; 19 | Self { 20 | sender, 21 | receiver, 22 | _listener: Listener::Render(listener), 23 | } 24 | } 25 | /// 向浏览器【帧渲染】挂载事件处理函数 26 | /// * `event_type: &str`会被映射给事件处理函数 event 实参的 type 属性值 27 | /// * `is_serial: bool`表示:当事件被频繁且连续地被触发时,事件处理函数是否被允许并发地执行,或者必须串行执行 28 | /// * 事件处理函数实参`event: CustomEvent`的`detail.timestamp`属性值是`js - requestAnimationFrame(timestamp => {...})`中的`timestamp`回调函数实参值。 29 | /// # Examples 30 | /// # Panics 31 | /// # Errors 32 | /// # Safety 33 | #[must_use] 34 | pub fn on_request_animation_frame(event_type: String, is_serial: bool, mut callback: CB) -> impl FnOnce() 35 | where CB: FnMut(CustomEvent) -> Fut + 'static, 36 | Fut: Future> + 'static { 37 | let stream = Self::with_request_animation_frame(); 38 | let sender = Rc::clone(&stream.sender); 39 | let callback = move |event| { 40 | if let Event::F64(timestamp) = event { 41 | #[derive(Deserialize, Serialize)] 42 | struct Detail { 43 | timestamp: f64 44 | } 45 | let detail = Detail {timestamp}; 46 | let detail = serde_wasm_bindgen::to_value(&detail).unwrap_throw(); 47 | callback(CustomEvent::new_with_event_init_dict( 48 | &event_type[..], 49 | CustomEventInit::new().bubbles(false).cancelable(true).detail(&detail) 50 | ).unwrap_throw()).map(|result| result.unwrap_throw()) 51 | } else { 52 | wasm_bindgen::throw_str("帧渲染事件仅支持 CustomEvent 事件类型") 53 | } 54 | }; 55 | wasm_bindgen_futures::spawn_local(if is_serial { 56 | stream.for_each(callback).left_future() 57 | } else { 58 | stream.for_each_concurrent(None, callback).right_future() 59 | }); 60 | move || { 61 | let mut sender = sender.borrow_mut(); 62 | executor::block_on(sender.close()).unwrap_throw() 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/browser-dom-element.rs: -------------------------------------------------------------------------------- 1 | mod option; 2 | 3 | use ::futures::{channel::mpsc, executor, FutureExt, SinkExt, StreamExt}; 4 | use ::gloo::events::{EventListener, EventListenerOptions}; 5 | use ::std::{borrow::Cow, cell::RefCell, convert::Into, future::Future, rc::Rc}; 6 | use ::wasm_bindgen::prelude::*; 7 | use ::web_sys::{Event as Event2, EventTarget}; 8 | use super::{Event, EventStream, Listener}; 9 | pub use option::Options; 10 | 11 | impl EventStream { 12 | fn new(target: &EventTarget, event_type: impl Into>, options: O) -> Self 13 | where O: Into> { 14 | let options: Option = options.into(); 15 | let (sender, receiver) = mpsc::unbounded(); 16 | let sender = Rc::new(RefCell::new(sender)); 17 | let listener = { 18 | let sender = Rc::clone(&sender); 19 | EventListener::new_with_options(&target, event_type, options.unwrap_or_default(), move |event| { 20 | sender.borrow().unbounded_send(Event::Event(event.clone())).unwrap_throw(); 21 | }) 22 | }; 23 | Self { 24 | sender, 25 | receiver, 26 | _listener: Listener::Event(listener), 27 | } 28 | } 29 | /// 向`DOM`元素挂载指定事件类型的事件处理函数 30 | /// * `options: Into>`被用来构造`gloo::events::EventListenerOptions`实例。 31 | /// # Examples 32 | /// # Panics 33 | /// # Errors 34 | /// # Safety 35 | #[must_use] 36 | pub fn on(target: &EventTarget, event_type: impl Into>, options: O, mut callback: CB) -> impl FnOnce() 37 | where O: Into>, 38 | CB: FnMut(Event2) -> Fut + 'static, 39 | Fut: Future> + 'static { 40 | let options = Into::>::into(options).unwrap_or_default(); 41 | let stream = Self::new(target, event_type, options.event_listener_options()); 42 | let sender = Rc::clone(&stream.sender); 43 | let callback = move |event| { 44 | if let Event::Event(event) = event { 45 | callback(event).map(|result| result.unwrap_throw()) 46 | } else { 47 | wasm_bindgen::throw_str("DOM 事件仅支持 Event 事件类型") 48 | } 49 | }; 50 | wasm_bindgen_futures::spawn_local(if options.is_serial() { 51 | stream.for_each(callback).left_future() 52 | } else { 53 | stream.for_each_concurrent(None, callback).right_future() 54 | }); 55 | move || { 56 | let mut sender = sender.borrow_mut(); 57 | executor::block_on(sender.close()).unwrap_throw() 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/browser-history.rs: -------------------------------------------------------------------------------- 1 | use ::futures::{channel::mpsc, executor, FutureExt, SinkExt, StreamExt}; 2 | use ::gloo::history::History; 3 | use ::std::{cell::RefCell, future::Future, rc::Rc}; 4 | use ::wasm_bindgen::prelude::*; 5 | use ::web_sys::CustomEvent; 6 | use super::{Event, EventStream, Listener}; 7 | 8 | impl EventStream { 9 | fn with_history(history: Rc) -> Self 10 | where T: History + 'static { 11 | let (sender, receiver) = mpsc::unbounded(); 12 | let sender = Rc::new(RefCell::new(sender)); 13 | let listener = { 14 | let sender = Rc::clone(&sender); 15 | Rc::clone(&history).listen(move || { 16 | sender.borrow().unbounded_send(Event::Location(history.location())).unwrap_throw(); 17 | }) 18 | }; 19 | Self { 20 | sender, 21 | receiver, 22 | _listener: Listener::History(listener), 23 | } 24 | } 25 | /// 向浏览器【历史栈】挂载活跃项变更事件处理函数 26 | /// * `event_type: &str`会被映射给事件处理函数 event 实参的 type 属性值 27 | /// * `is_serial: bool`表示:当事件被频繁且连续地被触发时,事件处理函数是否被允许并发地执行,或者必须串行执行。 28 | /// # Examples 29 | /// # Panics 30 | /// # Errors 31 | /// # Safety 32 | #[must_use] 33 | pub fn on_history(history: Rc, event_type: String, is_serial: bool, callback: CB) -> impl FnOnce() 34 | where S: 'static, 35 | T: History + 'static, 36 | CB: Fn(CustomEvent, Option>) -> Fut + 'static, 37 | Fut: Future> + 'static { 38 | let stream = Self::with_history(history); 39 | let sender = Rc::clone(&stream.sender); 40 | let callback = move |event| { 41 | if let Event::Location(location) = event { 42 | let custom_event = CustomEvent::new(&event_type[..]).unwrap_throw(); 43 | let state = location.state::(); 44 | callback(custom_event, state).map(|result| result.unwrap_throw()) 45 | } else { 46 | wasm_bindgen::throw_str("历史栈变更事件仅支持 CustomEvent 事件类型") 47 | } 48 | }; 49 | wasm_bindgen_futures::spawn_local(if is_serial { 50 | stream.for_each(callback).left_future() 51 | } else { 52 | stream.for_each_concurrent(None, callback).right_future() 53 | }); 54 | move || { 55 | let mut sender = sender.borrow_mut(); 56 | executor::block_on(sender.close()).unwrap_throw() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/interval.rs: -------------------------------------------------------------------------------- 1 | use ::futures::{channel::mpsc, executor, FutureExt, SinkExt, StreamExt}; 2 | use ::gloo::timers::callback::Interval; 3 | use ::std::{cell::RefCell, future::Future, rc::Rc}; 4 | use ::wasm_bindgen::prelude::*; 5 | use ::web_sys::CustomEvent; 6 | use super::{Event, EventStream, Listener, Vm}; 7 | 8 | impl EventStream { 9 | fn with_interval(duration: u32) -> Self { 10 | let (sender, receiver) = mpsc::unbounded(); 11 | let sender = Rc::new(RefCell::new(sender)); 12 | let listener = { 13 | let sender = Rc::clone(&sender); 14 | Interval::new(duration, move || { 15 | sender.borrow().unbounded_send(Event::None).unwrap_throw(); 16 | }) 17 | }; 18 | Self { 19 | sender, 20 | receiver, 21 | _listener: Listener::Interval(listener), 22 | } 23 | } 24 | /// 向浏览器【循环计划任务】挂载事件处理函数 25 | /// * `event_type: &str`会被映射给事件处理函数 event 实参的 type 属性值 26 | /// * `duration: u32` 事件的触发间隔周期 27 | /// * `is_serial: bool`表示:当事件被频繁且连续地被触发时,事件处理函数是否被允许并发地执行,或者必须串行执行 28 | /// # Examples 29 | /// # Panics 30 | /// # Errors 31 | /// # Safety 32 | #[must_use] 33 | pub fn on_interval(event_type: String, duration: u32, is_serial: bool, mut callback: CB) -> impl FnOnce() 34 | where CB: FnMut(Vm) -> Fut + 'static, 35 | Fut: Future> + 'static { 36 | let stream = Self::with_interval(duration); 37 | let sender = Rc::clone(&stream.sender); 38 | let callback = move |_| callback(CustomEvent::new(&event_type).map_or_else(|_| { 39 | Vm::Nodejs(event_type.clone()) 40 | }, |event| { 41 | Vm::Browser(event) 42 | })).map(|result| result.unwrap_throw()); 43 | wasm_bindgen_futures::spawn_local(if is_serial { 44 | stream.for_each(callback).left_future() 45 | } else { 46 | stream.for_each_concurrent(None, callback).right_future() 47 | }); 48 | move || { 49 | let mut sender = sender.borrow_mut(); 50 | executor::block_on(sender.close()).unwrap_throw() 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use ::futures::{channel::mpsc, stream::Stream}; 2 | use ::gloo::{events::EventListener, history::{HistoryListener, Location}, render::AnimationFrame, timers::callback::{Interval, Timeout}}; 3 | use ::std::{cell::RefCell, pin::Pin, rc::Rc, task::{Context, Poll}}; 4 | use ::web_sys::{Event as BrowserEvent, CustomEvent}; 5 | 6 | enum Listener { 7 | Event(EventListener), 8 | History(HistoryListener), 9 | Render(AnimationFrame), 10 | Interval(Interval), 11 | Timeout(Timeout) 12 | } 13 | pub enum Event { 14 | Event(BrowserEvent), 15 | Location(Location), 16 | String(String), 17 | F64(f64), 18 | None 19 | } 20 | pub enum Vm { 21 | Browser(CustomEvent), 22 | Nodejs(String) 23 | } 24 | pub struct EventStream { 25 | sender: Rc>>, 26 | receiver: mpsc::UnboundedReceiver, 27 | _listener: Listener, 28 | } 29 | impl Stream for EventStream { 30 | type Item = Event; 31 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { 32 | Pin::new(&mut self.receiver).poll_next(cx) 33 | } 34 | } 35 | #[path ="./browser-dom-element.rs"] 36 | mod browser_dom_element; 37 | pub use browser_dom_element::Options; 38 | #[path ="./browser-history.rs"] 39 | mod browser_history; 40 | #[path ="./browser-animation-frame.rs"] 41 | mod browser_animation_frame; 42 | mod interval; 43 | mod timeout; 44 | -------------------------------------------------------------------------------- /src/option.rs: -------------------------------------------------------------------------------- 1 | use ::gloo::events::EventListenerOptions; 2 | #[derive(Default)] 3 | pub struct Options { 4 | event_listener_options: EventListenerOptions, 5 | is_serial: bool 6 | } 7 | impl Options { 8 | /// 形参`is_serial: bool`表示:当事件被频繁且连续地被触发时,事件处理函数是否被允许并发地执行。 9 | /// * is_serial = true 串行执行 10 | /// * is_serial = false 并发执行 11 | /// # Examples 12 | /// # Panics 13 | /// # Errors 14 | /// # Safety 15 | pub fn enable_prevent_default(is_serial: bool) -> Self { 16 | Options { 17 | event_listener_options: EventListenerOptions::enable_prevent_default(), 18 | is_serial 19 | } 20 | } 21 | pub fn is_serial(&self) -> bool { 22 | self.is_serial 23 | } 24 | pub fn event_listener_options(&self) -> EventListenerOptions { 25 | self.event_listener_options 26 | } 27 | } -------------------------------------------------------------------------------- /src/timeout.rs: -------------------------------------------------------------------------------- 1 | use ::futures::{channel::mpsc, executor, FutureExt, SinkExt, StreamExt}; 2 | use ::gloo::timers::callback::Timeout; 3 | use ::std::{cell::RefCell, future::Future, rc::Rc}; 4 | use ::wasm_bindgen::prelude::*; 5 | use ::web_sys::CustomEvent; 6 | use super::{Event, EventStream, Listener, Vm}; 7 | 8 | impl EventStream { 9 | fn with_timeout(duration: u32) -> Self { 10 | let (sender, receiver) = mpsc::unbounded(); 11 | let sender = Rc::new(RefCell::new(sender)); 12 | let listener = { 13 | let sender = Rc::clone(&sender); 14 | Timeout::new(duration, move || { 15 | sender.borrow().unbounded_send(Event::None).unwrap_throw(); 16 | }) 17 | }; 18 | Self { 19 | sender, 20 | receiver, 21 | _listener: Listener::Timeout(listener), 22 | } 23 | } 24 | /// 向浏览器【单次计划任务】挂载事件处理函数 25 | /// * `event_type: &str`会被映射给事件处理函数 event 实参的 type 属性值 26 | /// * `duration: u32` 事件的触发的延迟时间 27 | /// # Examples 28 | /// # Panics 29 | /// # Errors 30 | /// # Safety 31 | #[must_use] 32 | pub fn on_timeout(event_type: String, duration: u32, mut callback: CB) -> impl FnOnce() 33 | where CB: FnMut(Vm) -> Fut + 'static, 34 | Fut: Future> + 'static { 35 | let stream = Self::with_timeout(duration); 36 | let sender = Rc::clone(&stream.sender); 37 | let callback = move |_| callback(CustomEvent::new(&event_type).map_or_else(|_| { 38 | Vm::Nodejs(event_type.clone()) 39 | }, |event| { 40 | Vm::Browser(event) 41 | })).map(|result| result.unwrap_throw()); 42 | wasm_bindgen_futures::spawn_local(stream.for_each(callback)); 43 | move || { 44 | let mut sender = sender.borrow_mut(); 45 | executor::block_on(sender.close()).unwrap_throw() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/case4dom_event.rs: -------------------------------------------------------------------------------- 1 | use ::deferred_future::LocalDeferredFuture; 2 | use ::futures::future; 3 | use ::gloo::{timers::future::TimeoutFuture, utils}; 4 | use ::wasm_bindgen::{JsCast, UnwrapThrowExt}; 5 | use ::wasm_bindgen_test::*; 6 | use ::wasm_gloo_dom_events::{EventStream, Options}; 7 | use ::web_sys::{Document, HtmlBodyElement, HtmlButtonElement, PointerEvent}; 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | #[wasm_bindgen_test] 10 | async fn dom_event() { 11 | let document = utils::document(); 12 | let body = utils::body().dyn_into::().unwrap_throw(); 13 | let button = create_element::(&document, "button"); 14 | body.append_child(&button).unwrap_throw(); 15 | let deferred_future = LocalDeferredFuture::default(); 16 | let defer = deferred_future.defer(); 17 | let off = EventStream::on(&button, "click", Options::enable_prevent_default(true), move |_event| { 18 | defer.borrow_mut().complete("12".to_string()); 19 | future::ready(Ok(())) 20 | }); 21 | wasm_bindgen_futures::spawn_local(async move { 22 | TimeoutFuture::new(500).await; 23 | let event = PointerEvent::new("click").unwrap_throw(); 24 | button.dispatch_event(&event).unwrap_throw(); 25 | }); 26 | let result = deferred_future.await; 27 | assert_eq!(result, "12"); 28 | off(); 29 | } 30 | fn create_element(document: &Document, tag_name: &str) -> T { 31 | document.create_element(tag_name).unwrap_throw().dyn_into::().unwrap_throw() 32 | } -------------------------------------------------------------------------------- /tests/case4history.rs: -------------------------------------------------------------------------------- 1 | use ::deferred_future::LocalDeferredFuture; 2 | use ::futures::future; 3 | use gloo::history::History; 4 | use ::gloo::{history::BrowserHistory, timers::future::TimeoutFuture}; 5 | use ::std::rc::Rc; 6 | use ::wasm_bindgen_test::*; 7 | use ::wasm_gloo_dom_events::EventStream; 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | #[wasm_bindgen_test] 10 | async fn history() { 11 | let browser_history = Rc::new(BrowserHistory::new()); 12 | let deferred_future: LocalDeferredFuture = LocalDeferredFuture::default(); 13 | let defer = deferred_future.defer(); 14 | let off = { 15 | let browser_history = Rc::clone(&browser_history); 16 | EventStream::on_history(Rc::clone(&browser_history), "测试".to_string(), true, move |_event, state: Option>| { 17 | defer.borrow_mut().complete(state.unwrap().to_string()); 18 | future::ready(Ok(())) 19 | }) 20 | }; 21 | { 22 | let browser_history = Rc::clone(&browser_history); 23 | wasm_bindgen_futures::spawn_local(async move { 24 | TimeoutFuture::new(500).await; 25 | browser_history.push_with_state("route1", "12"); 26 | }); 27 | } 28 | let result = deferred_future.await; 29 | assert_eq!(result, "12"); 30 | off(); 31 | } -------------------------------------------------------------------------------- /tests/case4interval.rs: -------------------------------------------------------------------------------- 1 | use ::deferred_future::LocalDeferredFuture; 2 | use ::futures::future; 3 | use ::wasm_bindgen_test::*; 4 | use ::wasm_gloo_dom_events::EventStream; 5 | #[cfg(not(feature = "nodejs"))] 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | #[wasm_bindgen_test] 8 | async fn timeout() { 9 | let deferred_future = LocalDeferredFuture::default(); 10 | let defer = deferred_future.defer(); 11 | let mut count = 0_u8; 12 | let off = EventStream::on_interval("interval".to_string(), 1000, true, move |_event| { 13 | count += 1; 14 | if count > 5 { 15 | defer.borrow_mut().complete("12".to_string()); 16 | } 17 | future::ready(Ok(())) 18 | }); 19 | let result = deferred_future.await; 20 | assert_eq!(result, "12"); 21 | off(); 22 | } -------------------------------------------------------------------------------- /tests/case4request_animation_frame.rs: -------------------------------------------------------------------------------- 1 | use ::deferred_future::LocalDeferredFuture; 2 | use ::futures::future; 3 | use ::wasm_bindgen_test::*; 4 | use ::wasm_gloo_dom_events::EventStream; 5 | wasm_bindgen_test_configure!(run_in_browser); 6 | #[wasm_bindgen_test] 7 | async fn request_animation_frame() { 8 | let deferred_future = LocalDeferredFuture::default(); 9 | let defer = deferred_future.defer(); 10 | let off = EventStream::on_request_animation_frame("requestAnimationFrame".to_string(), true, move |_event| { 11 | defer.borrow_mut().complete("12".to_string()); 12 | future::ready(Ok(())) 13 | }); 14 | let result = deferred_future.await; 15 | assert_eq!(result, "12"); 16 | off(); 17 | } 18 | -------------------------------------------------------------------------------- /tests/case4timeout.rs: -------------------------------------------------------------------------------- 1 | use ::deferred_future::LocalDeferredFuture; 2 | use ::futures::future; 3 | use ::wasm_bindgen_test::*; 4 | use ::wasm_gloo_dom_events::EventStream; 5 | #[cfg(not(feature = "nodejs"))] 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | #[wasm_bindgen_test] 8 | async fn timeout() { 9 | let deferred_future = LocalDeferredFuture::default(); 10 | let defer = deferred_future.defer(); 11 | let off = EventStream::on_timeout("timeout".to_string(), 1000, move |_event| { 12 | defer.borrow_mut().complete("12".to_string()); 13 | future::ready(Ok(())) 14 | }); 15 | let result = deferred_future.await; 16 | assert_eq!(result, "12"); 17 | off(); 18 | } -------------------------------------------------------------------------------- /tests/suite4browser.rs: -------------------------------------------------------------------------------- 1 | mod case4dom_event; 2 | mod case4history; 3 | mod case4request_animation_frame; 4 | mod case4timeout; 5 | mod case4interval; -------------------------------------------------------------------------------- /tests/suite4nodejs.rs: -------------------------------------------------------------------------------- 1 | mod case4timeout; 2 | mod case4interval; -------------------------------------------------------------------------------- /webdriver.json: -------------------------------------------------------------------------------- 1 | { 2 | "moz:firefoxOptions": { 3 | "prefs": { 4 | "media.navigator.streams.fake": true, 5 | "media.navigator.permission.disabled": true 6 | }, 7 | "args": [] 8 | }, 9 | "goog:chromeOptions": { 10 | "args": [ 11 | "--use-fake-device-for-media-stream", 12 | "--use-fake-ui-for-media-stream", 13 | "--disable-gpu" 14 | ] 15 | } 16 | } --------------------------------------------------------------------------------