Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Server Side Rendering Support #65

Merged
merged 8 commits into from
Mar 1, 2018

Conversation

zaaack
Copy link
Contributor

@zaaack zaaack commented Feb 27, 2018

Pure F#, No Nodejs Required!

Recently I'm working on a web app which needs SEO, while search engines in my country don't support SPA well, so server-side-rendering becomes my first choice. At first, I was looking at something like aspnet/JavaScriptServices, but it's a little complicated, and running nodejs instances in an asp.net server would require more overhead. it's just too complicated for my case.

This approach is inspired by Giraffe's view engine, it runs fable-react in the server side by simply replace each HTML tag to an HTMLNode tree, so it can be rendered to HTML.

Props

  • Simplier, don't need to install nodejs or maintain nodejs instances on the server-side.
  • Easier, everything is F#, easy integration with Elmish.
  • Faster, about 2x ~ 3x faster then node + react-dom/server (with NODE_ENV=production) in my macbook
  • Support React 16's new server-side rendering API (.hydrate)

Cons

Elements created from js native won't render.

Elements support server-side rendering:

  • Pure functions (e.g. Elmish Components).
  • Class Components written in F# (ofType)
  • Function Componets written in F# (ofFunction)
  • React Components from FFI (ofImport)
    ...

Copy link
Member

@alfonsogarciacaro alfonsogarciacaro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one! So is this to add a React API that can be used from .NET servers? Will the generated tags be recognized by React on the frontend side? (I think they need some special attributes for that.)

match child with
| Some c -> c
| None -> (renderList (children |> Seq.cast))
sprintf "<%s %s>%s</%s>" tag attrs child tag
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sprintf is a bit slow, I'd use + for string concatenation here

[ rawText (sprintf """
var __INIT_STATE__ = %s
""" (toJson initState)) ]
script [ _src "http://localhost:8080/public/bundle.js" ] []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if this is a sample, I'd try to avoid the URL literal here. If you can't use public/bundle.js because Wepback dev server and Giraffe are running in different ports, I'd use a variable so to minimize the chances http://localhost:8080 reaches production.

@@ -22,9 +22,13 @@ module Props =
| Key of string
| Ref of (Browser.Element->unit)
interface IHTMLProp
[<Pojo>]
type DangeousHtml = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor typo: DangerousHtml

@zaaack
Copy link
Contributor Author

zaaack commented Feb 28, 2018

@alfonsogarciacaro Sorry for my late, I was planning to reply in this morning but got a busy day, then I forgot...:sweat_smile:

I haven't tried this with React 16's new server-side rendering API .hydrate yet, but I suppose it will be OK because the HTML generated in server looks really like those from react-dom/server. I'll add it to samples to it works or not.

@@ -755,22 +804,21 @@ let inline ofFunction<[<Pojo>]'P> (f: 'P -> ReactElement) (props: 'P) (children:
/// Example: `ofImport "Map" "leaflet" { x = 10; y = 50 } []`
let inline ofImport<[<Pojo>]'P> (importMember: string) (importPath: string) (props: 'P) (children: ReactElement list): ReactElement =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking to surround ofImport, ofType, ofFunction with #if FABLE_COMPILER to remove them when compiling on server-side. But this would cause conflicts with IDEs on frontend side, because they don't define FABLE_COMPILER. Not sure, what's the best solution. I guess ofImport throws an exception quickly (and users won't probably import a JS file in .NET either) but I don't know what happens with ofType and ofFunction. Do they throw exception or fail silently?

The other option would be to extract common parts into a Fable.React.Core package and distribute two packages: Fable.React for JS side and Fable.ReactServer or Fable.React.Net 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need a design guideline for shared libraries between JS and .NET. @MangelMaxime is already working on this: haf/Http.fs#142

Copy link
Contributor Author

@zaaack zaaack Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess ofImport throws an exception quickly (and users won't probably import a JS file in .NET either) but I don't know what happens with ofType and ofFunction. Do they throw exception or fail silently?

I was thinking this problem, too. In my imagination, we can create a help function by wrapping runtime exceptions in a function that only executed in the client side, but not tested yet.

let hybridView (clientView: 'model -> ReactElement) (serverView: 'model -> ReactElement) (model: 'model) =
#if FABLE_COMPILER
    clientView model
#else
    serverView model
#endif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And import a js module could write like this:

#if FABLE_COMPILER
let JsComp: React.ComponentClass<JsCompProps> = importDefault "./jsComp"
#else
let JsComp = Unchecked.defaultof<React.ComponentClass<JsCompProps>>
#endif

@zaaack zaaack force-pushed the master branch 2 times, most recently from 30844e7 to 88f1b03 Compare March 1, 2018 03:47
@zaaack
Copy link
Contributor Author

zaaack commented Mar 1, 2018

@alfonsogarciacaro ofType and ofFunction are working now! By using dotnet's reflection API we can render React Components written in F# just as react-dom/server do!

But I got some trouble when implementing an isomorphic version of import, any thoughts or advice about this?

repl

[<Emit(""" require($1)[$0] """)>]
let importOrDefault<'T> (selector: string) (path: string) =
    Unchecked.defaultof<'T>

@alfonsogarciacaro
Copy link
Member

I wouldn't use the Emit attribute with import expressions for several reasons:

If you've made ofType and ofFunction work that's awesome! I wouldn't worry too much about ofImport: if users try to compile it on the server side, a "JS only" exception will be thrown right away so hopefully users will spot the problem quickly.

I'm OK with merging this 👍 Is it good to go or do you want to work a bit more on it?

@zaaack
Copy link
Contributor Author

zaaack commented Mar 1, 2018

@alfonsogarciacaro I think I'm done here if I don't need to implement isomorphic import. Please tell me if there is something wrong or missing.

@alfonsogarciacaro
Copy link
Member

Awesome, thanks a lot for this amazing feature @zaaack! I'll merge, try locally and let you know if I have any question 😄

@alfonsogarciacaro alfonsogarciacaro merged commit 38ae554 into fable-compiler:master Mar 1, 2018
@forki
Copy link
Collaborator

forki commented Mar 1, 2018 via email

@forki
Copy link
Collaborator

forki commented Mar 2, 2018

@zaaack any chance you cou try to add SSR to https://github.com/SAFE-Stack/SAFE-BookStore

also the tutotial should contain some guidance on when to use it and when not.
Also needs more DotNet core advertising - I know MS loves that

@zaaack
Copy link
Contributor Author

zaaack commented Mar 2, 2018

@forki

any chance you cou try to add SSR to https://github.com/SAFE-Stack/SAFE-BookStore
The sample project actually is based on the SAFE-Stack template! I think I can send a pr to SAFE-BookStore if it's OK to have SSR as default in SAFE-Stack.

also the tutotial should contain some guidance on when to use it and when not.
Also needs more DotNet core advertising - I know MS loves that

Good advice, I think I can add some guidance in the tutorial, but I'm not sure how to add DotNet core advertising? Actually I'm not an experienced dotnet developer, not sure about how to do this right...

@forki
Copy link
Collaborator

forki commented Mar 2, 2018

that dotnet core advertising is something that we need to take to the MS guys. Don't worry about that.
You did fantastic work here!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants