Skip to content

Commit

Permalink
Improve js_callback example (#2611)
Browse files Browse the repository at this point in the history
* improve js_callback example

it now loads two modules:
1. imp.js
2. unimp.js

imp.js is loaded at page load whereas unimp.js is loaded when requested. A fallback is displayed during loading using Suspense

* Hash is not hardcoded anymore

* pre_build hook to build the post build hook binary

* fmt

* add .gitignore for trunk_post_build executable

* move js imports to bindings module, upadte README
  • Loading branch information
ranile committed Apr 18, 2022
1 parent 504693f commit 59e2f82
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 91 deletions.
4 changes: 1 addition & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ The examples are built with [trunk](https://github.com/thedodd/trunk).
You can install it with the following command:

```bash
# at some point in the future, trunk will automatically download wasm-bindgen
cargo install trunk wasm-bindgen-cli
cargo install --locked trunk
```

Running an example is as easy as running a single command:
Expand Down Expand Up @@ -50,7 +49,6 @@ As an example, check out the TodoMVC example here: <https://examples.yew.rs/todo
| [portals](portals) | S | Renders elements into out-of-tree nodes with the help of portals |
| [router](router) | S | The best yew blog built with `yew-router` |
| [simple_ssr](simple_ssr) | F | Demonstrates server-side rendering |
| [store](store) | S | Showcases the `yewtil::store` API |
| [suspense](suspense) | F | This is an example that demonstrates `<Suspense />` support |
| [function_memory_game](function_memory_game) | F | Implementation of [Memory Game](https://github.com/bradlygreen/Memory-Game) |
| [timer](timer) | S | Demonstrates the use of the interval and timeout services |
Expand Down
2 changes: 2 additions & 0 deletions examples/js_callback/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# this dir needs its own .gitignore because it has more build artificats
trunk_post_build
9 changes: 3 additions & 6 deletions examples/js_callback/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ license = "MIT OR Apache-2.0"
[dependencies]
wasm-bindgen = "0.2"
yew = { path = "../../packages/yew", features = ["csr"] }

[dependencies.web-sys]
version = "0.3"
features = [
"HtmlTextAreaElement",
]
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
once_cell = "1"
27 changes: 26 additions & 1 deletion examples/js_callback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,36 @@
The example uses wasm-bindgen to import functionality from Javascript.
To learn more about the subject, refer to ["The `wasm-binden` Guide"](https://rustwasm.github.io/wasm-bindgen/examples/import-js.html).

This example also demonstrates how to delay the loading of the snippet using Suspense.

### Serving JS files

JS files can be served when they're present in `dist` directory. There are two ways to copy these files:
1. Use [JS Snippets](https://rustwasm.github.io/wasm-bindgen/reference/js-snippets.html).
2. Use trunk to copy the file and import it manually

### Using JS Snippets

This example uses this approach. `wasm-bindgen` handles copying the files with this approach.
All you have to do is define the `extern` block. The files are copied to `dist/snippets/<bin-name>-<hash>/` directory.

If the file is to be loaded with the initial load, you can simply use the JS imports, as shown we `imp.js`.

If you would like to lazy-load the JS module, you need to use the `trunk`'s `post_build` hook
from [`trunk_post_build.rs`](trunk_post_build.rs) access the snippets' directory path at runtime and use in
[`import()` for dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports)


### Copying file with trunk

This approach is only needed if the JS module is to be lazy-loaded. It allows us to skip the step where we
provide the snippets' directory path to the app. Instead, the file is copied at a known location that can
easily be referenced im `import()` statement.

## Improvements

This example is a purely technical demonstration and lacks an actual purpose.
The best way to improve this example would be to incorporate this concept into a small application.

- Do something more complex in the Javascript code to demonstrate more of `wasm-bindgen`'s capabilities.
- Show a loading indicator while waiting after pressing "Get the payload later!".
- Improve the presentation of the example with CSS.
9 changes: 9 additions & 0 deletions examples/js_callback/Trunk.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[[hooks]]
stage = "pre_build"
command = "rustc"
command_arguments = ["trunk_post_build.rs"]

[[hooks]]
stage = "post_build"
command = "./trunk_post_build"

1 change: 0 additions & 1 deletion examples/js_callback/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<title>Yew • Js Callback</title>

<link data-trunk rel="rust" />
<link data-trunk rel="sass" href="index.scss" />
</head>

<body></body>
Expand Down
4 changes: 0 additions & 4 deletions examples/js_callback/index.scss

This file was deleted.

3 changes: 3 additions & 0 deletions examples/js_callback/js/imp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function hello() {
return "very important message"
}
3 changes: 3 additions & 0 deletions examples/js_callback/js/unimp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function bye() {
return "unimportant message"
}
34 changes: 28 additions & 6 deletions examples/js_callback/src/bindings.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
use wasm_bindgen::prelude::*;

// wasm-bindgen will automatically take care of including this script
#[wasm_bindgen(module = "/src/get-payload-script.js")]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = "getPayload")]
pub fn get_payload() -> String;
// this should be in js-sys but is not. see https://github.com/rustwasm/wasm-bindgen/issues/2865
pub fn import(s: &str) -> js_sys::Promise;

#[wasm_bindgen(js_name = "getPayloadLater")]
pub fn get_payload_later(payload_callback: JsValue);
pub type Window;

#[wasm_bindgen(method, getter, js_name = "wasmBindgenSnippetsPath")]
pub fn wasm_bindgen_snippets_path(this: &Window) -> String;
}

#[wasm_bindgen(module = "/js/imp.js")]
extern "C" {
#[wasm_bindgen]
pub fn hello() -> String;
}

#[wasm_bindgen]
extern "C" {
pub type UnimpModule;

#[wasm_bindgen(method)]
pub fn bye(this: &UnimpModule) -> String;
}

#[wasm_bindgen(module = "/js/unimp.js")]
extern "C" {
/// This exists so that wasm bindgen copies js/unimp.js to dist/snippets/<bin-name>-<hash>/js/uninp.js
#[wasm_bindgen]
fn _dummy_fn_so_wasm_bindgen_copies_over_the_file();
}
11 changes: 0 additions & 11 deletions examples/js_callback/src/get-payload-script.js

This file was deleted.

121 changes: 62 additions & 59 deletions examples/js_callback/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,77 +1,80 @@
use wasm_bindgen::prelude::*;
use web_sys::HtmlTextAreaElement;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;

use once_cell::sync::OnceCell;
use yew::prelude::*;
use yew::suspense::{use_future, SuspensionResult};

mod bindings;

pub enum Msg {
Payload(String),
AsyncPayload,
static WASM_BINDGEN_SNIPPETS_PATH: OnceCell<String> = OnceCell::new();

#[function_component]
fn Important() -> Html {
let msg = use_memo(|_| bindings::hello(), ());
html! {
<>
<h2>{"Important"}</h2>
<p>{msg}</p>
</>
}
}

pub struct App {
payload: String,
// Pointless field just to have something that's been manipulated
debugged_payload: String,
#[hook]
fn use_do_bye() -> SuspensionResult<String> {
let path = WASM_BINDGEN_SNIPPETS_PATH
.get()
.map(|path| format!("{}/js/unimp.js", path))
.unwrap();
let s = use_future(|| async move {
let promise = bindings::import(&path);
let module = JsFuture::from(promise).await.unwrap_throw();
let module = module.unchecked_into::<bindings::UnimpModule>();
module.bye()
})?;
Ok((*s).clone())
}

impl Component for App {
type Message = Msg;
type Properties = ();
#[function_component]
fn UnImportant() -> HtmlResult {
let msg = use_do_bye()?;
Ok(html! {
<>
<h2>{"Unimportant"}</h2>
<p>{msg}</p>
</>
})
}

fn create(_ctx: &Context<Self>) -> Self {
Self {
payload: String::default(),
debugged_payload: format!("{:?}", ""),
}
}
#[function_component]
fn App() -> Html {
let showing_unimportant = use_state(|| false);

fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Payload(payload) => {
if payload != self.payload {
self.debugged_payload = format!("{:?}", payload);
self.payload = payload;
true
} else {
false
let show_unimportant = {
let showing_unimportant = showing_unimportant.clone();
move |_| showing_unimportant.set(true)
};
let fallback = html! {"fallback"};
html! {
<main>
<Important />
<button onclick={show_unimportant}>{"load unimportant data"}</button>
<Suspense {fallback}>
if *showing_unimportant {
<UnImportant />
}
}
Msg::AsyncPayload => {
let callback = ctx.link().callback(Msg::Payload);
bindings::get_payload_later(Closure::once_into_js(move |payload: String| {
callback.emit(payload)
}));
false
}
}
}

fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<textarea
class="code-block"
oninput={ctx.link().callback(|e: InputEvent| {
let input: HtmlTextAreaElement = e.target_unchecked_into();
Msg::Payload(input.value())
})}
value={self.payload.clone()}
/>
<button onclick={ctx.link().callback(|_| Msg::Payload(bindings::get_payload()))}>
{ "Get the payload!" }
</button>
<button onclick={ctx.link().callback(|_| Msg::AsyncPayload)} >
{ "Get the payload later!" }
</button>
<p class="code-block">
{ &self.debugged_payload }
</p>
</>
}
</Suspense>
</main>
}
}

fn main() {
let wasm_bindgen_snippets_path = js_sys::global()
.unchecked_into::<bindings::Window>()
.wasm_bindgen_snippets_path();
WASM_BINDGEN_SNIPPETS_PATH
.set(wasm_bindgen_snippets_path)
.expect("unreachable");
yew::Renderer::<App>::new().render();
}
43 changes: 43 additions & 0 deletions examples/js_callback/trunk_post_build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::fs;
use std::io::{Read, Write};

fn read_env(env: &'static str) -> String {
std::env::var(env).unwrap_or_else(|e| panic!("can't read {} env var: {}", env, e))
}

fn main() {
let stage_dir = read_env("TRUNK_STAGING_DIR");
let mut res = fs::read_dir(format!("{stage_dir}/snippets")).expect("no snippets dir in stage");
let dir = res
.next()
.expect("there must be one snippets dir present")
.expect("can't read snippets dir");
let dir_name = dir.file_name().to_string_lossy().to_string();
let mut index_html =
fs::File::open(format!("{stage_dir}/index.html")).expect("can't open index.html");
let mut html = String::new();
index_html
.read_to_string(&mut html)
.expect("can't read index.html");

let mut split = html
.split("</head>")
.map(|it| it.to_string())
.collect::<Vec<String>>();

let public_url = read_env("TRUNK_PUBLIC_URL");
let public_url = public_url.strip_suffix("/").unwrap_or(&public_url);
let wasm_bindgen_snippets_path = format!("{public_url}/snippets/{dir_name}");

split.insert(1, format!("<script>window.wasmBindgenSnippetsPath = '{wasm_bindgen_snippets_path}';</script></head>"));
let joined = split.join("");
drop(index_html);
let mut index_html = fs::File::options()
.write(true)
.truncate(true)
.open(format!("{stage_dir}/index.html"))
.expect("can't open index.html");
index_html
.write_all(joined.as_ref())
.expect("can't write index.html")
}

1 comment on commit 59e2f82

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yew master branch benchmarks (Lower is better)

Benchmark suite Current: 59e2f82 Previous: 504693f Ratio
yew-struct-keyed 01_run1k 209.4815 171.6885 1.22
yew-struct-keyed 02_replace1k 223.65249999999995 196.066 1.14
yew-struct-keyed 03_update10th1k_x16 390.5425 404.02 0.97
yew-struct-keyed 04_select1k 70.779 74.086 0.96
yew-struct-keyed 05_swap1k 98.236 89.103 1.10
yew-struct-keyed 06_remove-one-1k 32.1105 28.2605 1.14
yew-struct-keyed 07_create10k 3254.23 3002.531 1.08
yew-struct-keyed 08_create1k-after1k_x2 463.937 428.198 1.08
yew-struct-keyed 09_clear1k_x8 181.343 189.66000000000005 0.96
yew-struct-keyed 21_ready-memory 1.457233428955078 1.457233428955078 1
yew-struct-keyed 22_run-memory 1.6618614196777344 1.6610145568847656 1.00
yew-struct-keyed 23_update5-memory 1.695598602294922 1.6644821166992188 1.02
yew-struct-keyed 24_run5-memory 1.94403076171875 1.944618225097656 1.00
yew-struct-keyed 25_run-clear-memory 1.3280715942382812 1.3275680541992188 1.00
yew-struct-keyed 31_startup-ci 1880.985 1881.015 1.00
yew-struct-keyed 32_startup-bt 36.852 31.936000000000007 1.15
yew-struct-keyed 33_startup-mainthreadcost 274.8040000000001 250.924 1.10
yew-struct-keyed 34_startup-totalbytes 328.7392578125 328.7392578125 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.