davedawkins / Sutil

Lightweight front-end framework for F# / Fable. No dependencies.
https://sutil.dev
MIT License
294 stars 18 forks source link

Plans for SSR? #18

Open kaeedo opened 3 years ago

kaeedo commented 3 years ago

Hello, I know this project is in its infancy, but it already seems really cool. Something I'm personally interested in, is Server Side Rendering of client side components which can then be hydrated. Are there any plans with this project to support that?

Since a Feliz View engine is coming, it might be possible to use their mechanisms.

Thanks.

davedawkins commented 3 years ago

Thank you for your kind words. I think its cool factor comes from a combination of Svelte, F#, Fable and Elmish, and I've just glued it all together. I'm really enjoying it, it's given me a renewed passion for coding.

Well the Feliz View engine is the typesafe API for HTML and CSS, and Sutil puts its own back-end on that. However, that's an interesting idea. It could make sense if we use SignalR to handle the reactivity events, though I have to admit I've not thought about SSR for Sutil very much. The Elmish and store approach does already provide an obvious point of abstraction between client and server.

Out of interest, what would be your personal reason for choosing an SSR architecture?

kaeedo commented 3 years ago

From the sounds of your comment, maybe a small misunderstanding. I'm not talking about a Blazor Server Side Model, or a Elixir Phoenix Live view style model, but only the very first request to the site. So the initial request to the server would load the base html and header stuff, but also render the home page component as defined in Sutil as static html. And then once javscript loads on the client, sutil would hook up the rendered html and attach any event listeners to continue its normal reactivity.

Here a simple React example: https://ms314006.github.io/simple-implementing-ssr-in-react/ Or a Svelte version: https://github.com/PierBover/svelte-ssr-example And here the Feliz api to render to string: https://zaid-ajaj.github.io/Feliz/#/Feliz/React/RenderStaticHtml

Sorry if you already knew this, but I was confused about your mention of SignalR.

My main usecase is simply to make the initial page load much faster, and adds the potential to at least load the index page without JS. It also has SEO benefits.

davedawkins commented 3 years ago

Ah! I see! Thank you for the detailed explanation. I have to look into that a bit more, but I think that's entirely possible. Let's keep the issue open for discussion

On Thu, 25 Feb 2021 at 12:38, Kai notifications@github.com wrote:

From the sounds of your comment, maybe a small misunderstanding. I'm not talking about a Blazor Server Side Model, or a Elixir Phoenix Live view style model, but only the very first request to the site. So the initial request to the server would load the base html and header stuff, but also render the home page component as defined in Sutil as static html. And then once javscript loads on the client, sutil would hook up the rendered html and attach any event listeners to continue its normal reactivity.

Here a simple React example: https://ms314006.github.io/simple-implementing-ssr-in-react/ Or a Svelte version: https://github.com/PierBover/svelte-ssr-example And here the Feliz api to render to string: https://zaid-ajaj.github.io/Feliz/#/Feliz/React/RenderStaticHtml

Sorry if you already knew this, but I was confused about your mention of SignalR.

My main usecase is simply to make the initial page load much faster, and adds the potential to at least load the index page without JS. It also has SEO benefits.

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/davedawkins/Sutil/issues/18#issuecomment-785865501, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACFV3KUZ2NYLRKQEGWL4GTTAZAFJANCNFSM4YGC2J3A .

lukemcdo commented 2 years ago

Hi, I was interested if this was still a topic.

I'd love to see (if possible) drop-in Giraffe.ViewEngine replacement from Sutil. My usage model would be different than above though -- my goal would be to use Sutil as-is for a typical experience, and use a theoretical Sutil.ViewEngine in a Giraffe project for a fully client-side static experience.

To tie into the SSR comment a bit more, I'd be interested in a prerenderer .NET Tool to build static HTML from Sutil templates. This would align with how Vue recommends its prerendering be done. Since most Fable CI builds already need dotnet tool, this wouldn't be hard for people to integrate into their existing workflows.

davedawkins commented 2 years ago

