git.iko.soy/rowtype-yoga/ry-starter-spa
on main help sign in
Add Server-Side-Rendering
commit96ed4b9d92d8928ca7c57d454f4f3fb64ac20a42 authorMark Eibes <mark.eibes@gmail.com> dateFri 31 Jul 2020, 18:45 (6 years ago) parentc7e1e0f
12 files changed+160−25
index.dev.js+1−1 index.prod.js+1−1 src/App/Page/Home/View.purs+6−2 src/App/Prelude/View.purs+1−1 src/Main.purs+32−11 ui-server/src/App+1 ui-server/src/Main.js+2 ui-server/src/Main.purs+76 ui-server/src/Server/Emotion.js+8 ui-server/src/Server/Emotion.purs+23 webpack.common.js−9 webpack.dev.js+9
index.dev.js+1−1
@@ -4,4 +4,4 @@ if (module.hot) {
44 module.hot.accept();
55}
66
7−Main.main()
7+Main.main({ serverSideRendering: false})()
index.prod.js+1−1
@@ -1,2 +1,2 @@
11var Main = require("./dce-output/Main");
2−Main.main()
2+Main.main({ serverSideRendering: true})()
src/App/Page/Home/View.purs+6−2
@@ -4,6 +4,7 @@ import App.Prelude.View
44
55import App.Components.Home.Style as Styles
66import App.Components.LoadingAnimation as LoadingAnimation
7+import Effect.Class.Console (log)
78import React.Basic.DOM as R
89import React.Basic.Emotion as E
910
@@ -20,8 +21,11 @@ component =
2021 , css: Styles.content
2122 , children:
2223 [ element LoadingAnimation.component { size: "14rem" }
23− , R.h1_ [ R.text "Welcome" ]
24− , R.a { href: "/docs", children: [R.text "What's next?"]}
24+ , R.h1
25+ { onClick: handler_ (log "Clicked")
26+ , children: [ R.text "Welcome" ]
27+ }
28+ , R.a { href: "/docs", children: [ R.text "What's next?" ] }
2529 ]
2630 }
2731 ]
src/App/Prelude/View.purs+1−1
@@ -8,5 +8,5 @@ module App.Prelude.View
88import App.Prelude.Default
99
1010import React.Basic.Events (class Merge, EventFn, EventHandler, SyntheticEvent, handler, handler_, merge, mergeImpl, syntheticEvent, unsafeEventFn)
11−import React.Basic.Hooks (type (/\), Component, Hook, JSX, Pure, ReactChildren, ReactComponent, ReactContext, Ref, Render, UnsafeReference(..), UseContext, UseDebugValue, UseEffect, UseLayoutEffect, UseLazy, UseMemo, UseReducer, UseRef, UseState, coerceHook, consumer, contextConsumer, contextProvider, createContext, displayName, element, elementKeyed, empty, fragment, keyed, memo, provider, reactChildrenFromArray, reactChildrenToArray, reactComponentFromHook, readRef, readRefMaybe, unsafeHook, unsafeRenderEffect, useContext, useDebugValue, useEffect, useEffectAlways, useEffectOnce, useLayoutEffect, useLayoutEffectAlways, useLayoutEffectOnce, useLazy, useMemo, useReducer, useRef, useState, useState', writeRef, (/\))
11+import React.Basic.Hooks (type (/\), Component, Hook, JSX, Pure, ReactChildren, ReactComponent, ReactContext, Ref, Render, UnsafeReference(..), UseContext, UseDebugValue, UseEffect, UseLayoutEffect, UseMemo, UseReducer, UseRef, UseState, coerceHook, consumer, contextConsumer, contextProvider, createContext, displayName, element, elementKeyed, empty, fragment, keyed, memo, provider, reactChildrenFromArray, reactChildrenToArray, reactComponentFromHook, readRef, readRefMaybe, unsafeHook, unsafeRenderEffect, useContext, useDebugValue, useEffect, useEffectAlways, useEffectOnce, useLayoutEffect, useLayoutEffectAlways, useLayoutEffectOnce, useMemo, useReducer, useRef, useState, useState', writeRef, (/\))
1212import App.Yoga (reactComponent, reactComponentWithChildren)
src/Main.purs+32−11
@@ -4,20 +4,38 @@ import App.Prelude.Default
44
55import App.Components.Router.Component as Router
66import Effect.Exception (throw)
7+import Page.Routes (parseOrNotFound)
78import React.Basic (JSX, element)
8−import React.Basic.DOM (render)
9+import React.Basic.DOM (render, hydrate)
910import Routing.PushState as PushState
1011import Web.DOM (Element)
1112import Web.DOM.NonElementParentNode (getElementById)
13+import Web.Event.EventTarget (addEventListener, eventListener)
1214import Web.HTML (window)
15+import Web.HTML.Event.EventTypes (load)
1316import Web.HTML.HTMLDocument (toNonElementParentNode)
1417import Web.HTML.Window (document)
18+import Web.HTML.Window as Window
19+
20+main ∷ { serverSideRendering ∷ Boolean } -> Effect Unit
21+main { serverSideRendering } = do
22+ app <- mkApp
23+ runOnLoad do
24+ container <- getContainerById containerId
25+ create app container
26+ where
27+ create =
28+ if serverSideRendering
29+ then hydrate
30+ else render
1531
1632-- | Constructs the main React Component for this single-page-application.
17−mkApp :: Effect JSX
33+mkApp ∷ Effect JSX
1834mkApp = do
1935 nav <- PushState.makeInterface
20− pure $ element Router.component { nav }
36+ locationState <- nav.locationState
37+ let initialRoute = parseOrNotFound locationState.path
38+ pure $ element Router.component { nav, initialRoute }
2139
2240-- | The id of the HTML element which will wrap the React App.
2341-- | It needs to match your `index.html` file, for example:
@@ -26,22 +44,25 @@ mkApp = do
2644-- | <div id="container"></div>
2745-- |</body>
2846-- |```
29−containerId :: String
47+containerId ∷ String
3048containerId = "container"
3149
32−main :: Effect Unit
33−main = do
34− container <- getContainerById containerId
35− app <- mkApp
36− render app container
37−
3850-- | Finds an Element based on its ID in
3951-- | the entire document and throws if it
4052-- | can't be found
41−getContainerById :: String -> Effect Element
53+getContainerById ∷ String -> Effect Element
4254getContainerById id = do
4355 documentNode <- window >>= document <#> toNonElementParentNode
4456 maybeContainerElement <- getElementById id documentNode
4557 case maybeContainerElement of
4658 Just containerElement -> pure containerElement
4759 Nothing -> throw "Container element not found."
60+
61+-- | Runs an effect when the "onload" event on window fires
62+runOnLoad :: Effect Unit -> Effect Unit
63+runOnLoad doOnLoad = do
64+ win <- window
65+ listener <- eventListener onLoad
66+ addEventListener load listener false (Window.toEventTarget win)
67+ where
68+ onLoad _ = doOnLoad
ui-server/src/Appnew file+1
@@ -0,0 +1 @@
1+../../src/App
ui-server/src/Main.jsnew file+2
@@ -0,0 +1,2 @@
1+const reactDom = require("react-dom/server")
2+exports.renderToString = reactDom.renderToString
ui-server/src/Main.pursnew file+76
@@ -0,0 +1,76 @@
1+module Main where
2+
3+import Prelude
4+
5+import App.Components.Router.Component as Router
6+import Data.Foldable (intercalate)
7+import Data.Interpolate (i)
8+import Effect (Effect)
9+import Effect.Console (log)
10+import Node.Express.App (App, get, listenHttp)
11+import Node.Express.Request (getPath)
12+import Node.Express.Response (send, sendFile)
13+import Node.HTTP (Server)
14+import Page.Routes (parseOrNotFound)
15+import React.Basic (JSX, element)
16+import React.Basic.Hooks (reactChildrenFromArray)
17+import Server.Emotion (getExtractCritical)
18+import Server.Emotion as Emotion
19+
20+main ∷ Effect Server
21+main = do
22+ app <- mkApp
23+ listenHttp app 55555 \_ ->
24+ log $ "Listening on " <> show 55555
25+
26+foreign import renderToString ∷ JSX -> String
27+
28+mkApp ∷ Effect App
29+mkApp = do
30+ emotionCache <- Emotion.createCache
31+ emotionServer <- Emotion.createEmotionServer emotionCache
32+ pure $ handlers emotionCache emotionServer
33+
34+handlers :: Emotion.Cache -> Emotion.Server -> App
35+handlers emotionCache emotionServer = do
36+ -- | Handler for loading our bundled JavaScript React App
37+ get "/bundle.js" do
38+ sendFile "bundle.js"
39+ -- | We always send a response
40+ get "*" do
41+ p <- getPath
42+ let
43+ route = parseOrNotFound p
44+ container = [ element Router.static { route } ] # reactChildrenFromArray
45+ jsx = element Emotion.cacheProvider { value: emotionCache, children: container }
46+ componentString = renderToString jsx
47+ critical = (emotionServer # getExtractCritical) componentString
48+ send $ indexHTML critical
49+
50+indexHTML ∷ Emotion.Critical -> String
51+indexHTML { html, css, ids } =
52+ i
53+ """
54+<!DOCTYPE html>
55+<html lang="en">
56+
57+<head>
58+ <meta charset="utf-8" />
59+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
60+ <link href="https://cdn.jsdelivr.net/npm/victormono@latest/dist/index.min.css" rel="stylesheet">
61+ <style data-emotion-css=""""
62+ (ids # intercalate " ")
63+ "\">"
64+ css
65+ """</style>
66+ <script src="/bundle.js"></script>
67+</head>
68+
69+<body>
70+ <div id="container">"""
71+ html
72+ """</div>
73+</body>
74+
75+</html>
76+"""
ui-server/src/Server/Emotion.jsnew file+8
@@ -0,0 +1,8 @@
1+const core = require('@emotion/core')
2+const createEmotionServer = require('create-emotion-server').default
3+const createCache = require('@emotion/cache').default
4+
5+exports.cacheProvider = core.CacheProvider
6+exports.createEmotionServer = (cache) => () => createEmotionServer(cache)
7+exports.createCache = createCache
8+exports.getExtractCritical = (server) => server.extractCritical
ui-server/src/Server/Emotion.pursnew file+23
@@ -0,0 +1,23 @@
1+module Server.Emotion where
2+
3+import Effect (Effect)
4+import React.Basic (JSX, ReactComponent)
5+import React.Basic.Hooks (ReactChildren)
6+
7+foreign import data Server ∷ Type
8+
9+foreign import data Cache ∷ Type
10+
11+foreign import createEmotionServer ∷ Cache -> Effect Server
12+
13+foreign import createCache ∷ Effect Cache
14+
15+foreign import getExtractCritical ∷ Server -> ExtractCritical
16+
17+foreign import cacheProvider ∷ ReactComponent { value ∷ Cache, children ∷ ReactChildren JSX }
18+
19+type ExtractCritical
20+ = String -> Critical
21+
22+type Critical
23+ = { html ∷ String, css ∷ String, ids ∷ Array String }
webpack.common.js−9
@@ -1,7 +1,5 @@
11"use strict";
22
3−const path = require("path");
4−const HtmlWebpackPlugin = require("html-webpack-plugin");
53const { CleanWebpackPlugin } = require("clean-webpack-plugin");
64
75module.exports = {
@@ -9,12 +7,5 @@ module.exports = {
97 // This plugin deletes (cleans) the output folder
108 // `./dist` in our case
119 new CleanWebpackPlugin(),
12− // This plugin allows us to use a HTML file template.
13− // In the settings we specify its title and 'entry'
14− // specifies a script to be injected into the template
15− new HtmlWebpackPlugin({
16− title: "PureScript Starter",
17− template: path.resolve(__dirname, "src", "index.html"),
18− }),
1910 ],
2011};
webpack.dev.js+9
@@ -4,6 +4,7 @@ const path = require("path");
44const { merge } = require('webpack-merge');
55const common = require('./webpack.common.js');
66const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
7+const HtmlWebpackPlugin = require("html-webpack-plugin");
78
89module.exports = merge(common, {
910 mode: 'development',
@@ -16,6 +17,14 @@ module.exports = merge(common, {
1617 // This plugin updates React components without losing their state
1718 plugins: [
1819 new ReactRefreshWebpackPlugin(),
20+
21+ // This plugin allows us to use a HTML file template.
22+ // In the settings we specify its title and 'entry'
23+ // specifies a script to be injected into the template
24+ new HtmlWebpackPlugin({
25+ title: "PureScript Starter",
26+ template: path.resolve(__dirname, "src", "index.html"),
27+ })
1928 ],
2029
2130 devServer: {