• Stars
    star
    821
  • Rank 55,549 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created about 4 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Use Vite for server side rendering in Node

Vite SSR logo

Vite SSR

Simple yet powerful Server Side Rendering for Vite 2 in Node.js (Vue & React).

  • ⚑ Lightning Fast HMR (powered by Vite, even in SSR mode).
  • πŸ’β€β™‚οΈ Consistent DX experience abstracting most of the SSR complexity.
  • πŸ” Small library, unopinionated about your page routing and API logic.
  • πŸ”₯ Fast and SEO friendly thanks to SSR, with SPA takeover for snappy UX.
  • 🧱 Compatible with Vite's plugin ecosystem such as file-based routing, PWA, etc.

Vite SSR can be deployed to any Node.js or browser-like environment, including serverless platforms like Vercel, Netlify, or even Cloudflare Workers. It can also run with more traditional servers like Express.js or Fastify.

Vite SSR is unopinionated about your API logic so you must bring your own. If you want a more opiniated and fullstack setup with filesystem-based API endpoints and auto-managed edge cache, have a look at Vitedge. It wraps Vite SSR and can be deployed to Cloudflare Workers or any Node.js environment.

Start a new SSR project right away with filesystem routes, i18n, icons, markdown and more with Vitesse (Vue) or Reactesse (React). See live demo.

Installation

Create a normal Vite project for Vue or React.

yarn create vite --template [vue|vue-ts|react|react-ts]

Then, add vite-ssr with your package manager (direct dependency) and your framework router.

# For Vue
yarn add vite-ssr vue@3 vue-router@4 @vueuse/head@^0.9.0

# For React
yarn add vite-ssr react@16 react-router-dom@5

Make sure that index.html contains a root element with id app: <div id="app"></div> (or change the default container id in plugin options: options.containerId).

Usage

Add Vite SSR plugin to your Vite config file (see vite.config.js for a full example).

// vite.config.js
import vue from '@vitejs/plugin-vue'
import viteSSR from 'vite-ssr/plugin.js'
// import react from '@vitejs/plugin-react'

export default {
  plugins: [
    viteSSR(),
    vue(), // react()
  ],
}

Then, simply import the main Vite SSR handler in your main entry file as follows. See full examples for Vue and React.

import App from './App' // Vue or React main app
import routes from './routes'
import viteSSR from 'vite-ssr'
// or from 'vite-ssr/vue' or 'vite-ssr/react', which slightly improves typings

export default viteSSR(App, { routes }, (context) => {
  /* Vite SSR main hook for custom logic */
  /* const { app, router, initialState, ... } = context */
})

That's right, in Vite SSR there's only 1 single entry file by default πŸŽ‰. It will take care of providing your code with the right environment.

If you need conditional logic that should only run in either client or server, use Vite's import.meta.env.SSR boolean variable and the tree-shaking will do the rest.

The third argument is Vite SSR's main hook, which runs only once at the start. It receives the SSR context and can be used to initialize the app or setup anything like state management or other plugins. See an example of Vue + Pinia here. In React, the same SSR Context is passed to the main App function/component as props.

Available options

The previous handler accepts the following options as its second argument:

  • routes: Array of routes, according to each framework's router (see vue-router or react-router-config).
  • base: Function that returns a string with the router base. Can be useful for i18n routes or when the app is not deployed at the domain root.
  • routerOptions: Additional router options like scrollBehavior in vue-router.
  • transformState: Modify the state to be serialized or deserialized. See State serialization for more information.
  • pageProps.passToPage: Whether each route's initialState should be automatically passed to the page components as props.
  • styleCollector: Only in React. Mechanism to extract CSS in JS. See integrations#React-CSS-inJS.
  • debug.mount: Pass false to prevent mounting the app in the client. You will need to do this manually on your own but it's useful to see differences between SSR and hydration.

SSR Context

