render.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. //! A shared pool of renderers for efficient server side rendering.
  2. use crate::{render::dioxus_core::NoOpMutations, server_context::with_server_context};
  3. use dioxus_ssr::{
  4. incremental::{RenderFreshness, WrapBody},
  5. Renderer,
  6. };
  7. use std::future::Future;
  8. use std::sync::Arc;
  9. use std::sync::RwLock;
  10. use tokio::task::block_in_place;
  11. use tokio::task::JoinHandle;
  12. use crate::prelude::*;
  13. use dioxus_lib::prelude::*;
  14. fn spawn_platform<Fut>(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle<Fut::Output>
  15. where
  16. Fut: Future + 'static,
  17. Fut::Output: Send + 'static,
  18. {
  19. #[cfg(not(target_arch = "wasm32"))]
  20. {
  21. tokio::task::spawn_blocking(move || {
  22. tokio::runtime::Runtime::new()
  23. .expect("couldn't spawn runtime")
  24. .block_on(f())
  25. })
  26. }
  27. #[cfg(target_arch = "wasm32")]
  28. {
  29. tokio::task::spawn_local(f())
  30. }
  31. }
  32. enum SsrRendererPool {
  33. Renderer(RwLock<Vec<Renderer>>),
  34. Incremental(RwLock<Vec<dioxus_ssr::incremental::IncrementalRenderer>>),
  35. }
  36. impl SsrRendererPool {
  37. async fn render_to(
  38. &self,
  39. cfg: &ServeConfig,
  40. route: String,
  41. virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
  42. server_context: &DioxusServerContext,
  43. ) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> {
  44. let wrapper = FullstackRenderer {
  45. cfg: cfg.clone(),
  46. server_context: server_context.clone(),
  47. };
  48. match self {
  49. Self::Renderer(pool) => {
  50. let server_context = server_context.clone();
  51. let mut renderer = pool.write().unwrap().pop().unwrap_or_else(pre_renderer);
  52. let (tx, rx) = tokio::sync::oneshot::channel();
  53. spawn_platform(move || async move {
  54. let mut vdom = virtual_dom_factory();
  55. let mut to = WriteBuffer { buffer: Vec::new() };
  56. // poll the future, which may call server_context()
  57. tracing::info!("Rebuilding vdom");
  58. with_server_context(server_context.clone(), || {
  59. block_in_place(|| vdom.rebuild(&mut NoOpMutations));
  60. });
  61. ProvideServerContext::new(vdom.wait_for_suspense(), server_context).await;
  62. tracing::info!("Suspense resolved");
  63. if let Err(err) = wrapper.render_before_body(&mut *to) {
  64. let _ = tx.send(Err(err));
  65. return;
  66. }
  67. if let Err(err) = renderer.render_to(&mut to, &vdom) {
  68. let _ = tx.send(Err(
  69. dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
  70. ));
  71. return;
  72. }
  73. if let Err(err) = wrapper.render_after_body(&mut *to) {
  74. let _ = tx.send(Err(err));
  75. return;
  76. }
  77. match String::from_utf8(to.buffer) {
  78. Ok(html) => {
  79. let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
  80. }
  81. Err(err) => {
  82. _ = tx.send(Err(
  83. dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
  84. err,
  85. )),
  86. ));
  87. }
  88. }
  89. });
  90. let (renderer, freshness, html) = rx.await.unwrap()?;
  91. pool.write().unwrap().push(renderer);
  92. Ok((freshness, html))
  93. }
  94. Self::Incremental(pool) => {
  95. let mut renderer =
  96. pool.write().unwrap().pop().unwrap_or_else(|| {
  97. incremental_pre_renderer(cfg.incremental.as_ref().unwrap())
  98. });
  99. let (tx, rx) = tokio::sync::oneshot::channel();
  100. let server_context = server_context.clone();
  101. spawn_platform(move || async move {
  102. let mut to = WriteBuffer { buffer: Vec::new() };
  103. match renderer
  104. .render(
  105. route,
  106. virtual_dom_factory,
  107. &mut *to,
  108. |vdom| {
  109. Box::pin(async move {
  110. // poll the future, which may call server_context()
  111. tracing::info!("Rebuilding vdom");
  112. with_server_context(server_context.clone(), || {
  113. block_in_place(|| vdom.rebuild(&mut NoOpMutations));
  114. });
  115. ProvideServerContext::new(
  116. vdom.wait_for_suspense(),
  117. server_context,
  118. )
  119. .await;
  120. tracing::info!("Suspense resolved");
  121. })
  122. },
  123. &wrapper,
  124. )
  125. .await
  126. {
  127. Ok(freshness) => {
  128. match String::from_utf8(to.buffer).map_err(|err| {
  129. dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
  130. err,
  131. ))
  132. }) {
  133. Ok(html) => {
  134. let _ = tx.send(Ok((freshness, html)));
  135. }
  136. Err(err) => {
  137. let _ = tx.send(Err(err));
  138. }
  139. }
  140. }
  141. Err(err) => {
  142. let _ = tx.send(Err(err));
  143. }
  144. }
  145. });
  146. let (freshness, html) = rx.await.unwrap()?;
  147. Ok((freshness, html))
  148. }
  149. }
  150. }
  151. }
  152. /// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
  153. #[derive(Clone)]
  154. pub struct SSRState {
  155. // We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
  156. renderers: Arc<SsrRendererPool>,
  157. }
  158. impl SSRState {
  159. /// Create a new [`SSRState`].
  160. pub fn new(cfg: &ServeConfig) -> Self {
  161. if cfg.incremental.is_some() {
  162. return Self {
  163. renderers: Arc::new(SsrRendererPool::Incremental(RwLock::new(vec![
  164. incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
  165. incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
  166. incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
  167. incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
  168. ]))),
  169. };
  170. }
  171. Self {
  172. renderers: Arc::new(SsrRendererPool::Renderer(RwLock::new(vec![
  173. pre_renderer(),
  174. pre_renderer(),
  175. pre_renderer(),
  176. pre_renderer(),
  177. ]))),
  178. }
  179. }
  180. /// Render the application to HTML.
  181. pub async fn render<'a>(
  182. &'a self,
  183. route: String,
  184. cfg: &'a ServeConfig,
  185. virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
  186. server_context: &'a DioxusServerContext,
  187. ) -> Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError> {
  188. let ServeConfig { .. } = cfg;
  189. let (freshness, html) = self
  190. .renderers
  191. .render_to(cfg, route, virtual_dom_factory, server_context)
  192. .await?;
  193. Ok(RenderResponse { html, freshness })
  194. }
  195. }
  196. struct FullstackRenderer {
  197. cfg: ServeConfig,
  198. server_context: DioxusServerContext,
  199. }
  200. impl dioxus_ssr::incremental::WrapBody for FullstackRenderer {
  201. fn render_before_body<R: std::io::Write>(
  202. &self,
  203. to: &mut R,
  204. ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
  205. let ServeConfig { index, .. } = &self.cfg;
  206. to.write_all(index.pre_main.as_bytes())?;
  207. Ok(())
  208. }
  209. fn render_after_body<R: std::io::Write>(
  210. &self,
  211. to: &mut R,
  212. ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
  213. // serialize the props
  214. // TODO: restore props serialization
  215. // crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to).map_err(
  216. // |err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)),
  217. // )?;
  218. // serialize the server state
  219. crate::html_storage::serialize::encode_in_element(
  220. &*self.server_context.html_data().map_err(|_| {
  221. dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new({
  222. #[derive(Debug)]
  223. struct HTMLDataReadError;
  224. impl std::fmt::Display for HTMLDataReadError {
  225. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  226. f.write_str(
  227. "Failed to read the server data to serialize it into the HTML",
  228. )
  229. }
  230. }
  231. impl std::error::Error for HTMLDataReadError {}
  232. HTMLDataReadError
  233. }))
  234. })?,
  235. to,
  236. )
  237. .map_err(|err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)))?;
  238. #[cfg(all(debug_assertions, feature = "hot-reload"))]
  239. {
  240. // In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
  241. // This is copied from the Dioxus-CLI package at ../packages/cli
  242. let disconnect_js = r#"(function () {
  243. var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
  244. var url = protocol + "//" + window.location.host + "/_dioxus/ws";
  245. var poll_interval = 8080;
  246. var reload_upon_connect = (event) => {
  247. // Firefox will send a 1001 code when the connection is closed because the page is reloaded
  248. // 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
  249. // We should not reload the page in this case
  250. if (event.code === 1001) {
  251. return;
  252. }
  253. window.setTimeout(() => {
  254. var ws = new WebSocket(url);
  255. ws.onopen = () => window.location.reload();
  256. ws.onclose = reload_upon_connect;
  257. }, poll_interval);
  258. };
  259. var ws = new WebSocket(url);
  260. ws.onmessage = (ev) => {
  261. console.log("Received message: ", ev, ev.data);
  262. if (ev.data == "reload") {
  263. window.location.reload();
  264. }
  265. };
  266. ws.onclose = reload_upon_connect;
  267. })();"#;
  268. to.write_all(r#"<script>"#.as_bytes())?;
  269. to.write_all(disconnect_js.as_bytes())?;
  270. to.write_all(r#"</script>"#.as_bytes())?;
  271. }
  272. let ServeConfig { index, .. } = &self.cfg;
  273. to.write_all(index.post_main.as_bytes())?;
  274. Ok(())
  275. }
  276. }
  277. /// A rendered response from the server.
  278. #[derive(Debug)]
  279. pub struct RenderResponse {
  280. pub(crate) html: String,
  281. pub(crate) freshness: RenderFreshness,
  282. }
  283. impl RenderResponse {
  284. /// Get the rendered HTML.
  285. pub fn html(&self) -> &str {
  286. &self.html
  287. }
  288. /// Get the freshness of the rendered HTML.
  289. pub fn freshness(&self) -> RenderFreshness {
  290. self.freshness
  291. }
  292. }
  293. fn pre_renderer() -> Renderer {
  294. let mut renderer = Renderer::default();
  295. renderer.pre_render = true;
  296. renderer
  297. }
  298. fn incremental_pre_renderer(
  299. cfg: &IncrementalRendererConfig,
  300. ) -> dioxus_ssr::incremental::IncrementalRenderer {
  301. let mut renderer = cfg.clone().build();
  302. renderer.renderer_mut().pre_render = true;
  303. renderer
  304. }
  305. struct WriteBuffer {
  306. buffer: Vec<u8>,
  307. }
  308. impl std::fmt::Write for WriteBuffer {
  309. fn write_str(&mut self, s: &str) -> std::fmt::Result {
  310. self.buffer.extend_from_slice(s.as_bytes());
  311. Ok(())
  312. }
  313. }
  314. impl std::ops::Deref for WriteBuffer {
  315. type Target = Vec<u8>;
  316. fn deref(&self) -> &Self::Target {
  317. &self.buffer
  318. }
  319. }
  320. impl std::ops::DerefMut for WriteBuffer {
  321. fn deref_mut(&mut self) -> &mut Self::Target {
  322. &mut self.buffer
  323. }
  324. }