소스 검색

Clean up and document the protocol handler

Jonathan Kelley 1 년 전
부모
커밋
2e9737ef57
3개의 변경된 파일97개의 추가작업 그리고 119개의 파일을 삭제
  1. 1 2
      examples/clock.rs
  2. 9 9
      packages/desktop/src/index.html
  3. 87 108
      packages/desktop/src/protocol.rs

+ 1 - 2
examples/clock.rs

@@ -1,8 +1,7 @@
 use dioxus::prelude::*;
 use dioxus_signals::use_signal;
 
-#[tokio::main]
-async fn main() {
+fn main() {
     dioxus_desktop::launch(app);
 }
 

+ 9 - 9
packages/desktop/src/index.html

@@ -1,12 +1,12 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <title>Dioxus app</title>
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <!-- CUSTOM HEAD -->
-  </head>
-  <body>
-    <div id="main"></div>
-    <!-- MODULE LOADER -->
-  </body>
+    <head>
+        <title>Dioxus app</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <!-- CUSTOM HEAD -->
+    </head>
+    <body>
+        <div id="main"></div>
+        <!-- MODULE LOADER -->
+    </body>
 </html>

+ 87 - 108
packages/desktop/src/protocol.rs

@@ -5,10 +5,11 @@ use std::{
 };
 use wry::{
     http::{status::StatusCode, Request, Response},
-    Result,
+    RequestAsyncResponder, Result,
 };
 
 static MINIFIED: &str = include_str!("./minified.js");
+static DEFAULT_INDEX: &str = include_str!("./index.html");
 
 pub(super) async fn desktop_handler(
     request: Request<Vec<u8>>,
@@ -18,120 +19,110 @@ pub(super) async fn desktop_handler(
     asset_handlers: &AssetHandlerRegistry,
     edit_queue: &EditQueue,
     headless: bool,
-    responder: wry::RequestAsyncResponder,
+    responder: RequestAsyncResponder,
 ) {
     let request = AssetRequest::from(request);
 
     // If the request is for the root, we'll serve the index.html file.
     if request.uri().path() == "/" {
-        // If a custom index is provided, just defer to that, expecting the user to know what they're doing.
-        // we'll look for the closing </body> tag and insert our little module loader there.
-        let body = match custom_index {
-            Some(custom_index) => custom_index
-                .replace(
-                    "</body>",
-                    &format!("{}</body>", module_loader(root_name, headless)),
-                )
-                .into_bytes(),
-
-            None => {
-                // Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
-                let mut template = include_str!("./index.html").to_string();
-
-                if let Some(custom_head) = custom_head {
-                    template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
-                }
-
-                template
-                    .replace(
-                        "<!-- MODULE LOADER -->",
-                        &module_loader(root_name, headless),
-                    )
-                    .into_bytes()
-            }
-        };
-
-        match Response::builder()
-            .header("Content-Type", "text/html")
-            .header("Access-Control-Allow-Origin", "*")
-            .body(Cow::from(body))
-        {
-            Ok(response) => {
-                responder.respond(response);
-                return;
-            }
-            Err(err) => tracing::error!("error building response: {}", err),
+        match build_index_file(custom_index, custom_head, root_name, headless) {
+            Ok(response) => return responder.respond(response),
+            Err(err) => return tracing::error!("error building response: {}", err),
         }
-    } else if request.uri().path().trim_matches('/') == "edits" {
-        edit_queue.handle_request(responder);
-        return;
     }
 
-    // If the user provided a custom asset handler, then call it and return the response
-    // if the request was handled.
+    // If the request is asking for edits (ie binary protocol streaming, do that)
+    if request.uri().path().trim_matches('/') == "edits" {
+        return edit_queue.handle_request(responder);
+    }
+
+    // If the user provided a custom asset handler, then call it and return the response if the request was handled.
     if let Some(response) = asset_handlers.try_handlers(&request).await {
-        responder.respond(response);
-        return;
+        return responder.respond(response);
     }
 
     // Else, try to serve a file from the filesystem.
+    match serve_from_fs(request) {
+        Ok(res) => responder.respond(res),
+        Err(e) => tracing::error!("Error serving request from filesystem {}", e),
+    }
+}
 
+fn serve_from_fs(request: AssetRequest) -> Result<AssetResponse> {
     // If the path is relative, we'll try to serve it from the assets directory.
     let mut asset = get_asset_root()
         .unwrap_or_else(|| Path::new(".").to_path_buf())
         .join(&request.path);
 
+    // If we can't find it, make it absolute and try again
     if !asset.exists() {
         asset = PathBuf::from("/").join(request.path);
     }
 
     if asset.exists() {
-        let content_type = match get_mime_from_path(&asset) {
-            Ok(content_type) => content_type,
-            Err(err) => {
-                tracing::error!("error getting mime type: {}", err);
-                return;
-            }
-        };
-        let asset = match std::fs::read(asset) {
-            Ok(asset) => asset,
-            Err(err) => {
-                tracing::error!("error reading asset: {}", err);
-                return;
-            }
-        };
-        match Response::builder()
+        let content_type = get_mime_from_path(&asset)?;
+        let asset = std::fs::read(asset)?;
+
+        Ok(Response::builder()
             .header("Content-Type", content_type)
-            .body(Cow::from(asset))
-        {
-            Ok(response) => {
-                responder.respond(response);
-                return;
-            }
-            Err(err) => tracing::error!("error building response: {}", err),
-        }
+            .body(Cow::from(asset))?)
+    } else {
+        Ok(Response::builder()
+            .status(StatusCode::NOT_FOUND)
+            .body(Cow::from(String::from("Not Found").into_bytes()))?)
     }
+}
 
-    match Response::builder()
-        .status(StatusCode::NOT_FOUND)
-        .body(Cow::from(String::from("Not Found").into_bytes()))
-    {
-        Ok(response) => {
-            responder.respond(response);
-            return;
-        }
-        Err(err) => tracing::error!("error building response: {}", err),
+/// Build the index.html file we use for bootstrapping a new app
+///
+/// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
+///
+/// This is similar to tauri, except we give more power to your rust code and less power to your frontend code.
+/// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually
+/// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can
+/// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you
+/// *do* have native control over elements, but that still won't work with liveview.
+fn build_index_file(
+    custom_index: Option<String>,
+    custom_head: Option<String>,
+    root_name: &str,
+    headless: bool,
+) -> std::result::Result<Response<Cow<'static, [u8]>>, wry::http::Error> {
+    // Load a custom index file if provided
+    let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
+
+    // Insert a custom head if provided
+    // We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
+    if let Some(head) = custom_head {
+        index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
     }
+
+    // Inject our module loader
+    index.insert_str(
+        index.find("</body>").expect("Body element to exist"),
+        &module_loader(root_name, headless),
+    );
+
+    Response::builder()
+        .header("Content-Type", "text/html")
+        .header("Access-Control-Allow-Origin", "*")
+        .body(Cow::from(index.into_bytes()))
 }
 
-fn module_loader(root_name: &str, headless: bool) -> String {
+/// Construct the inline script that boots up the page and bridges the webview with rust code.
+///
+/// The arguments here:
+/// - root_name: the root element (by Id) that we stream edits into
+/// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
+///             interpreter can't connect until the window is ready.
+fn module_loader(root_id: &str, headless: bool) -> String {
     format!(
         r#"
 <script type="module">
     {MINIFIED}
     // Wait for the page to load
     window.onload = function() {{
-        let rootname = "{root_name}";
+        let rootname = "{root_id}";
         let root_element = window.document.getElementById(rootname);
         if (root_element != null) {{
             window.interpreter.initialize(root_element);
@@ -144,34 +135,30 @@ fn module_loader(root_name: &str, headless: bool) -> String {
     )
 }
 
+/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
+///
+/// Currently supports:
+/// - [x] macOS
+/// - [ ] Windows
+/// - [ ] Linux (rpm)
+/// - [ ] Linux (deb)
+/// - [ ] iOS
+/// - [ ] Android
 #[allow(unreachable_code)]
 fn get_asset_root() -> Option<PathBuf> {
-    /*
-    We're matching exactly how cargo-bundle works.
-
-    - [x] macOS
-    - [ ] Windows
-    - [ ] Linux (rpm)
-    - [ ] Linux (deb)
-    - [ ] iOS
-    - [ ] Android
-
-    */
-
+    // If running under cargo, there's no bundle!
+    // There might be a smarter/more resilient way of doing this
     if std::env::var_os("CARGO").is_some() {
         return None;
     }
 
-    // TODO: support for other platforms
     #[cfg(target_os = "macos")]
     {
         let bundle = core_foundation::bundle::CFBundle::main_bundle();
         let bundle_path = bundle.path()?;
         let resources_path = bundle.resources_path()?;
         let absolute_resources_root = bundle_path.join(resources_path);
-        let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
-
-        return Some(canonical_resources_root);
+        return dunce::canonicalize(absolute_resources_root).ok();
     }
 
     None
@@ -183,18 +170,10 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
         return Ok("image/svg+xml");
     }
 
-    let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
-        Some(f) => {
-            if f == "text/plain" {
-                get_mime_by_ext(trimmed)
-            } else {
-                f
-            }
-        }
-        None => get_mime_by_ext(trimmed),
-    };
-
-    Ok(res)
+    match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
+        Some(f) if f != "text/plain" => Ok(f),
+        _ => Ok(get_mime_by_ext(trimmed)),
+    }
 }
 
 /// Get the mime type from a URI using its extension