The context passed to the main hook (and to React's root component) contains:

  • initialState: Object that can be mutated during SSR to save any data to be serialized. This same object and data can be read in the browser.
  • url: Initial URL.
  • isClient: Boolean similar to import.meta.env.SSR. Unlike the latter, isClient does not trigger tree shaking.
  • request: Available during SSR.
  • redirect: Isomorphic function to redirect to a different URL.
  • writeResponse: Function to add status or headers to the response object (only in backend).
  • router: Router instance in Vue, and a custom router in React to access the routes and page components.
  • app: App instance, only in Vue.
  • initialRoute: Initial Route object, only in Vue.

This context can also be accesed from any component by using useContext hook:

import { useContext } from 'vite-ssr'

//...
function() {
  // In a component
  const { initialState, redirect } = useContext()
  // ...
}

Using separate entry files

Even though Vite SSR uses 1 single entry file by default, thus abstracting complexity from your app, you can still have separate entry files for client and server if you need more flexibility. This can happen when building a library on top of Vite SSR, for example.

Simply provide the entry file for the client in index.html (as you would normally do in an SPA) and pass the entry file for the server as a CLI flag: vite-ssr [dev|build] --ssr <path/to/entry-server>.

Then, import the main SSR handlers for the entry files from vite-ssr/vue/entry-client and vite-ssr/vue/entry-server instead. Use vite-ssr/react/* for React.

SSR initial state and data fetching

The SSR initial state is the application data that is serialized as part of the server-rendered HTML for later hydration in the browser. This data is normally gathered using fetch or DB requests from your API code.

Vite SSR initial state consists of a plain JS object that is passed to your application and can be modified at will during SSR. This object will be serialized and later hydrated automatically in the browser, and passed to your app again so you can use it as a data source.

export default viteSSR(App, { routes }, ({ initialState }) => {
  if (import.meta.env.SSR) {
    // Write in server
    initialState.myData = 'DB/API data'
  } else {
    // Read in browser
    console.log(initialState.myData) // => 'DB/API data'
  }

  // Provide the initial state to your stores, components, etc. as you prefer.
})

If you prefer having a solution for data fetching out of the box, have a look at Vitedge. Otherwise, you can implement it as follows:

Initial state in Vue

Vue has multiple ways to provide the initial state to Vite SSR:

  • Calling your API before entering a route (Router's beforeEach or beforeEnter) and populate route.meta.state. Vite SSR will get the first route's state and use it as the SSR initial state. See a full example here.
export default viteSSR(App, { routes }, async ({ app }) => {
  router.beforEach(async (to, from) => {
    if (to.meta.state) {
      return // Already has state
    }

    const response = await fetch('my/api/data/' + to.name)

    // This will modify initialState
    to.meta.state = await response.json()
  })
})
  • Calling your API directly from Vue components using Suspense, and storing the result in the SSR initial state. See a full example with Suspense here. If you prefer Axios, there's also an example here.
import { useContext } from 'vite-ssr'
import { useRoute } from 'vue-router'
import { inject, ref } from 'vue'

// This is a custom hook to fetch data in components
export async function useFetchData(endpoint) {
  const { initialState } = useContext()
  const { name } = useRoute() // this is just a unique key
  const state = ref(initialState[name] || null)

  if (!state.value) {
    state.value = await (await fetch(endpoint)).json()

    if (import.meta.env.SSR) {
      initialState[name] = state.value
    }
  }

  return state
}
// Page Component with Async Setup
export default {
  async setup() {
    const state = await useFetchData('my-api-endpoint')
    return { data }
  },
}

// Use Suspense in your app root
<template>
  <RouterView v-slot="{ Component }">
    <Suspense>
      <component :is="Component" />
    </Suspense>
  </RouterView>
</template>
// Main
export default viteSSR(App, { routes }, ({ app, initialState }) => {
  // You can pass it to your state management
  // or use `useContext()` like in the Suspense example
  const pinia = createPinia()

  // Sync initialState with the store:
  if (import.meta.env.SSR) {
    initialState.pinia = pinia.state.value
  } else {
    pinia.state.value = initialState.pinia
  }

  app.use(pinia)
})

// Page Component with Server Prefetch
export default {
  beforeMount() {
    // In browser
    this.fetchMyData()
  },
  async serverPrefetch() {
    // During SSR
    await this.fetchMyData()
  },
  methods: {
    fetchMyData() {
      const store = useStore()
      if (!store.myData) {
        return fetch('my/api/data').then(res => res.json()).then((myData) => {
          store.myData = myData
        })
      }
    },
  },
}

Initial state in React

There are a few ways to provide initial state in React:

  • Call your API and throw a promise in order to leverage React's Suspense (in both browser and server) anywhere in your components. Vite SSR is already adding Suspense to the root so you don't need to provide it.
function App({ initialState }) {
  if (!initialState.ready) {
    const promise = getPageProps(route).then((state) => {
      Object.assign(initialState, state)
      initialState.ready = true
    })

    // Throw the promise so Suspense can await it
    throw promise
  }

  return <div>{initialState}</div>
}
  • Calling your API before entering a route and populate route.meta.state. Vite SSR will get the first route's state and use it as the SSR initial state. See a full example here.
function App({ router }) {
  // This router is provided by Vite SSR.
  // Use it to render routes and save initial state.

  return (
    <Routes>
      {router.routes.map((route) => {
        if (!route.meta.state) {
          // Call custom API and return a promise
          const promise = getPageProps(route).then((state) => {
            // This is similar to modifying initialState in the previous example
            route.meta.state = state
          })

          // Throw the promise so Suspense can await it
          throw promise
        }

        return (
          <Route key={route.path} path={route.path} element={
            <route.component props={...route.meta.state} />
          } />
        )
      })}
    </Routes>
  )
}

State serialization

Vite SSR simply uses JSON.stringify to serialize the state, escapes certain characters to prevent XSS and saves it in the DOM. This behavior can be overriden by using the transformState hook in case you need to support dates, regexp or function serialization:

import viteSSR from 'vite-ssr'
import App from './app'
import routes from './routes'

export default viteSSR(App, {
  routes,
  transformState(state, defaultTransformer) {
    if (import.meta.env.SSR) {
      // Serialize during SSR by using,
      // for example, using @nuxt/devalue
      return customSerialize(state)

      // -- Or use the defaultTransformer after modifying the state:
      // state.apolloCache = state.apolloCache.extract()
      // return defaultTransformer(state)
    } else {
      // Deserialize in browser
      return customDeserialize(state)
    }
  },
})

Accessing response and request objects

In development, both response and request objects are passed to the main hook during SSR:

export default viteSSR(
  App,
  { routes },
  ({ initialState, request, response }) => {
    // Access request cookies, etc.
  }
)

In production, you control the server so you must pass these objects to the rendering function in order to have them available in the main hook:

import render from './dist/server'

//...

const { html } = await render(url, {
  manifest,
  preload: true,
  request,
  response,
  // Anything here will be available in the main hook.
  initialState: { hello: 'world' }, // Optional prefilled state
})

Beware that, in development, Vite uses plain Node.js + Connect for middleware. Therefore, the request and response objects might differ from your production environment if you use any server framework such as Fastify, Express.js or Polka. If you want to use your own server during development, check Middleware Mode.

Editing Response and redirects

It's possible to set status and headers to the response with writeResponse utility. For redirects, the redirect utility works both in SSR (server redirect) and browser (history push):

import { useContext } from 'vite-ssr'

// In a component
function () {
  const { redirect, writeResponse } = useContext()

  if (/* ... */) {
    redirect('/another-page', 302)
  }

  if (import.meta.env.SSR && /* ... */) {
    writeResponse({
      status: 404,
      headers: {}
    })
  }

  // ...
}

