Similarly to Nuxt,
we define a new page by creating a new .page.vue
file.
<!-- /pages/index.page.vue -->
<!-- Environment: Browser, Node.js -->
<template>
This page is rendered to HTML and interactive: <Counter />
</template>
<script>
import Counter from '../components/Counter.vue'
export default { components: { Counter } }
</script>
By default, vite-plugin-ssr
does Filesystem Routing.
FILESYSTEM URL
pages/index.page.vue /
pages/about.page.vue /about
We can also define a page's route with a Route String (for parameterized routes such as /movies/:id
) or a Route Function (for full programmatic flexibility).
// /pages/index.page.route.js
// Environment: Node.js (and Browser if we choose Client Routing)
// Note how the two files share the same base `/pages/index.page.`; this is how `vite-plugin-ssr`
// knows that `/pages/index.page.route.js` defines the route of `/pages/index.page.vue`.
// Route Function
export default pageContext => pageContext.url === '/'
// If we don't create a `.page.route.js` file then vite-plugin-ssr does Filesystem Routing
Unlike Nuxt, we define how our pages are rendered.
// _default.page.server.js
// Environment: Node.js
import { createSSRApp, h } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
export { render }
async function render(pageContext) {
const { Page, pageProps } = pageContext
const app = createSSRApp({
render: () => h(Page, pageProps)
})
const appHtml = await renderToString(app)
const title = 'Vite SSR'
return escapeInject`<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<div id="app">${dangerouslySkipEscape(appHtml)}</div>
</body>
</html>`
}
// _default.page.client.js
// Environment: Browser
import { createSSRApp, h } from 'vue'
import { getPage } from 'vite-plugin-ssr/client'
hydrate()
async function hydrate() {
const pageContext = await getPage() // (`pageContext` is preloaded in production)
const { Page, pageProps } = pageContext
const app = createSSRApp({
render: () => h(Page, pageProps)
})
app.mount('#app')
}
You may see
import { createSSRApp, h } from 'vue'
for the first time but every SSR app actually usescreateSSRApp
andh
.Nuxt abstracts this away from us and gives us a slightly faster getting started. But this also means that we lose control over a central piece of our app architecture and we'll eventually lose much more time circumventing Nuxt's limiting black box as we scale.
The boilerplate
$ npm init vite-plugin-ssr@latest
provides a functioning setup that will require only small adaptations for most users.
The render()
hook in _default.page.server.js
gives us full control over how our pages are rendered to HTML,
and _default.page.client.js
gives us full control over the browser-side code.
This control enables us to easily and naturally integrate any tool we want (Vuex, GraphQL, Service Worker, ...).
There are four suffixes:
.page.js
: exports the page's root Vue component..page.client.js
: defines the page's browser-side code..page.server.js
: exports page hooks (always run in Node.js)..page.route.js
: exports the page's Route String or Route Function.Instead of creating a .page.client.js
and .page.server.js
file for each page,
we can create _default.page.client.js
and _default.page.server.js
which apply as default for all pages.
The last two files we created are actually _default.page.client.js
and _default.page.server.js
,
which means that we can now create a new page just by defining a new .page.vue
file (the .page.route.js
file is optional).
The _default
files can be overridden. For example, we can create a page with a different browser-side code than our other pages.
// /pages/about.page.client.js
// This file is empty which means that the `/about` page has zero browser-side JavaScript.
<!-- /pages/about.page.vue -->
<template>
This page is only rendered to HTML.
</template>
By also overriding the render()
hook we can
even render a page with a completely different UI framework, e.g. React or another Vue version (for progressive Vue version upgrade).
Let's now have a look at how to fetch data.
<!-- /pages/star-wars/movie.page.vue -->
<!-- Environment: Browser, Node.js -->
<template>
<h1>{{movie.title}}</h1>
<p>Release Date: {{movie.release_date}}</p>
<p>Director: {{movie.director}}</p>
</template>
<script lang="js">
const pageProps = ['movie']
export default { props: pageProps }
</script>
// /pages/star-wars/movie.page.route.js
// Environment: Node.js
// Route String
export default '/star-wars/:movieId'
// /pages/star-wars/movie.page.server.js
// Environment: Node.js
import fetch from 'node-fetch'
export async function onBeforeRender(pageContext) {
// The route parameter of `/star-wars/:movieId` is available at `pageContext.routeParams`
const { movieId } = pageContext.routeParams
// `.page.server.js` files always run in Node.js; we could use SQL/ORM queries here.
const response = await fetch(`https://swapi.dev/api/films/${movieId}`)
let movie = await response.json()
// Our render and hydrate functions we defined earlier pass `pageContext.pageProps` to
// the root Vue component `Page`; this is where we define `pageProps`.
const pageProps = { movie }
// We make `pageProps` available as `pageContext.pageProps`
return {
pageContext: {
pageProps
}
}
}
// By default `pageContext.*` are available only on the server. But our hydrate function
// we defined earlier runs in the browser and needs `pageContext.pageProps`; we use
// `passToClient` to tell `vite-plugin-ssr` to serialize and make `pageContext.pageProps`
// available to the browser.
export const passToClient = ['pageProps']
That's it for the tour and we have actually already seen most of the interface;
vite-plugin-ssr
is not only flexible but also simple to use.
$ npm init vite-plugin-ssr
$ npm init vite-plugin-ssr
to scaffold a new Vite/vite-plugin-ssr
app, or add vite-plugin-ssr
to your existing app by following the instructions here.