Server-Side Rendering
Vue CLI Plugin
I made a plugin for vue-cli so you can transform your vue-apollo app into an isomorphic SSR app in literary two minutes! â¨đ
In your vue-cli 3 project:
vue add @akryum/ssr
Prefetch components
On the queries you want to prefetch on the server, add the prefetch
option. It can either be:
- a variables object,
- a function that gets the context object (which can contain the URL for example) and return a variables object,
false
to disable prefetching for this query.
If you are returning a variables object in the prefetch
option, make sure it matches the result of the variables
option. If they do not match, the query's data property will not be populated while rendering the template server-side.
WARNING
You don't have access to the component instance when doing prefetching on the server.
Example:
export default {
apollo: {
allPosts: {
// This will be prefetched
query: gql`query AllPosts {
allPosts {
id
imageUrl
description
}
}`,
}
}
}
Example 2:
export default {
apollo: {
post: {
query: gql`query Post($id: ID!) {
post (id: $id) {
id
imageUrl
description
}
}`,
prefetch: ({ route }) => {
return {
id: route.params.id,
}
},
variables () {
return {
id: this.id,
}
},
}
}
}
Skip prefetching
Example that doesn't prefetch the query:
export default {
apollo: {
allPosts: {
query: gql`query AllPosts {
allPosts {
id
imageUrl
description
}
}`,
// Don't prefetch
prefetch: false,
}
}
}
If you want to skip prefetching all the queries for a specific component, use the $prefetch
option:
export default {
apollo: {
// Don't prefetch any query
$prefetch: false,
allPosts: {
query: gql`query AllPosts {
allPosts {
id
imageUrl
description
}
}`,
}
}
}
You can also put a no-prefetch
attribute on any component so it will be ignored while walking the tree to gather the Apollo queries:
<ApolloQuery no-prefetch>
On the server
In the server entry, you need to install ApolloSSR
plugin into Vue:
import Vue from 'vue'
import ApolloSSR from 'vue-apollo/ssr'
Vue.use(ApolloSSR)
To prefetch all the apollo queries you marked, use the ApolloSSR.prefetchAll
method. The first argument is the apolloProvider
. The second argument is the array of component definition to include (e.g. from router.getMatchedComponents
method). The third argument is the context object passed to the prefetch
hooks (see above). It is recommended to pass the vue-router currentRoute
object. It returns a promise resolved when all the apollo queries are loaded.
Here is an example with vue-router and a Vuex store:
import Vue from 'vue'
import ApolloSSR from 'vue-apollo/ssr'
import App from './App.vue'
Vue.use(ApolloSSR, {
// SSR config
fetchPolicy: 'network-only',
suppressRenderErrors: false,
})
export default () => new Promise((resolve, reject) => {
const { app, router, store, apolloProvider } = CreateApp({
ssr: true,
})
// set router's location
router.push(context.url)
// wait until router has resolved possible async hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// no matched routes
if (!matchedComponents.length) {
reject({ code: 404 })
}
let js = ''
// Call preFetch hooks on components matched by the route.
// A preFetch hook dispatches a store action and returns a Promise,
// which is resolved when the action is complete and store state has been
// updated.
// Vuex Store prefetch
Promise.all(matchedComponents.map(component => {
return component.asyncData && component.asyncData({
store,
route: router.currentRoute,
})
}))
// Apollo prefetch
// This will prefetch all the Apollo queries in the whole app
.then(() => ApolloSSR.prefetchAll(apolloProvider, [App, ...matchedComponents], {
store,
route: router.currentRoute,
}))
.then(() => {
// Inject the Vuex state and the Apollo cache on the page.
// This will prevent unnecessary queries.
// Vuex
js += `window.__INITIAL_STATE__=${JSON.stringify(store.state)};`
// Apollo
js += ApolloSSR.exportStates(apolloProvider)
resolve({
app,
js,
})
}).catch(reject)
})
})
Use the ApolloSSR.exportStates(apolloProvider, options)
method to get the JavaScript code you need to inject into the generated page to pass the apollo cache data to the client.
It takes an options
argument which defaults to:
{
// Global variable name
globalName: '__APOLLO_STATE__',
// Global object on which the variable is set
attachTo: 'window',
// Prefix for the keys of each apollo client state
exportNamespace: '',
}
You can also use the ApolloSSR.getStates(apolloProvider, options)
method to get the JS object instead of the script string.
It takes an options
argument which defaults to:
{
// Prefix for the keys of each apollo client state
exportNamespace: '',
}
Creating the Apollo Clients
It is recommended to create the apollo clients inside a function with an ssr
argument, which is true
on the server and false
on the client.
Here is an example:
// src/api/apollo.js
import Vue from 'vue'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'
// Install the vue plugin
Vue.use(VueApollo)
// Create the apollo client
export function createApolloClient (ssr = false) {
const httpLink = new HttpLink({
// You should use an absolute URL here
uri: ENDPOINT + '/graphql',
})
const cache = new InMemoryCache()
// If on the client, recover the injected state
if (!ssr) {
// If on the client, recover the injected state
if (typeof window !== 'undefined') {
const state = window.__APOLLO_STATE__
if (state) {
// If you have multiple clients, use `state.<client_id>`
cache.restore(state.defaultClient)
}
}
}
const apolloClient = new ApolloClient({
link: httpLink,
cache,
...(ssr ? {
// Set this on the server to optimize queries when SSR
ssrMode: true,
} : {
// This will temporary disable query force-fetching
ssrForceFetchDelay: 100,
}),
})
return apolloClient
}
Example for common CreateApp
method:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import { sync } from 'vuex-router-sync'
import VueApollo from 'vue-apollo'
import { createApolloClient } from './api/apollo'
import App from './ui/App.vue'
import routes from './routes'
import storeOptions from './store'
Vue.use(VueRouter)
Vue.use(Vuex)
function createApp (context) {
const router = new VueRouter({
mode: 'history',
routes,
})
const store = new Vuex.Store(storeOptions)
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)
// Apollo
const apolloClient = createApolloClient(context.ssr)
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
return {
app: new Vue({
el: '#app',
router,
store,
apolloProvider,
...App,
}),
router,
store,
apolloProvider,
}
}
export default createApp
On the client:
import CreateApp from './app'
CreateApp({
ssr: false,
})
On the server:
import CreateApp from './app'
export default () => new Promise((resolve, reject) => {
const { app, router, store, apolloProvider } = CreateApp({
ssr: true,
})
// set router's location
router.push(context.url)
// wait until router has resolved possible async hooks
router.onReady(() => {
// Prefetch, render HTML (see above)
})
})
See the SSR API for more details and other features.