Yes it is, it's just waiting on me to understand how it actually works. Reading more about it now

davedawkins commented 2 years ago

This might not be hard to do, but I think I do need an example to look at / work on. Some discussion would be very welcome.

lukemcdo commented 2 years ago

Re: Drop-in Engine:

I might be being stupid here -- you wrote that Sutil is a backend for Feliz.ViewEngine up above, but informally. My possibly-naive understanding was that Sutil is a backend for Feliz.Engine. Is there a way to bridge this gap? Or is there no gap and I'm just mistaken?

Feliz.ViewEngine is a drop-in Giraffe view engine. It's type-compatible with Feliz, but don't know if that's true for compatibility with Feliz.Engine.

davedawkins commented 2 years ago

Yes, they're two different things. Feliz.Engine is an abstracted typesafe DSL for HTML and CSS.

We might be able to implement a Feliz.ViewEngine (or Sutil.ViewEngine if you like), but I don't really understand what it would do exactly. If its purpose is to create a static HTML page, then I get it, we can in fact put a static SSR implementation on the back-end of Feliz.Engine. I'm guessing though that's not the objective, since we have dynamic behavour, interactivity and reactivity to deal with too (most likely and usefully). Without those parts, there's no React/Feliz/Sutil etc, it's just HTML. Am I understanding that part correctly?

So, then I wonder what happens to the application logic that's normally embedded in the onClick handlers (and for Sutil, in the binding statements). Do we separate our app into SSR HTML-only, and then write client-side Fable that... and this is where I get lost.

Educate me :-)

lukemcdo commented 2 years ago

There's a few things here.

Up front: while the reactive tools in Sutil are cool, quite frankly I'm here because Sutil has Fable-compatible templates that look like they can be rendered server-side without too much work (I'm no savant but even if the project doesn't take it on, I am pretty sure I could get there eventually), and Sutil doesn't have a React dependency.

The application logic embedded in the onClick handlers would probably at first need to be ignored. An ultra-fancy version of this could go all-out and run Fable builds on the server to compile these handlers into JS on-request with the relevant context, but that's certainly a bigger project that would almost tie into Fable.Remoting I think?

Both myself and the original post author aren't picturing the latter onClick example, but kaeedo seems to be picturing dehydrated/rehydrated web apps, the idea being that browser-side Sutil can identify prepopulated content and replace it with up-to-date data once it has received that data.

From my view, the reason that a static implementation is useful is separation of duties. It's extremely common for end-user pages to be static or close-to-static with server side rendering. Let's say, though, that there's an online editor for these pages. Imagine using Sutil for a form page (or even WYSIWYG editor) for site maintainers with preview for some sort of post (eCommerce, blog, whatever). The webserver doesn't get bogged down with the editing process or rendering a bunch of preview pages. Then, the content (context) is uploaded, and Sutil.ViewEngine prepares it either on-demand (SSR) or once (SSG).

davedawkins commented 2 years ago

because Sutil has Fable-compatible templates

I'm not sure what this means. Sutil DSL (without any of the reactivity which may have dependencies on JS) should compile on the server-side, if that's what you mean?

Also : Sutil DSL = Feliz.Engine + Sutil DOM Builder . If we just need this:

    Html.div [
         text "hello world"
    ]

to produce

<div>hello world</div>

then we don't need Sutil at all. We can use it, but even myself I'd use Feliz.Engine + HTML Builder (in fact, Fable.Lit is very likely to be doing this already).

(that said, there are some other neat things that Sutil can do, with respect to stylesheets, that would still work in a static context.)

What we could do is produce a server-side implementation of Feliz.Engine that doesn't produce DOM objects, but string output. (Again, see Fable.Lit). This would be non-Sutil.

To a certain extent, I can't help thinking that Feliz.ViewEngine would already do this - presumably though it includes <script src=react.js></script> in the header etc.

[ ignore application logic for now ]

OK, this makes sense. You've also touched on the idea of dynamically hooking the application logic into the initial static page, and that makes sense (later).

[ online editor ]