In the browser, this will just behave as a normal Router push.

Head tags and global attributes

Use your framework's utilities to handle head tags and attributes for html and body elements.

Vue Head

Install @vueuse/head as follows:

import { createHead } from '@vueuse/head'

export default viteSSR(App, { routes }, ({ app }) => {
  const head = createHead()
  app.use(head)

  return { head }
})

// In your components:
// import { useHead } from '@vueuse/head'
// ... useHead({ ... })

React Helmet

Use react-helmet-async from your components (similar usage to react-helmet). The provider is already added by Vite SSR.

import { Helmet } from 'react-helmet-async'

// ...
;<>
  <Helmet>
    <html lang="en" />
    <meta charSet="utf-8" />
    <title>Home</title>
    <link rel="canonical" href="http://mysite.com/example" />
  </Helmet>
</>

Rendering only in client/browser

Vite SSR exports ClientOnly component that renders its children only in the browser:

import { ClientOnly } from 'vite-ssr'

//...
;<div>
  <ClientOnly>
    <div>...</div>
  </ClientOnly>
</div>

Development

There are two ways to run the app locally for development:

  • SPA mode: vite dev command runs Vite directly without any SSR.
  • SSR mode: vite-ssr dev command spins up a local SSR server. It supports similar attributes to Vite CLI, e.g. vite-ssr --port 1337 --open.

