frandiox / vite-ssr

Use Vite for server side rendering in Node
MIT License
823 stars 91 forks source link

Vue Teleport Support #173

Open michaelkplai opened 2 years ago

michaelkplai commented 2 years ago

I wanted to ask about support for Vue’s Teleport Component. Currently, using teleport leads to a hydration error.

Teleport SSR considerations have been documented here. Teleport content is exposed in the Vue context object and must be manually injected into the HTML.

I think this can be implemented by adding a teleports field to DocParts and adjusting buildHtmlDocument to inject the teleport content.

In the beginning we can support only teleporting to body like Nuxt does. Additional teleport support could be added (full CSS selectors), but it would require a DOM parser or clever regex.

Please let me know if this is a feature we would be interested in adding. I’d be happy to look into creating a PR.

michaelkplai commented 2 years ago

I’ve got a working local pnpm patch based on this Nuxt PR.

Misc. notes:

MichealPearce commented 2 years ago

Do you have a example of it causing a hydration error? I currently use Teleport perfectly fine within my vite-ssr app.

Is the element you're teleporting to rendered by Vue or outside of it like in the raw html?

michaelkplai commented 2 years ago

I am teleporting to an element outside of Vue in the raw html.

I’ve created a simple reproduction on top of the vitesse template.

Oddly enough I’m not getting a hydration error. However, the teleported elements are not being rendered on the server (verified using view Page Source).

phlegx commented 1 year ago

Has the problem been solved in the meantime?

phlegx commented 1 year ago

@michaelkplai can you share please an example?

phlegx commented 1 year ago

I have found a test in vue source code: ssrTeleport.spec.ts

And here we see how vite-ssr does renderToString: src/vue/entry-server.ts#LL68C24-L68C38

And here the vue core docs about SSR and teleport: https://vuejs.org/guide/scaling-up/ssr.html#teleports

And here the nuxt code with html part bodyPrepend: renderer.ts#L308

And here an example: server.js

So, the problem adding a bodyPrepend to SSR is, that the teleport are included twice in client (server side rendered teleports and client side teleports).

michaelkplai commented 1 year ago

I haven't looked at this problem in the last year and have since migrated away from the library. I created a reproduction previously:

reproduction on top of the vitesse template.

Here's the pnpm patch I used before:

vite-ssr@0.16.0.patch

diff --git a/core/entry-server.js b/core/entry-server.js
index b4a4439f779ac6aa0c75960dd3e855797c75c50a..0d5c97420b0c6ad0e9b78b8403ebb2c261cc5398 100644
--- a/core/entry-server.js
+++ b/core/entry-server.js
@@ -52,6 +52,9 @@ export const viteSSR = function viteSSR(options, hook) {
                 htmlParts.headTags += renderPreloadLinks(htmlParts.dependencies);
             }
         }
+
+        htmlParts.teleports = context.teleports || {}
+
         return {
             html: buildHtmlDocument(template, htmlParts),
             ...htmlParts,
diff --git a/utils/html.js b/utils/html.js
index 9f8f2156aed52847626f1a2e934be8fa39441754..3694f165230ee7428dde8546fbc6e1ecb7e0c523 100644
--- a/utils/html.js
+++ b/utils/html.js
@@ -22,7 +22,7 @@ export function renderPreloadLinks(files) {
 // @ts-ignore
 const containerId = __CONTAINER_ID__;
 const containerRE = new RegExp(`<div id="${containerId}"([\\s\\w\\-"'=[\\]]*)><\\/div>`);
-export function buildHtmlDocument(template, { htmlAttrs, bodyAttrs, headTags, body, initialState }) {
+export function buildHtmlDocument(template, { htmlAttrs, bodyAttrs, headTags, body, initialState, teleports }) {
     // @ts-ignore
     if (__DEV__) {
         if (template.indexOf(`id="${containerId}"`) === -1) {
@@ -33,7 +33,7 @@ export function buildHtmlDocument(template, { htmlAttrs, bodyAttrs, headTags, bo
         template = template.replace('<html', `<html ${htmlAttrs} `);
     }
     if (bodyAttrs) {
-        template = template.replace('<body', `<body ${bodyAttrs} `);
+        template = template.replace('<body>', `<body ${bodyAttrs}>${ teleports.body || '' }`);
     }
     if (headTags) {
         template = template.replace('</head>', `\n${headTags}\n</head>`);
phlegx commented 1 year ago

Like described in the official documentation of vue, we should consider to use an own DOM node:

Avoid targeting body when using Teleports and SSR together - usually, will contain other server-rendered content which makes it impossible for Teleports to determine the correct starting location for hydration. Instead, prefer a dedicated container, e.g. <div id="teleported"></div> which contains only teleported content.

We have two possible solutions:

  1. Teleport components are rendered only on client side (no hydration node mismatch).
<ClientOnly>
  <Teleport to="body">
    ...
  </Teleport>
</ClientOnly> 
  1. Required SSR rendering of teleport components, by using a unique DOM node outside app node (solves hydration node mismatch).
<Teleport to="#teleported">
  ...
</Teleport>

with index.html like:

...
<body>
  <div id="app"></div>
  <div id="teleported"></div>
</body>

SSR rendering does something like:

teleported = context.teleports['#teleported']
...
// Replace <div id="teleported"> with <div id="teleported" data-server-rendered="true">${teleported}

This is a solution for body teleports and SSR only!

phlegx commented 1 year ago

@michaelkplai please take a look at PR #207