Thanks for this example. I don't think you mean that we're editing DSL in the browser here and sending F# back to the server. I think you mean that the form is pre-defined on the server, and the template HTML is rendered by the server, so that the editor can let the user edit the custom content for that template. While editing, the editor binds the content into the static HTML for preview, and when done, sends that content back to the server so that it can be bound and then served statically. Excuse me restating what you wrote, it was necessary to make sure I understood.

What's interesting about this example is the complete separation of dependencies between the SS renderer and the editor. The editor could indeed be Sutil, but it won't care how the HTML that it binds into is generated (unless we have some rehydration / binding protocol). The binding could just be based on element IDs though.

My understanding so far - we want a server side DSL that produces HTML.

lukemcdo commented 2 years ago

Fable.Lit has a React dependency, it looks like? Which is ironic considering that it looks like Google's Lit uses WebComponents. Either way, not ideal.

The point isn't whether or not Feliz.Engine + HTML Engine can match the features, or that the Feliz.Engine syntax "looks the same," it's that both server and client side run the same code on the same inputs and get the same outputs. Then you can go and put all of your templates in a .NET Standard library and depend on those shared views (plus of course Sutil) across both your SSR/SSG application as well as your Fable client-side code.

The fact that I "can" manually convert the CSS and strip out the OnClick functions isn't the point. That step is where I'm likely to make mistakes in consistency.

lukemcdo commented 2 years ago

Yea with the online editor bit you're spot on. Sorry I didn't address that in the first post but yes absolutely. And you're right, in theory there's an absolute separation between the SS renderer and editor. Keeping two separate renderers in sync with regards to template compatibility has proven monstrous when it has been attempted, which is why NodeJS became quite so popular.

The problem is that then you're either stuck using NodeJS as your application server (which is ugly, for both tooling and performance reasons in my opinion), or you're calling NodeJS from something else. Neither is pretty.

Feliz + Feliz.ViewEngine technically solves my problem, and can solve the OP's problem, but I'd really rather not use Femto nor manage a React dependency that doesn't feel necessary and adds weight.

davedawkins commented 2 years ago

Fable.Lit has a react dependency

but this is not an issue on the server side, and the point I'm making here is that we have a DSL->HTML generator (because it implemented one for Feliz.Engine). In theory, we can take that code and make the SSR. This is following the idea of server-client separation, but in any case, you're not looking to go down that path. I envisaged making use of Fable.Lit's generator to make the SSR, and allowing the use of an independent client side. This idea provoked the term "monstrous" so let's leave it there :-)

The point isn't whether or not Feliz.Engine + HTML Engine can match the features, or that the Feliz.Engine syntax "looks the same," it's that both server and client side run the same code on the same inputs and get the same outputs.

I was lost again for a second = we do expect to run the same page templates on the client and server? But this is actually what you meant with your online editor. The server didn't send the editor a pre-rendered template to bind the custom data into - the client and editor and both built against a shared project which contains the templates.

It's about ViewEngines consuming Templates and producing HTML, client or server side. I thought it was only server side.

lukemcdo commented 2 years ago

The idea isn't monstrous but when it goes sideways, the maintenance burden can be quite bad. If the project you decide to track doesn't go the same direction with its DSL, you might end up having to implement things you don't want to in order for the templates to remain compatible. That could be easy if the project you're tracking is also aiming to be compatible though. Not necessarily monstrous, but there's potential for problems, and the worst-case is monstrous and pretty easy to end up at by accident.

I was splitting the line with "we do expect to run the same page templates" on the more difficult parts of Sutil templates, not on templates at all. The template structure and data fields should be supported the same on client and server, but the server shouldn't be expected to do anything that would require invoking Fable on parts of the template before executing it server-side. Sutil OnClick bindings don't look fun to implement, for example. Not sure how I'd solve that, or maybe I'm overthinking it.

I'll try and pose the example. I'd share this template as a file in a .NET Standard MyViews.fsproj. Maybe I import it via a private nuget feed or shared project or something.

module App

open Sutil
open Sutil.DOM
open Sutil.Attr

