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 (SSR) #306

Merged
merged 14 commits into from
Mar 22, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"*.suo":true,
"*.suo":true,
"*.user":true,
"*.sln.docstates":true,
"*.userprefs":true,
Expand All @@ -25,4 +25,4 @@
"src/**/obj":true,
"src/Client/out":true
}
}
}
4 changes: 4 additions & 0 deletions BookStore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{95D2789C-8B80-49E7-915A-AC670C36D3FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The following document describes the [SAFE-Stack](https://safe-stack.github.io/)
SAFE is a technology stack that brings together several technologies into a single, coherent stack for typesafe,
flexible end-to-end web-enabled applications that are written entirely in F#.

![SAFE-Stack](src/Client/images/safe_logo.png "SAFE-Stack")
![SAFE-Stack](src/Client/Images/safe_logo.png "SAFE-Stack")

You can see it running on Microsoft Azure at http://fable-suave.azurewebsites.net.

Expand Down Expand Up @@ -206,7 +206,7 @@ Add the `src/Client/pages/Tomato.fs` to your .fsproj file and move it above `App
4. Change the `Tomato.view` function (and add in required packages):

```fsharp

open Fable.Helpers.React
open Fable.Helpers.React.Props
//...
Expand Down
34 changes: 31 additions & 3 deletions build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@ Target "InstallClient" (fun _ ->

Target "BuildClient" (fun _ ->
runDotnet clientPath "restore"
runDotnet clientPath "fable webpack --port free -- -p"
runDotnet clientPath "fable webpack --port free -- -p --mode production"
)

// --------------------------------------------------------------------------------------
// Rename driver for macOS or Linux

Target "RenameDrivers" (fun _ ->
if not isWindows then
run npmTool "install phantomjs-prebuilt" ""
run yarnTool "add phantomjs-prebuilt" ""
try
if isMacOS && not <| File.Exists "test/UITests/bin/Debug/net461/chromedriver" then
Fake.FileHelper.Rename "test/UITests/bin/Debug/net461/chromedriver" "test/UITests/bin/Debug/net461/chromedriver_macOS"
Expand Down Expand Up @@ -185,6 +185,7 @@ Target "RunClientTests" (fun _ ->

let ipAddress = "localhost"
let port = 8080
let serverPort = 8085

FinalTarget "KillProcess" (fun _ ->
killProcess "dotnet"
Expand All @@ -205,7 +206,7 @@ Target "Run" (fun _ ->

if result <> 0 then failwith "Website shut down." }

let fablewatch = async { runDotnet clientPath "fable webpack-dev-server --port free" }
let fablewatch = async { runDotnet clientPath "fable webpack-dev-server --port free -- --mode development" }
let openBrowser = async {
System.Threading.Thread.Sleep(5000)
Diagnostics.Process.Start("http://"+ ipAddress + sprintf ":%d" port) |> ignore }
Expand All @@ -216,6 +217,30 @@ Target "Run" (fun _ ->
)


Target "RunSSR" (fun _ ->
runDotnet clientPath "restore"
Copy link
Member

Choose a reason for hiding this comment

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

why do we need this? what is it doing?

Copy link
Contributor Author

@zaaack zaaack Mar 7, 2018

Choose a reason for hiding this comment

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

This will set a compile directive DEBUG_SSR to use webpack-dev-server's bundle file path in the server-rendered path. Ideally, in RunSSR we can trigger client and server to rebuild when we edit any file, so we can dev and debug with SSR; but in normal Run we would only trigger the server to rebuild when we edit back-end files, and we can dev with webpack-dev-server and use the server code as an API service.

But I cannot pass the configuration to dotnet-watch to make it work differently in different command, currently, I totally ignored client files' change when watch run the server...

https://github.com/SAFE-Stack/SAFE-BookStore/pull/306/files#diff-694c6c636cd70bcc23aa716434744995R12
https://github.com/SAFE-Stack/SAFE-BookStore/pull/306/files#diff-3b3c3fd8d4441c71716037ce14cb42b0R14

runDotnet serverTestsPath "restore"

let unitTestsWatch = async {
let result =
ExecProcess (fun info ->
info.FileName <- dotnetExePath
info.WorkingDirectory <- serverTestsPath
info.Arguments <- sprintf "watch msbuild /t:TestAndRun /p:DotNetHost=%s /p:DebugSSR=true" dotnetExePath) TimeSpan.MaxValue

if result <> 0 then failwith "Website shut down." }

let fablewatch = async { runDotnet clientPath "fable webpack --port free -- -w --mode development" }
let openBrowser = async {
System.Threading.Thread.Sleep(10000)
Diagnostics.Process.Start("http://"+ ipAddress + sprintf ":%d" serverPort) |> ignore }

Async.Parallel [| unitTestsWatch; fablewatch; openBrowser |]
|> Async.RunSynchronously
|> ignore
)


// --------------------------------------------------------------------------------------
// Release Scripts

Expand Down Expand Up @@ -354,4 +379,7 @@ Target "All" DoNothing
"InstallClient"
==> "Run"

"InstallClient"
==> "RunSSR"

RunTargetOrDefault "All"
6 changes: 4 additions & 2 deletions paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ nuget System.Net.NetworkInformation
nuget jose-jwt

nuget Fable.Core
nuget Fable.React 3.0.0
nuget Fable.Elmish
nuget Fable.Elmish.React
nuget Fable.Elmish.Browser
nuget Fable.Elmish.Debugger
nuget Fable.Elmish.React
nuget Fable.Elmish.HMR
nuget Microsoft.Azure.WebJobs prerelease
nuget Microsoft.Azure.WebJobs.Extensions prerelease
Expand All @@ -41,4 +43,4 @@ group UITests
group Build
source https://nuget.org/api/v2
framework >= net461
nuget FAKE
nuget FAKE
8 changes: 4 additions & 4 deletions paket.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ NUGET
System.Collections.Immutable (>= 1.4) - restriction: >= netstandard1.6
System.Reflection.Metadata (>= 1.5) - restriction: >= netstandard1.6
System.Runtime.Loader (>= 4.3) - restriction: >= netstandard1.6
Fable.Core (1.3.8)
Fable.Core (1.3.11)
FSharp.Core (>= 4.2.3) - restriction: >= netstandard1.6
NETStandard.Library (>= 1.6.1) - restriction: >= netstandard1.6
Fable.Elmish (1.0.1) - restriction: >= netstandard1.6
Fable.Elmish (1.0.1)
Fable.Core (>= 1.2.4) - restriction: >= netstandard1.6
Fable.PowerPack (>= 1.3) - restriction: >= netstandard1.6
FSharp.Core (>= 4.2.3) - restriction: >= netstandard1.6
Expand All @@ -37,7 +37,7 @@ NUGET
Fable.Core (>= 1.2.4) - restriction: >= netstandard1.6
Fable.Elmish (>= 0.9.2) - restriction: >= netstandard1.6
FSharp.Core (>= 4.2.3) - restriction: >= netstandard1.6
Fable.Elmish.React (1.0.1)
Fable.Elmish.React (1.0.2)
Fable.Core (>= 1.3.8) - restriction: >= netstandard1.6
Fable.Elmish (>= 1.0.1) - restriction: >= netstandard1.6
Fable.PowerPack (>= 1.3.2) - restriction: >= netstandard1.6
Expand All @@ -53,7 +53,7 @@ NUGET
Fable.Core (>= 1.3.8) - restriction: >= netstandard1.6
Fable.Import.Browser (>= 1.0) - restriction: >= netstandard1.6
FSharp.Core (>= 4.2.3) - restriction: >= netstandard1.6
Fable.React (2.1) - restriction: >= netstandard1.6
Fable.React (3.0)
Fable.Core (>= 1.3.7) - restriction: >= netstandard1.6
Fable.Import.Browser (>= 0.1) - restriction: >= netstandard1.6
FSharp.Core (>= 4.2.3) - restriction: >= netstandard1.6
Expand Down
94 changes: 32 additions & 62 deletions src/Client/App.fs
Original file line number Diff line number Diff line change
@@ -1,47 +1,31 @@
module Client.App

open Fable.Core
open Fable.Core.JsInterop

open Fable.Import
open Fable.PowerPack
open Elmish
open Elmish.React
open Fable.Import.Browser
open Elmish.Browser.Navigation
open Elmish.HMR
open Client.Shared
open Client.Pages
open ServerCode.Domain

JsInterop.importSideEffects "whatwg-fetch"
JsInterop.importSideEffects "babel-polyfill"

/// The composed model for the different possible page states of the application
type PageModel =
| HomePageModel
| LoginModel of Login.Model
| WishListModel of WishList.Model

/// The composed model for the application, which is a single page state plus login information
type Model =
{ User : UserData option
PageModel : PageModel }

/// The composed set of messages that update the state of the application
type Msg =
| LoggedIn of UserData
| LoggedOut
| StorageFailure of exn
| LoginMsg of Login.Msg
| WishListMsg of WishList.Msg
| Logout of unit

/// The navigation logic of the application given a page identity parsed from the .../#info
let handleNotFound (model: Model) =
Browser.console.error("Error parsing url: " + Browser.window.location.href)
( model, Navigation.modifyUrl (toPath Page.Home) )

/// The navigation logic of the application given a page identity parsed from the .../#info
/// information in the URL.
let urlUpdate (result:Page option) model =
let urlUpdate (result:Page option) (model: Model) =
match result with
| None ->
Browser.console.error("Error parsing url: " + Browser.window.location.href)
( model, Navigation.modifyUrl (toHash Page.Home) )
handleNotFound model

| Some Page.Login ->
let m, cmd = Login.init model.User
Expand Down Expand Up @@ -69,11 +53,17 @@ let deleteUserCmd =

let init result =
let user = loadUser ()
let model =
{ User = user
PageModel = HomePageModel }

urlUpdate result model
let stateJson: string option = !!Browser.window?__INIT_MODEL__
match stateJson, result with
| Some json, Some Page.Home ->
let model: Model = ofJson json
{ model with User = user }, Cmd.none
| _ ->
let model =
{ User = user
PageModel = HomePageModel }

urlUpdate result model

let update msg model =
match msg, model.PageModel with
Expand All @@ -89,7 +79,7 @@ let update msg model =
| Login.ExternalMsg.NoOp ->
Cmd.none
| Login.ExternalMsg.UserLoggedIn newUser ->
Cmd.ofMsg (LoggedIn newUser)
saveUserCmd newUser

{ model with
PageModel = LoginModel m },
Expand All @@ -104,53 +94,33 @@ let update msg model =
{ model with
PageModel = WishListModel m }, Cmd.map WishListMsg cmd

| WishListMsg _, _ ->
| WishListMsg _, _ ->
model, Cmd.none

| LoggedIn newUser, _ ->
let nextPage = Page.WishList
{ model with User = Some newUser },
Cmd.batch [
saveUserCmd newUser
Navigation.newUrl (toHash nextPage) ]
Navigation.newUrl (toPath nextPage) ]

| LoggedOut, _ ->
{ model with
User = None
PageModel = HomePageModel },
Navigation.newUrl (toHash Page.Home)
PageModel = HomePageModel },
Cmd.batch [
Copy link
Member

Choose a reason for hiding this comment

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

don't need to batch here

Navigation.newUrl (toPath Page.Home) ]

| Logout(), _ ->
model, deleteUserCmd

// VIEW

open Fable.Helpers.React
open Fable.Helpers.React.Props
open Client.Style

/// Constructs the view for a page given the model and dispatcher.
let viewPage model dispatch =
match model.PageModel with
| HomePageModel ->
Home.view ()

| LoginModel m ->
[ Login.view m (LoginMsg >> dispatch) ]

| WishListModel m ->
[ WishList.view m (WishListMsg >> dispatch) ]
open Elmish.Debug

/// Constructs the view for the application given the model.
let view model dispatch =
div [] [
Menu.view (Logout >> dispatch) model.User
hr []
div [ centerStyle "column" ] (viewPage model dispatch)
]
let withReact =
if (!!Browser.window?__INIT_MODEL__)
then Program.withReactHydrate
else Program.withReact

open Elmish.React
open Elmish.Debug

// App
Program.mkProgram init update view
Expand All @@ -159,7 +129,7 @@ Program.mkProgram init update view
|> Program.withConsoleTrace
|> Program.withHMR
#endif
|> Program.withReact "elmish-app"
|> withReact "elmish-app"
#if DEBUG
|> Program.withDebugger
#endif
Expand Down
10 changes: 1 addition & 9 deletions src/Client/Client.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<Import Project="../Shared/Shared.props" />
<ItemGroup>
<Compile Include="ReleaseNotes.fs" />
<Compile Include="../Server/Shared/Domain.fs" />
<Compile Include="../Server/Shared/ServerUrls.fs" />
<Compile Include="Pages.fs" />
<Compile Include="Style.fs" />
<Compile Include="views/Menu.fs" />
<Compile Include="pages/Home.fs" />
<Compile Include="pages/WishList.fs" />
<Compile Include="pages/Login.fs" />
<Compile Include="App.fs" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
Expand Down
File renamed without changes
14 changes: 7 additions & 7 deletions src/Client/Pages.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ open Elmish.Browser.UrlParser

/// The different pages of the application. If you add a new page, then add an entry here.
[<RequireQualifiedAccess>]
type Page =
type Page =
| Home
| Login
| WishList

let toHash =
let toPath =
function
| Page.Home -> "#home"
| Page.Login -> "#login"
| Page.WishList -> "#wishlist"
| Page.Home -> "/"
| Page.Login -> "/login"
| Page.WishList -> "/wishlist"


/// The URL is turned into a Result.
let pageParser : Parser<Page -> Page,_> =
oneOf
[ map Page.Home (s "home")
[ map Page.Home (s "")
map Page.Login (s "login")
map Page.WishList (s "wishlist") ]

let urlParser location = parseHash pageParser location
let urlParser location = parsePath pageParser location
Loading