SPA mode will be slightly faster but the SSR one will have closer behavior to a production environment.

Middleware Mode

If you want to run your own dev server (e.g. Express.js) instead of Vite's default Node + Connect, you can use Vite SSR in middleware mode:

const express = require('express')
const { createSsrServer } = require('vite-ssr/dev')

async function createServer() {
  const app = express()

  // Create vite-ssr server in middleware mode.
  const viteServer = await createSsrServer({
    server: { middlewareMode: 'ssr' },
  })

  // Use vite's connect instance as middleware
  app.use(viteServer.middlewares)

  app.listen(3000)
}

createServer()

Production

Run vite-ssr build for buildling your app. This will create 2 builds (client and server) that you can import and use from your Node backend. See an Express.js example server here, or a serverless function deployed to Vercel here.

Keeping index.html in the client build

In an SSR app, index.html is already embedded in the server build, and is thus removed from the client build in order to prevent serving it by mistake. However, if you would like to keep index.html in the client build (e.g. when using server side routing to selectively use SSR for a subset of routes), you can set build.keepIndexHtml to true in the plugin options:

// vite.config.js

export default {
  plugins: [
    viteSSR({
      build: {
        keepIndexHtml: true,
      },
    }),
    [...]
  ],
}

Integrations

Common integrations will be added here:

React CSS in JS

Use the styleCollector option to specify an SSR style collector. vite-ssr exports 3 common CSS-in-JS integrations: styled-components, material-ui-core-v4 and emotion:

import viteSSR from 'vite-ssr/react'
import styleCollector from 'vite-ssr/react/style-collectors/emotion'

export default viteSSR(App, { routes, styleCollector })

You can provide your own by looking at the implementation of any of the existing collectors.

Note that you still need to install all the required dependencies from these packages (e.g. @emotion/server, @emotion/react and @emotion/cache when using Emotion).

Custom Typings

You can define your own typings with vite-ssr. To declare custom types, the file mostly needs to import or export something not to break other types. Example transforming request and response to types of express:

import { Request, Response } from 'express'

declare module 'vite-ssr/vue' {
  export interface Context {
    request: Request
    response: Response
  }
}

Community contributions

Feel free to submit your projects:

Templates

  • Vue 3, Vercel, Axios. Link.

Addons

  • vite-ssr-middleware: Add route middlewares for vite-ssr and Vue, similar to Nuxt. Link.

Examples

  • Imitating Nuxt's asyncData in Vue options API. Link.
  • Fetch data from Vue components with composition API hook and Axios. Link.
  • Vue + TypeScript with API calls. Link.
  • Vue + TypeScript using serverPrefetch. Link.

References

The following projects served as learning material to develop this tool:

