render.rs 13 KB

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