Vite: Is the --ssr flag fully functional yet?

Created on 16 Aug 2020  路  4Comments  路  Source: vitejs/vite

I'm playing around with a custom vue ssr setup at the moment, and saw there have been some commits around the ssr flag for vite that has largely gone undocumented so far: https://github.com/vitejs/vite/commit/49e79e7603f5a53756f016494dd17ee5f76f37b6

It seems like it's successfully compiling an index.js file that parses the single file components and renders a nice static page when served with express. However, it seems like it's missing a lot: no bundle to hydrate on the client. Static assets for images and css are missing as well. I've been wondering what way i should go about this? Should the client bundle be built separately without the ssr flag? But then how would i know which bundles should be loaded alongside the ssr page?

Before I go down too far into a rabbit hole here trying to making things work and customizing the vite configuration... Is this feature actually supposed to be functional yet, or have these commits just been some work in progress and it's actually impossible to get a config going that fully builds out the static pages but also adds hydration on the client?

Most helpful comment

@cliqer absolutely. I'd also be happy to hear some more input around this issue. It's still very rough and the rehydration is not working as it should yet, since all documentation of vite and vue3 is still partially incomplete, especially around, but what I've done so far is the following:

Setting up a folder structure that has two separate entry points as per the vue docs, so you have a client-entry.js that renders the app normally:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app', true);

and a server-entry.js that rewrites the static asset path and looks like this

import { createSSRApp } from 'vue'
import renderer from '@vue/server-renderer';
import App from './App.vue'

const express = require('express');
const path = require('path');
const server = express();

server.use('/_assets', express.static(path.join(__dirname, '../client/_assets')));

server.get('*', (req, res) => {

  const app = createSSRApp(App);

  ; (async () => {
    const html = await renderer.renderToString(app)
    res.end(`__HTML__`)
  })()
})

server.listen(8080);

then i use a custom build file where i build both the client and the server version:

const { ssrBuild, build } = require('vite')
const replace = require('@rollup/plugin-replace');

;(async () => {
  const clientResult = await build({
    outDir: 'dist/client',
    rollupInputOptions: {
      input: './src/entry-client.js'
    },
  })


  const serverResult = await ssrBuild({
    outDir: 'dist/server',
    rollupPluginVueOptions: {
      target: 'node'
    },
    rollupInputOptions: {
      plugins: [
        replace({
          __HTML__: clientResult.html.replace('<div id="app">', '<div id="app" data-server-rendered="true">${html}')
        })
      ],
      input: './src/entry-server.js'
    },
  })
})()

I parse the html output of the client build and then insert it into the server build. This seems to then serve the SSR version when you serve dist/server/entry-server.js and will shortly after mount the vue instance. However, despite setting data-server-rendered="true" or mounting the app in forced hydration mode, it seems to just remove all DOM nodes and inject new ones. Trying to figure this one out now. If you have any ideas let me know. I'd also be happy to keep the issue open if it could serve as a point of discussion around ssr?

EDIT: i just found out that the rehydration problems were caused by me mixing createApp and createSSRApp. It seems like both client and server need to use createSSRApp. See the updated client-entry.js below:

import { createSSRApp } from 'vue'
import App from './App.vue'
import './index.css'

createSSRApp(App).mount('#app', true);

All 4 comments

nevermind, seems like a custom build file and combining bundles for both client / server seems to do the trick

Hi @tbgse. While we are on it can you please provide an example of how you made it work as a reference?
Many Thanks

@cliqer absolutely. I'd also be happy to hear some more input around this issue. It's still very rough and the rehydration is not working as it should yet, since all documentation of vite and vue3 is still partially incomplete, especially around, but what I've done so far is the following:

Setting up a folder structure that has two separate entry points as per the vue docs, so you have a client-entry.js that renders the app normally:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app', true);

and a server-entry.js that rewrites the static asset path and looks like this

import { createSSRApp } from 'vue'
import renderer from '@vue/server-renderer';
import App from './App.vue'

const express = require('express');
const path = require('path');
const server = express();

server.use('/_assets', express.static(path.join(__dirname, '../client/_assets')));

server.get('*', (req, res) => {

  const app = createSSRApp(App);

  ; (async () => {
    const html = await renderer.renderToString(app)
    res.end(`__HTML__`)
  })()
})

server.listen(8080);

then i use a custom build file where i build both the client and the server version:

const { ssrBuild, build } = require('vite')
const replace = require('@rollup/plugin-replace');

;(async () => {
  const clientResult = await build({
    outDir: 'dist/client',
    rollupInputOptions: {
      input: './src/entry-client.js'
    },
  })


  const serverResult = await ssrBuild({
    outDir: 'dist/server',
    rollupPluginVueOptions: {
      target: 'node'
    },
    rollupInputOptions: {
      plugins: [
        replace({
          __HTML__: clientResult.html.replace('<div id="app">', '<div id="app" data-server-rendered="true">${html}')
        })
      ],
      input: './src/entry-server.js'
    },
  })
})()

I parse the html output of the client build and then insert it into the server build. This seems to then serve the SSR version when you serve dist/server/entry-server.js and will shortly after mount the vue instance. However, despite setting data-server-rendered="true" or mounting the app in forced hydration mode, it seems to just remove all DOM nodes and inject new ones. Trying to figure this one out now. If you have any ideas let me know. I'd also be happy to keep the issue open if it could serve as a point of discussion around ssr?

EDIT: i just found out that the rehydration problems were caused by me mixing createApp and createSSRApp. It seems like both client and server need to use createSSRApp. See the updated client-entry.js below:

import { createSSRApp } from 'vue'
import App from './App.vue'
import './index.css'

createSSRApp(App).mount('#app', true);

For future reference, the custom build is just a normal nodejs script that can be run like any other scriptnode build.js and it is not part of the Vite ecosystem. The script tag in in index.html needs also to be changed to <script type="module" src="/src/entry-client.js"></script>

@tbgse was kind enough to provide an example at : https://github.com/tbgse/vue3-vite-ssr-example
Thank you Tobias

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wobsoriano picture wobsoriano  路  4Comments

stefnotch picture stefnotch  路  3Comments

ashubham picture ashubham  路  3Comments

shen-zhao picture shen-zhao  路  3Comments

Dykam picture Dykam  路  4Comments