Todos

  • TypeScript
  • Make src/main.js file name configurable
  • Support build options as CLI flags (--ssr entry-file supported)
  • Support React
  • SSR dev-server
  • Make SSR dev-server similar to Vite's dev-server (options, terminal output)
  • Research if vite-ssr CLI logic can be moved to the plugin in Vite 2 to use vite command instead.
  • Docs

More Repositories

1

vitedge

Edge-side rendering and fullstack Vite framework
JavaScript
728
star
2

vitesse-ssr-template

πŸ• Opinionated Vue + Vite Starter Template with SSR in Node.js
TypeScript
187
star
3

vitessedge-template

πŸ• Opinionated Vite Starter Template with SSR in Cloudflare Workers
TypeScript
153
star
4

vue-graphql-enterprise-boilerplate

A GraphQL ready, very opinionated Vue SPA template for Vue CLI 3
JavaScript
120
star
5

OnsenUI-Todo-App

Onsen UI 2.0 To-Do sample application implemented in Vanilla JavaScript.
JavaScript
100
star
6

vue-tiny-validator

Tiny form validation tool for Vue 3
TypeScript
51
star
7

vue-geo-suggest

Renderless Vue component for finding addresses using Google Places API
JavaScript
42
star
8

medium-json-feed

Get Medium latest articles in JSON format
JavaScript
38
star
9

OnsenUI-YouTube

Example jukebox app made with Onsen UI, AngularJS and Youtube API
JavaScript
37
star
10

reactesse-ssr-template

πŸ• Opinionated React + Vite Starter Template with SSR in Node.js
TypeScript
33
star
11

vue-use-stripe

Thin Vue 3 wrapper for Stripe.js
TypeScript
33
star
12

reactesse-edge-template

TypeScript
28
star
13

onsenui-vue-router

Example that integrates vue-router and vue-onsenui
JavaScript
26
star
14

vue-onsenui-kitchensink

Vue.js + Onsen UI Kitchen Sink Example
Vue
20
star
15

OnsenUI-router

OnsenUI example app with ui-router
HTML
13
star
16

gulp-vue-compiler

Vue single file component compiler plugin for Gulp
JavaScript
13
star
17

OnsenUI-Material-Tabbar

Onsen UI 2.0 example with Material Design Tabbar
JavaScript
12
star
18

OnsenUI-Memo-App

Memo example application using OnsenUI and developing in Monaca
JavaScript
12
star
19

cf-workers-boilerplate

Deploy Cloudflare Workers easily without sacrifying developer experience
TypeScript
12
star
20

OnsenUI-Meteor-ToDo

Combining Onsen UI, React and Meteor
CSS
10
star
21

hydrogen-vercel-edge

Deploy a Hydrogen storefront to Vercel Edge Functions
JavaScript
9
star
22

graphql-info-transformer

JavaScript
6
star
23

multienv-loader

Dotenv loader for multiple environments
JavaScript
5
star
24

Nipponline

TeX
5
star
25

OnsenUI-LazyRepeat

Example of ons-lazy-repeat feature
JavaScript
4
star
26

karma-chai-spies

Karma plugin adapter for Chai-spies
JavaScript
4
star
27

normalize-unicode-text

Normalize strings with diacritics, zero-width whitespaces and other non-latin characters
JavaScript
4
star
28

dotfiles

Linux user dotfiles
Vim Script
3
star
29

hydrogen-vercel-serverless

Deploy a Hydrogen storefront to Vercel Serverless Functions
JavaScript
2
star
30

Cryptography-for-Network-Security

An introduction to cryptography
2
star
31

CDUC

Crohn's disease and Ulcerative Colitis classifier
1
star
32

vuetify-graphcool-poc

Vuetify + GraphCool
JavaScript
1
star
33

vite-ssr-vue-query

Created with CodeSandbox
TypeScript
1
star
34

postcss-url-resolver

PostCSS plugin that resolves urls (CSS imports and images) via http requests. Isomorphic (node + browser).
JavaScript
1
star