let view() =
    Html.div [
        style [
            Css.fontFamily "Arial, Helvetica,sans-serif"
            Css.textAlign "center"
            Css.marginTop "40px"
            Css.fontSize "10ex"
        ]
        text "Hello World"
    ]

//mountElement "sutil-app" (view())

But the mountElement function is left off in the theoretical shared project.

My client then just calls mountElement with view(), my server assigns the view() function to a route.

But in this example, the shared component could only be the inner divs, unless the bindStore is evaluated with its default value server-side and the onClicks are left off until Sutil client-side takes over/rehydrates. But most people using Sutil would probably want its default state evaluated so the templates could be shared as-is. As I understand it, supporting this would not be a trivial change to Fable.Lit:

module Counter

open Sutil
open Sutil.DOM
open Sutil.Attr

let view() =
    Html.div [
        bindStore 0 <| fun count -> fragment [
            Html.div [
                class' "block"
                Bind.el(count, fun n -> text $"Counter = {n}")
            ]

            Html.div [
                class' "block"
                Html.button [
                    onClick (fun _ -> count <~= (fun n -> n-1)) []
                    text "-"
                ]

                Html.button [
                    onClick (fun _ -> count <~= (fun n -> n+1)) []
                    text "+"
                ]
            ]]
    ]

Anyway, I hope that clears up my view and understanding of it.

davedawkins commented 2 years ago

It's made it a lot clearer, many thanks for taking the time. I think I should write an app with Feliz and Feliz.ViewEngine to learn more.

AngelMunoz commented 2 years ago

There are two main approaches here to take in mind

  1. PreRendering
  2. Server Side Rendering (SSR)

Pre-rendered pages are like the approaches you have already discussed here, the server compiles the HTML on the fly on each request, and the full page is served, Sutil then would need to pick up the existing DOM and enhance it with whatever client code is made.

Server Side Rendering is the more complete (and complicated) approach where the SutilElement instances have some sort of life-cycle hooks the user can tap into and choose whether some code executes in the server OR on the client side, this can mean fetch data, this can mean updating the DOM tree or any other operation related to expresing how the UI needs to be shown, the server sends a particular DOM tree and the client-side then needs to "re-hydrate" (i.e. moving from static HTML to dynamic via JS or Fable code) the DOM tree, this is the approach used in things like nextjs, nuxt.

Both approaches require server support (likely an ASP.NET middleware in our case) the Feliz folks already have an SSR implementation but that's because react provides that

here's a little bit more on SSR

https://ssr.vuejs.org/#what-is-server-side-rendering-ssr

What I think it's missing (and also the hard part) is that Sutil would need to know how to "hydrate" existing DOM trees sent from the backend on client side

For example given the pre-rendered DOM Tree:

<div id="root">
  <ul>
    <li>Car</li>
    <li>Train</li>
    <li>Aurplane</li>
    <li>Boat</li>
  </ul>
</div>

the Sutil code like this

let vehicles = Store.make ["car";"train";"Airplane";"Boat"; "Bicycle"]

let view() = 
  Html.div [ 
    Html.ul [
      Bind.el(vehicles, fun vehicle -> Html.li [Html.text vehicle])
    ]
  ]

Program.mountElement "root" (view())

would then need to append the bicycle element rather than re-render the whole DOM Tree, that I think would be the ideal first goal once Pre-rendering is enabled the other scenarios might be able to be tacked

bigjonroberts commented 1 year ago

I think this should be revisited now that sveltkit has hit GA.

I think now we would just need some bindings for sveltkit apis similar to this set for NextJS.

The biggest hurdle here might be how to integrate with vite?

joprice commented 3 months ago

Although I think the conversation was about a native dotnet version to enable ssr, I'd be interested in a node-based one as well (and perhaps others would want rust, python, or other languages that fable can output). It could be a done as separate project that produces the same output expected by the hydration step, or perhaps could also make use of more that if the SSR implementation doesn't depend directly on aspnet pieces, mapping from request to response at the edges.