1
1
mirror of https://github.com/srid/ema.git synced 2024-11-29 17:46:08 +03:00

Speed up hot reload using Dom patching (#11)

* Speed DOM replace using patch via morphdom

* Don't send JS shims when sending HTML down websocket

Don't need.

* Fix a bug with recursive route switch

* Iron out some bugs
This commit is contained in:
Sridhar Ratnakumar 2021-04-22 13:53:22 -04:00 committed by GitHub
parent 7c7df0cb63
commit 14ffcdd1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 32 deletions

View File

@ -28,22 +28,22 @@ Run `bin/run` (or <kbd>Ctrl+Shift+B</kbd> in VSCode). This runs the clock exampl
- [x] client to server reconnect (on ghcid reload, or accidental client disconnect)
- [x] or, investigate https://hackage.haskell.org/package/ghci-websockets
- [x] Multi-websocket-client support
- [ ] Refactor Server.hs
- [ ] Static site generation mode
- [ ] add common examples,
- [x] filesystem watcher
- [ ] docs site for self (w/ sidebar and possibly even search)
pre-announce,
- [ ] plan features / messaging, re: hakyll
- Safer and simpler routes system
- Template system?
- [ ] CLI UX (opts, logging, etc.)
- [ ] documentation ([howto](https://documentation.divio.com/))
- [ ] How to serve non-generated files (css, img, etc.)
- [ ] Publish Data.LVar to Hackage
- [ ] documentation ([howto](https://documentation.divio.com/))
doc notes,
- use async:race to avoid ghcid ghosts
- at most one ws client supported right now
- tailwind + blaze-html layout (BlazeWind?) for no-frills getting started
- [dealing with errors](https://github.com/srid/memoir/issues/1)
- messaging re: hakyll
- safer/ simpler routes system
- bring your own templates / DSL

View File

@ -57,7 +57,7 @@ runServerWithWebSocketHotReload port model render = do
Left newHtml -> do
-- The page the user is currently viewing has changed. Send
-- the new HTML to them.
WS.sendTextData conn $ routeHtml newHtml watchingRoute
WS.sendTextData conn $ renderWithEmaHtmlShims newHtml watchingRoute
log $ "[Watch]: ~~> " <> show watchingRoute
loop
Right nextRoute -> do
@ -67,7 +67,7 @@ runServerWithWebSocketHotReload port model render = do
-- request immediately following this).
log $ "[Switch]: <~~ " <> show nextRoute
html <- LVar.get model
WS.sendTextData conn $ routeHtml html nextRoute
WS.sendTextData conn $ renderWithEmaHtmlShims html nextRoute
log $ "[Switch]: ~~> " <> show nextRoute
loop
try loop >>= \case
@ -81,13 +81,14 @@ runServerWithWebSocketHotReload port model render = do
pure (H.status404, "No route")
Just r -> do
val <- LVar.get model
pure (H.status200, routeHtml val r)
pure (H.status200, renderWithEmaShims val r)
f $ Wai.responseLBS status [(H.hContentType, "text/html")] v
renderWithEmaShims m r =
render m r <> emaStatusHtml <> wsClientShim
renderWithEmaHtmlShims m r =
render m r <> emaStatusHtml
routeFromPathInfo =
fromSlug . fmap (fromString . toString)
routeHtml :: model -> route -> LByteString
routeHtml m r = do
render m r <> emaStatusHtml <> wsClientShim
-- | Return the equivalent of WAI's @pathInfo@, from the raw path string
-- (`document.location.pathname`) the browser sends us.
@ -100,14 +101,33 @@ wsClientShim :: LByteString
wsClientShim =
encodeUtf8
[text|
<script type="module" src="https://cdn.jsdelivr.net/npm/morphdom@2.6.1/dist/morphdom-umd.min.js"></script>
<script type="module">
function htmlToElem(html) {
let temp = document.createElement('template');
html = html.trim(); // Never return a space text node as a result
temp.innerHTML = html;
return temp.content.firstChild;
};
function setHtml(elm, html) {
patchInnerHtml(elm, html);
};
function patchInnerHtml(elm, html) {
var htmlElem = htmlToElem(html);
morphdom(elm, html);
};
// Replace the DOM with a new raw HTML
//
// This function tries to trigger evaluation of <script> tags in the
// HTML, but for some reason it doesn't seem to work reliably.
// cf. the shims in Ema.Helper.Tailwind
// https://stackoverflow.com/a/47614491/55246
function setInnerHtml(elm, html) {
function setInnerHtmlSlow(elm, html) {
elm.innerHTML = html;
Array.from(elm.querySelectorAll("script")).forEach(oldScript => {
const newScript = document.createElement("script");
@ -162,24 +182,7 @@ wsClientShim =
ws.send(path);
}
ws.onopen = () => {
// window.connected();
window.hideIndicator();
watchCurrentRoute();
};
ws.onclose = () => {
console.log("ema: closed; reconnecting ..");
window.reloading();
// Reconnect after 1s, which is typical time it takes for ghcid to reboot.
// Then, retry in another 1s. Ideally we need an exponential retry logic.
setTimeout(init, 1000);
};
ws.onmessage = evt => {
console.log("ema: ✍ Replacing DOM")
setInnerHtml(document.documentElement, evt.data);
// Intercept route click events, and ask server for its HTML whilst
// managing history state.
document.body.addEventListener(`click`, e => {
function handleRouteClicks(e) {
const origin = e.target.closest("a");
if (origin) {
if (window.location.host === origin.host) {
@ -189,8 +192,28 @@ wsClientShim =
e.preventDefault();
};
}
});
// Continue observing
};
// Intercept route click events, and ask server for its HTML whilst
// managing history state.
window.addEventListener(`click`, handleRouteClicks);
ws.onopen = () => {
// window.connected();
window.hideIndicator();
watchCurrentRoute();
};
ws.onclose = () => {
console.log("ema: closed; reconnecting ..");
window.removeEventListener(`click`, handleRouteClicks);
window.reloading();
// Reconnect after 1s, which is typical time it takes for ghcid to reboot.
// Then, retry in another 1s. Ideally we need an exponential retry logic.
setTimeout(init, 1000);
};
ws.onmessage = evt => {
console.log("ema: ✍ Patching DOM")
setHtml(document.documentElement, evt.data);
watchCurrentRoute();
};
window.onbeforeunload = evt => { ws.close(); };