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

More info

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.

Last Updated: 11/8/2018, 5:38:11 PM