Files
dioxus/packages/web/src/hot_reload.rs
T
88af3e7eff Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)
Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx!
Add a TUI renderer to the CLI.
Improve the CLI build system to be async and parallel.
Refactor RSX to allow partial expansion of expressions.
Merge autofmt implementations for consistency.
Merge the representation of elements and components under the hood.
Add a diagnostics system for rsx for improved error messages.
Drop interprocess and move to websockets for communication between the CLI and the server.
Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts.
Add hotreloading to any body of component/for loop/if chain/etc.

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
2024-07-17 19:11:18 -07:00

159 lines
6.1 KiB
Rust

//! Handler code for hotreloading.
//!
//! This sets up a websocket connection to the devserver and handles messages from it.
//! We also set up a little recursive timer that will attempt to reconnect if the connection is lost.
use dioxus_hot_reload::{DevserverMsg, HotReloadMsg};
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use js_sys::JsString;
use wasm_bindgen::JsCast;
use wasm_bindgen::{closure::Closure, JsValue};
use web_sys::{window, CloseEvent, MessageEvent, WebSocket};
const POLL_INTERVAL_MIN: i32 = 250;
const POLL_INTERVAL_MAX: i32 = 4000;
const POLL_INTERVAL_SCALE_FACTOR: i32 = 2;
pub(crate) fn init() -> UnboundedReceiver<HotReloadMsg> {
// Create the tx/rx pair that we'll use for the top-level future in the dioxus loop
let (tx, rx) = unbounded();
// Wire up the websocket to the devserver
make_ws(tx, POLL_INTERVAL_MIN, false);
rx
}
fn make_ws(tx: UnboundedSender<HotReloadMsg>, poll_interval: i32, reload: bool) {
// Get the location of the devserver, using the current location plus the /_dioxus path
// The idea here being that the devserver is always located on the /_dioxus behind a proxy
let location = web_sys::window().unwrap().location();
let url = format!(
"{protocol}//{host}/_dioxus",
protocol = match location.protocol().unwrap() {
prot if prot == "https:" => "wss:",
_ => "ws:",
},
host = location.host().unwrap(),
);
let ws = WebSocket::new(&url).unwrap();
// Set the onmessage handler to bounce messages off to the main dioxus loop
let tx_ = tx.clone();
ws.set_onmessage(Some(
Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
let Ok(text) = e.data().dyn_into::<JsString>() else {
return;
};
// The devserver messages have some &'static strs in them, so we need to leak the source string
let string: String = text.into();
let leaked: &'static str = Box::leak(Box::new(string));
match serde_json::from_str::<DevserverMsg>(leaked) {
Ok(DevserverMsg::HotReload(hr)) => _ = tx_.unbounded_send(hr),
// todo: we want to throw a screen here that shows the user that the devserver has disconnected
// Would be nice to do that with dioxus itself or some html/css
// But if the dev server shutsdown we don't want to be super aggressive about it... let's
// play with other devservers to see how they handle this
Ok(DevserverMsg::Shutdown) => {
web_sys::console::error_1(&"Connection to the devserver was closed".into())
}
// The devserver is telling us to reload the whole page
Ok(DevserverMsg::FullReload) => window().unwrap().location().reload().unwrap(),
Err(e) => web_sys::console::error_1(
&format!("Error parsing devserver message: {}", e).into(),
),
}
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
// Set the onclose handler to reload the page if the connection is closed
ws.set_onclose(Some(
Closure::<dyn FnMut(CloseEvent)>::new(move |e: CloseEvent| {
// Firefox will send a 1001 code when the connection is closed because the page is reloaded
// Only firefox will trigger the onclose event when the page is reloaded manually: https://stackoverflow.com/questions/10965720/should-websocket-onclose-be-triggered-by-user-navigation-or-refresh
// We should not reload the page in this case
if e.code() == 1001 {
return;
}
// set timeout to reload the page in timeout_ms
let tx = tx.clone();
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
Closure::<dyn FnMut()>::new(move || {
make_ws(
tx.clone(),
POLL_INTERVAL_MAX.min(poll_interval * POLL_INTERVAL_SCALE_FACTOR),
true,
);
})
.into_js_value()
.as_ref()
.unchecked_ref(),
poll_interval,
)
.unwrap();
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
// Set the onopen handler to reload the page if the connection is closed
ws.set_onopen(Some(
Closure::<dyn FnMut(MessageEvent)>::new(move |_evt| {
if reload {
window().unwrap().location().reload().unwrap()
}
})
.into_js_value()
.as_ref()
.unchecked_ref(),
));
// monkey patch our console.log / console.error to send the logs to the websocket
// this will let us see the logs in the devserver!
// We only do this if we're not reloading the page, since that will cause duplicate monkey patches
if !reload {
// the method we need to patch:
// https://developer.mozilla.org/en-US/docs/Web/API/Console/log
// log, info, warn, error, debug
let ws: &JsValue = ws.as_ref();
dioxus_interpreter_js::minimal_bindings::monkeyPatchConsole(ws.clone());
}
}
/// Force a hotreload of the assets on this page by walking them and changing their URLs to include
/// some extra entropy.
///
/// This should... mostly work.
pub(crate) fn invalidate_browser_asset_cache() {
// it might be triggering a reload of assets
// invalidate all the stylesheets on the page
let links = web_sys::window()
.unwrap()
.document()
.unwrap()
.query_selector_all("link[rel=stylesheet]")
.unwrap();
let noise = js_sys::Math::random();
for x in 0..links.length() {
use wasm_bindgen::JsCast;
let link: web_sys::Element = links.get(x).unwrap().unchecked_into();
let href = link.get_attribute("href").unwrap();
_ = link.set_attribute("href", &format!("{}?{}", href, noise));
}
}