chinedufn / percy

Build frontend browser apps with Rust + WebAssembly. Supports server side rendering.
https://chinedufn.github.io/percy/
Apache License 2.0
2.26k stars 84 forks source link

Fetching Dynamic Server-Side Data #113

Closed aubaugh closed 5 years ago

aubaugh commented 5 years ago

I have two routes that render the following views accordingly, dir_route renders a DirView while file_route renders a FileView. Both dir_route and file_route take a dynamic percent-encoded path as a variable to their corresponding relative server directory or file path.

E.g:

#[route(path = "/file/:encoded_path")]
fn file_route(store: Provided<Rc<RefCell<Store>>>, encoded_path: String) -> VirtualNode {
    // Decode path to include '/' characters
    let decoded_path = percent_decode(encoded_path.as_bytes()).decode_utf8().unwrap();
    let new_path     = PathBuf::from(decoded_path.to_string());
    FileView::new(Rc::clone(&store), new_path).render()
}                                                                                        

DirView lists the contents of a given server directory (using std::fs::read_dir). While FileView displays the contents of some file that exists within the server. Both the server and client attempt to perform these operations, which is expected behavior, but of course only the server succeeds and the client panics.

I'd like to know if there is a way only the server could perform these operations, or how the client would fetch this dynamic filesystem data from the server. I'd still like to use the client to navigate routes and do other tasks.

chinedufn commented 5 years ago

Hey @austinsheep !

Thanks for your description!


A good way to do this might be to register a function to run anytime you switch to a Route.

This function would send a Msg to your Store, which would then read the files from disk or from XHR request depending on if you're on the client or the server.

That was a lot! So let's dive into the pieces...:

A good way to do this might be to register a function to run anytime you switch to a Route.

This might look something like:

#[route(path = "/file/:encoded_path")]
fn file_route(
  store: Provided<Rc<RefCell<Store>>>,
  encoded_path: String,
  find_files: BeforeRoute
) -> VirtualNode {
}

fn find_files (
  store: Provided<Rc<RefCell<Store>>>,
  encoded_path: String,
) {
  store.msg(&Msg::FindFiles(encoded.as_str())
}                                                                                      

This function would send a Msg to your Store, which would then read the files from disk or from XHR request depending on if you're on the client or the server.

So your Store should handle communication with the outside world before then forwarding the message along to your State. You can see that we do something similar with the Msg::Path here.

https://github.com/chinedufn/percy/blob/792b4e412cec19d09bdca0c601a117cfe8dd86ad/examples/isomorphic/app/src/store.rs#L19

So in your case, your Store now handles Msg::FindFiles

Something like this

pub fn msg(&mut self, msg: &Msg) {
  // ...
  Msg::FindFiles(path) => {
    #[cfg(target = "wasm32")] {
      let file_data = make_xhr_request_for_files();
      self.state.msg(Msg::StoreFiles(file_data));
    }

    #[cfg(not(target = "wasm32")] {
      let file_data = read_files_from_disk();
      self.state.msg(Msg::StoreFiles(file_data));
    }
  }
  // ...
}

This is just a quick and dirty illustration.

One problem exists here though. The BeforeRoute piece doesn't actually exist today!

However, I should be able to whip something up on that front - so if the above approach sounds suitable let me know and I'll put something together for running code before any Route.


Cheers - let me know if I explained anything poorly!

aubaugh commented 5 years ago

This looks like it would fulfill my goal perfectly!

Would the purpose of BeforeRoute be to wait for the async XHR request? Additionally, should I use web_sys::XmlHttpRequest for this?

Thanks for the detailed explanation 😄

chinedufn commented 5 years ago

Nice!!

I think that BeforeRoute would be handled by a synchronous function that could potentially call something asynchronous inside of it (like an XHR request)

A very rough and incomplete pseudocode sketch of how it might look:

enum ShouldRenderRoute {
  Yes,
  SkipAndTryNextMatchingRoute
}

fn before_route (...) -> ShouldRenderRoute {
  let callback = move || {}; // Need to Box and Closure::wrap this ...
  download_bytes("https://my.url.here", callback);

  ShouldRenderRoute::Yes // Router uses this to know whether or not to render route
}

In my HTML file I'll usually have a downloadBytes method .. something like:

        <script>
          function downloadBytes(path, callback) {
              window.fetch(path)
                  .then(response => response.arrayBuffer())
                    .then(bytes => {
                        bytes = new Uint8Array(bytes)
                        callback(bytes)
                    })
            }
        </script>

And I'll define it in Rust as

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_name = "downloadBytes")]
    pub fn download_bytes(path: &str, callback: &js_sys::Function);
}

Thanks for the detailed explanation 😄

No problem - really - anytime!!


Just a heads up I'm off at a work trip until Saturday so I won't be able to dive into this implementation until then.

However - I can answer any and all questions / thoughts here as soon as possible (I'd love to reply faster but there's a lot going on this week! So sorry!)

It'll also be a good idea to update the example project with an example of downloading data - so I'm really glad that you're bringing this up!

chinedufn commented 5 years ago

This should be solved by https://github.com/chinedufn/percy/pull/114

Let me know if I can help in any way! i.e. any specific questions you might have to make it clear how to accomplish whatever you're trying to accomplish

I'm all ears!

aubaugh commented 5 years ago

Hey @chinedufn

So happy to have #115 merged 😄

For the project I'm working on (that is built from the isomorphic example), I'm planning on hosting some JSON data on a actix-web Resource within the server. Since the data for the JSON serialization is already available for the server, I plan on having the server pre-render the data within the view.

I was wondering how to have the client wait for the callback function of download_json before updating the state of the given route. This is ultimately to remove the current Loading... message when downloading JSON that is pre-rendered. I'm thinking that if on_visit could have return value such as what was mentioned previously, Msg::SetPath could wait to finish executing until the JSON has been downloaded.

If route-rs needs changes to allow on_visit to return values, I'd be willing to add a pre-rendered download to the isomorphic example that uses the return value in this way. We could add info about the server, or add info from a static JSON file.

chinedufn commented 5 years ago

So happy to have #115 merged 😄

Me too! Thanks again for that awesome addition!

Since the data for the JSON serialization is already available for the server, I plan on having the server pre-render the data within the view.

Could you

  1. Have server call Store.msg(&Msg::SetMyData(data) so that the initial state for the app already has the json data before the client even starts.

  2. State's SetMyData handler sets already_downloaded_json to true

  3. Client hits the on_visit callback, but sees already_downloaded_json as true so it does nothing.

Would that solve what you're going for - or am I missing something?

Cheers!

aubaugh commented 5 years ago

I see, this makes a lot more sense than what I was thinking 😆

I'll get working on it and let you know if I run into any roadblocks.