From 316696ef9b25981e5393c497160f6e621a482497 Mon Sep 17 00:00:00 2001 From: Sven Tschui Date: Tue, 27 Oct 2020 19:55:39 +0100 Subject: [PATCH 1/2] Very rough draft --- demo/plugins/ssr/ssr-environment.js | 8 +-- demo/public/{index.tsx => app.tsx} | 0 demo/public/document.tsx | 44 ++++++++++++++++ demo/public/hydrate.tsx | 13 +++++ demo/public/hydrateable.tsx | 25 +++++++++ demo/public/hydrateable2.tsx | 25 +++++++++ demo/public/index.html | 22 -------- demo/public/pages/about/index.js | 2 + demo/public/ssr.js | 34 +++---------- demo/public/with-hydration.tsx | 78 +++++++++++++++++++++++++++++ 10 files changed, 197 insertions(+), 54 deletions(-) rename demo/public/{index.tsx => app.tsx} (100%) create mode 100644 demo/public/document.tsx create mode 100644 demo/public/hydrate.tsx create mode 100644 demo/public/hydrateable.tsx create mode 100644 demo/public/hydrateable2.tsx delete mode 100644 demo/public/index.html create mode 100644 demo/public/with-hydration.tsx diff --git a/demo/plugins/ssr/ssr-environment.js b/demo/plugins/ssr/ssr-environment.js index 10c349c4e..023dbec93 100644 --- a/demo/plugins/ssr/ssr-environment.js +++ b/demo/plugins/ssr/ssr-environment.js @@ -103,10 +103,10 @@ function finish({ url, res }, result) { } res.setHeader('Link', scripts.map(script => `<${script.url}>;rel=preload;as=script;crossorigin`).join(', ')); - for (const script of scripts) { - // head += ``; - body += ``; - } + // for (const script of scripts) { + // // head += ``; + // body += ``; + // } if (/<\/head>/i.test(result)) result = result.replace(/(<\/head>)/i, head + '$1'); else result = head + result; diff --git a/demo/public/index.tsx b/demo/public/app.tsx similarity index 100% rename from demo/public/index.tsx rename to demo/public/app.tsx diff --git a/demo/public/document.tsx b/demo/public/document.tsx new file mode 100644 index 000000000..1b802e8b1 --- /dev/null +++ b/demo/public/document.tsx @@ -0,0 +1,44 @@ +import { App } from './app'; +import { Hydratable } from './hydrateable'; +import { HydrationContextProvider, useHydrationRegistrations } from './with-hydration'; + +function HydrationScripts() { + const components = useHydrationRegistrations(); + + return ( + <> + {components?.map(({ specifier, script, targetSelector }, i) => { + const importAs = 'Component'; + + return ( + - - - - - - - - - diff --git a/demo/public/pages/about/index.js b/demo/public/pages/about/index.js index d940e00a1..284a9239e 100644 --- a/demo/public/pages/about/index.js +++ b/demo/public/pages/about/index.js @@ -1,3 +1,4 @@ +import { Hydratable } from '../../hydrateable2'; import styles from './style.module.css'; const About = ({ query }) => ( @@ -5,6 +6,7 @@ const About = ({ query }) => (

About

My name is Jason.

{JSON.stringify(query)}
+ ); diff --git a/demo/public/ssr.js b/demo/public/ssr.js index 12e0044c1..23ddb5436 100644 --- a/demo/public/ssr.js +++ b/demo/public/ssr.js @@ -1,31 +1,9 @@ -import { promises as fs } from 'fs'; import renderToString from 'preact-render-to-string'; +import prepass from 'preact-ssr-prepass'; -async function prepass(vnode, maxDepth = 20, maxTime = 5000) { - let attempts = 0; - const start = Date.now(); - while (++attempts < maxDepth && Date.now() - start < maxTime) { - try { - return renderToString(vnode); - } catch (e) { - if (e && e.then) { - await e; - continue; - } - throw e; - } - } -} - -export async function ssr({ url }) { - const { App } = await import('./index.tsx'); - let body = await prepass(, 20, 5000); - const html = await fs.readFile('./public/index.html', 'utf-8'); - // body = html.replace(/(
)<\/div>/, '$1' + body + '
'); - if (/]*?)?>/.test(html)) { - body = html.replace(/(]*?)?)>/, '$1 ssr>' + body); - } else { - body = html + body; - } - return body; +export async function ssr(req) { + const { Document } = await import('./document.tsx'); + const vnode = ; + await prepass(vnode); + return renderToString(vnode, {}, { pretty: true }); } diff --git a/demo/public/with-hydration.tsx b/demo/public/with-hydration.tsx new file mode 100644 index 000000000..05610baf4 --- /dev/null +++ b/demo/public/with-hydration.tsx @@ -0,0 +1,78 @@ +import { h, createContext } from 'preact'; +import { useContext, useMemo } from 'preact/hooks'; + +interface ComponentRegistration { + script: string; + specifier: string; + targetSelector: string; +} + +let hydrationComponentIdCounter = 0; +let hydrationDataIdCounter = 0; + +interface HydrationContext { + registerComponent(component: ComponentRegistration): void; + getComponents(): ComponentRegistration[]; +} + +export function HydrationContextProvider({ req, children }) { + const hydrationContextValue = useMemo(() => { + const components: ComponentRegistration[] = []; + + return { + registerComponent(comp: ComponentRegistration) { + if (components.find(c => c.targetSelector === comp.targetSelector)) { + return; + } + + components.push(comp); + }, + getComponents() { + return components; + } + }; + }, [req]); + + return {children}; +} + +const hydrationContext = createContext(null); + +export function useHydrationRegistrations() { + return useContext(hydrationContext)?.getComponents(); +} + +export function withHydration({ specifier, importUrl }: { specifier: string; importUrl: string }) { + const componentId = String(hydrationComponentIdCounter++); + + return function (Component) { + return function HydrateableComponent(props) { + if (typeof document !== 'undefined') { + return ; + } + + const ctx = useContext(hydrationContext); + + ctx?.registerComponent?.({ + script: importUrl.replace('http://0.0.0.0:8080', ''), + specifier, + targetSelector: `[data-hydration-component-id="${componentId}"]` + }); + + const dataId = String(hydrationDataIdCounter++); + + return ( + <> +
+ +
+