|
| 1 | +# Five Step to Enable Server-Side Rendering in your Elmish + Dotnet App! |
| 2 | + |
| 3 | +[SSR Sample App of safe-stack template is awailable!](https://github.com/fable-compiler/fable-react/tree/master/Samples/SSRSample) |
| 4 | + |
| 5 | +## Step 1: Reorganize your source files |
| 6 | + |
| 7 | +Separate all your elmish view and types to standalone files, like this: |
| 8 | + |
| 9 | +```F# |
| 10 | +pages |
| 11 | +|-- Home |
| 12 | + |-- View.fs // contains view function. |
| 13 | + |-- Types.fs // contains msg and model type definitions, also should include init function. |
| 14 | + |-- State.fs // contains update function |
| 15 | +
|
| 16 | +``` |
| 17 | + |
| 18 | +View.fs and Types.fs will be shared between client and server. |
| 19 | + |
| 20 | +## Step 2. Make shared files can be executed on the server side |
| 21 | + |
| 22 | +Some code works in Fable might throw a run time exception when executed on dotnet, so we should be careful with unsafe type casting and add compiler directives to remove some code if necessary. |
| 23 | + |
| 24 | +Here are some hints about doing this: |
| 25 | + |
| 26 | +### 1. Replace unsafe cast (unbox and `!!`) in your html attributes and css props with `HTMLAttr.Custom`, `SVGAttr.Custom` and `CSSProp.Custom` |
| 27 | + |
| 28 | +```diff |
| 29 | +- div [ !!("class", "container") ] [] |
| 30 | ++ div [ HTMLAttr.Custom ("class", "container") ] |
| 31 | + |
| 32 | + |
| 33 | +- div [ Style [ !!("class", "container") ] ] [] |
| 34 | ++ div [ Style [ CSSProp.Custom("class", "container") ] ] [] |
| 35 | + |
| 36 | +- svg [ !!("width", 100) ] [] |
| 37 | ++ svg [ SVGAttr.Custom("class", "container") ] [] |
| 38 | +``` |
| 39 | + |
| 40 | + |
| 41 | +### 2. Make sure your browser/js code won't be executed on the server side |
| 42 | + |
| 43 | +One big challenge of sharing code between client and sever is that server side has different API environment with client side. In this respect Fable + dotnet's SSR is has not much difference with nodejs, except in dotnet you should not only prevent browser's API call, but also js. |
| 44 | + |
| 45 | +Thanks for Fable Compiler's `FABLE_COMPILER` directive, we can easly distinguish client environment and server environment and execute different code in each environment: |
| 46 | + |
| 47 | +```#F |
| 48 | +#if FABLE_COMPILER |
| 49 | + executeOnClient () |
| 50 | +#else |
| 51 | + executeOnServer () |
| 52 | +#endif |
| 53 | +``` |
| 54 | + |
| 55 | +We also provice a help function in `Fable.Helpers.Isomorphic` of this, the definition is: |
| 56 | + |
| 57 | +```F# |
| 58 | +let inline isomorphicExec clientFn serverFn input = |
| 59 | +#if FABLE_COMPILER |
| 60 | + clientFn input |
| 61 | +#else |
| 62 | + serverFn input |
| 63 | +#endif |
| 64 | +``` |
| 65 | + |
| 66 | +Full example: |
| 67 | + |
| 68 | +```diff |
| 69 | +open Fable.Core |
| 70 | +open Fable.Import.JS |
| 71 | +open Fable.Helpers.Isomorphic |
| 72 | +open Fable.Import.Browser |
| 73 | + |
| 74 | +// example code to make your document's title has marquee effect |
| 75 | +-window.setInterval( |
| 76 | +- fun () -> |
| 77 | +- document.title <- document.title.[1..len - 1] + document.title.[0..0], |
| 78 | +- 600 |
| 79 | +-) |
| 80 | + |
| 81 | + |
| 82 | ++let inline clientFn () = |
| 83 | ++ window.setInterval( |
| 84 | ++ fun () -> |
| 85 | ++ document.title <- document.title.[1..len - 1] + document.title.[0..0], |
| 86 | ++ 600 |
| 87 | ++ ) |
| 88 | ++isomorphicExec clientFn ignore () |
| 89 | +``` |
| 90 | + |
| 91 | + |
| 92 | +### 3. Add placeholder for components cannot been rendered on the serve side, like js native components. |
| 93 | + |
| 94 | +In `Fable.Helpers.Isomorphic` we also implemented a help funciton to render a placeholder element for components that cannot been rendered on the server side, this function will also help [React.hydrate](https://reactjs.org/docs/react-dom.html#hydrate) to understand the differences between htmls rendered by client and server, so React won't treat it as a mistake and warn about it. |
| 95 | + |
| 96 | +```diff |
| 97 | +open Fable.Core |
| 98 | +open Fable.Import.JS |
| 99 | +open Fable.Helpers.React |
| 100 | +open Fable.Helpers.Isomorphic |
| 101 | +open Fable.Import.Browser |
| 102 | + |
| 103 | +type [<Pojo>] JsCompProps = { |
| 104 | + text: string |
| 105 | +} |
| 106 | + |
| 107 | + |
| 108 | +let jsComp (props: JsCompProps) = |
| 109 | + ofImport "default" "./jsComp" props [] |
| 110 | + |
| 111 | +-jsComp { text="I'm rendered by a js Component!" } |
| 112 | + |
| 113 | ++let jsCompServer (props: JsCompProps) = |
| 114 | ++ div [] [ str "loading" ] |
| 115 | ++ |
| 116 | ++isomorphicView jsComp jsCompServer { text="I'm rendered by a js Component!" } |
| 117 | +``` |
| 118 | + |
| 119 | +## Step 3. Create your init state on the server side. |
| 120 | + |
| 121 | +On the server side, you could create routes like normal MVC app, just make sure the model passed to server rendering function is exactly match the model on the client side in current route. |
| 122 | + |
| 123 | +Here is an example: |
| 124 | + |
| 125 | +```F# |
| 126 | +
|
| 127 | +open Giraffe |
| 128 | +open Giraffe.GiraffeViewEngine |
| 129 | +open FableJson |
| 130 | +
|
| 131 | +let initState: Model = { |
| 132 | + counter = Some 42 |
| 133 | + someString = "Some String" |
| 134 | + someFloat = 11.11 |
| 135 | + someInt = 22 |
| 136 | +} |
| 137 | +
|
| 138 | +let renderHtml () = |
| 139 | + // This would render the html by model create on the server side. |
| 140 | + // Note in an Elmish app, view function takes two parameters, |
| 141 | + // the first is model, and the second is dispatch, |
| 142 | + // which simple ignored here because React will bind event handlers for you on the client side. |
| 143 | + let htmlStr = Fable.Helpers.ReactServer.renderToString(Client.View.view initState ignore) |
| 144 | +
|
| 145 | + // We also need to pass the model to Elmish and React by print a json string in html to let them know what's the model that used to rendering the html. |
| 146 | + // Note we call ofJson twice here, |
| 147 | + // because Elmish's model can contains some complicate type instead of pojo, |
| 148 | + // the first one will seriallize the state to json string, |
| 149 | + // and the second one will seriallize the json string to a legally js string, |
| 150 | + // so we can deseriallize it by Fable's ofJson and get the correct types. |
| 151 | + let stateJsonStr = toJson (toJson initState) |
| 152 | +
|
| 153 | + html [] |
| 154 | + [ head [] [] |
| 155 | + body [] |
| 156 | + [ div [_id "elmish-app"] [ rawText htmlStr ] |
| 157 | + script [] |
| 158 | + [ rawText (sprintf """ |
| 159 | + var __INIT_STATE__ = %s |
| 160 | + """ stateJsonStr) ] // |
| 161 | + script [ _src (assetsBaseUrl + "/public/bundle.js") ] [] |
| 162 | + ] |
| 163 | + ] |
| 164 | +``` |
| 165 | + |
| 166 | +## Step 4. Update your elmish app's init function |
| 167 | + |
| 168 | +1. Init your elmish app by state printed in the html. |
| 169 | +2. Remove init commands that still fetch data which already printed in the html. |
| 170 | + |
| 171 | +e.g. |
| 172 | + |
| 173 | +```F# |
| 174 | +let init () = |
| 175 | + // Init model by server side state |
| 176 | + let model = ofJson<Model> !!window?__INIT_STATE__ |
| 177 | + // let cmd = |
| 178 | + // Cmd.ofPromise |
| 179 | + // (fetchAs<int> "/api/init") |
| 180 | + // [] |
| 181 | + // (Ok >> Init) |
| 182 | + // (Error >> Init) |
| 183 | + model, Cmd.none |
| 184 | +``` |
| 185 | + |
| 186 | +## Step 5. Using React.hydrate to render your app |
| 187 | + |
| 188 | +```diff |
| 189 | +Program.mkProgram init update view |
| 190 | +#if DEBUG |
| 191 | +|> Program.withConsoleTrace |
| 192 | +|> Program.withHMR |
| 193 | +#endif |
| 194 | +-|> Program.withReact "elmish-app" |
| 195 | ++|> Program.withReactHydrate "elmish-app" |
| 196 | +#if DEBUG |
| 197 | +|> Program.withDebugger |
| 198 | +#endif |
| 199 | +|> Program.run |
| 200 | +``` |
| 201 | + |
| 202 | +Now enjoy! If you find bugs or just need some help, please create an issue and let we know. |
| 203 | + |
| 204 | +## Try the sample app |
| 205 | + |
| 206 | +```sh |
| 207 | +git clone https://github.com/fable-compiler/fable-react.git |
| 208 | +cd ./fable-react/Samples/SSRSample/ |
| 209 | +./build.sh # or ./build.cmd in windows |
| 210 | +``` |
0 commit comments