How To Create A Universal Vue App With GraphQL
I’ve been creating Universal Apps with Vue lately. These apps will render HTML on both the client- and server side. I wondered how to combine this with GraphQL.
Why GraphQL?
GraphQL, built by Facebook, is a query language for your APIs. It improves the process of creating an app by removing a lot of confusion between front- and back-end developers.
It does that by enabling developers to create an API, write documentation, and create a test client at the same time. GraphQL also has a built in mock system so you can start building interfaces on top of it in no-time.
REST will dictate how your client should communicate and needs a lot of round trips to the server to get all the desired data. GraphQL lets the client decide what data it needs to fetch, which is extremely useful when building multiple applications on the same API.
In the following example you can see that GraphQL is also a bit more efficient than REST. It’s able to fetch multiple resources and associations in a single request where REST will need to do a lot of chained requests.
This URL will fetch a list of users and their friends plus a list of posts and their authors:
https://example.com/graphql?query={users{firstName,friends{firstName}},posts{title,author{firstName}}}In REST we would have to do multiple requests:
get list of users -> https://example.com/api/users
for each user -> https://example.com/api/users/{id}/friends
get a list of posts -> https://example.com/api/posts
for each post -> https://example.com/api/posts/{id}/author
Firing XHR requests is relatively slow, especially on mobile devices. Bundling them in one requests saves a lot of latency and will be experienced as faster by your users.
Why Universal Apps?
The first and main reason is performance. Universal Apps render their HTML on the server and client side, this is called Server Side Rendering (SSR for short). When a client does a request on your app, it will immediately have HTML content available without waiting for the JavaScript to load.
The second reason is SEO, since the transition to AJAX apps, this became an awkward topic, there have been solutions like PhantomJS and Prerender.io, but these can’t compete with a Universal App.
Creating Universal Apps With Vue
At first, there was little documentation about this topic, but Evan You built a Hackernews clone to demonstrate how it works. I’ve used a lot of that example in mine. It’s still really good to go over it when you’re new to this concept. Lately, there’s also a lot of documentation located at https://ssr.vuejs.org/en.
Cloning and installing my demo
Note: This part assumes that you have a good understanding of the covered technologies. I will write more in depth tutorials in the future.
My code demonstrates a Universal app fetching data from a GraphQL server. The example app is really basic and only shows a simple message, but it can easily be extended with more data. I‘ll explain what’s going on in the sections bellow. You can install and run my demo by running the following commands.
git clone https://github.com/jonaskuiler/universal-vue-app-with-graphql
yarn # or npm install
yarn dev # or npm run dev# The front-end will be served at http://localhost:8080
# GraphQL will be served at http://localhost:47274
When you inspect the page source (CMD+OPTION+U on a Mac) and scroll to the contens of the <body>
element of the page you can see that it’s server side rendered.
You can also see that it embeds a script in the page source which is exposing window.__APOLLO_STATE__
. This is to make sure that our client-side JavaScript is also aware of the state the server has created for it, this is called state hydration.
Configuring Vue Apollo
To make Vue work with Apollo I use a Vue Plugin by Akryum. As I mentioned earlier, this is a wrapper for Apollo Client which, in a nutshell, enables us to do GraphQL queries from Vue components.
You basically create an instance of it just like you would do with Vue Router and add it to your root component’s instance.
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import App from './App.vue'
import { createRouter } from './router'
import { createApolloProvider } from './apollo'
export function createApp () {
// create router
const router = createRouter()
// create apollo provider
const apolloProvider = createApolloProvider()
// create app instance
const app = new Vue({
router,
apolloProvider,
render: h => h(App)
})
return { app, apolloProvider, router }
}
It uses a createApolloProvider
function which holds all the Apollo specific configuration and returns an instance of VueApollo.
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { ApolloClient } from 'apollo-client'
import { createApolloFetch } from 'apollo-fetch'
import { print } from 'graphql/language/printer'
Vue.use(VueApollo)
const isBrowser = (typeof window !== 'undefined')
// create apollo config since there's a difference
// between the one on the server and client
const createApolloConfig = () => {
const uri = 'http://localhost:47274/'
const apolloFetch = createApolloFetch({ uri })
const networkInterface = {
// use apollo-fetch as network interface
// it uses isomorphic-fetch to do requests
query: req => {
return apolloFetch({
...req,
query: print(req.query)
})
}
}
// Only use ssrMode in server context
let apolloConfig = {
ssrMode: true,
networkInterface
}
// hydrate the application in browser context
// this is to make sure that the state of the client
// is in sync with the state generated on the server
if (isBrowser) {
const state = window.__APOLLO_STATE__
if (state) {
apolloConfig = {
...apolloConfig,
ssrMode: false,
initialState: state.defaultClient
}
}
}
return apolloConfig
}
// return a new VueApollo instance
export function createApolloProvider () {
const apolloConfig = createApolloConfig()
const defaultClient = new ApolloClient(apolloConfig)
return new VueApollo({ defaultClient })
}
Currently, apollo-client can not do requests from a server because it’s using fetch, which is not supported in a Node environment. You could, of course, hack this behavior by adding polyfills or something similar.
To fix it, I use apollo-fetch which uses isomorphic-fetch to fire requests to a specific URL.
Querying the GraphQL server
In my example, I only have one view which does a simple GraphQL query to the server. When that content is fetched it will show a message to the client.
<template>
<div class="view">
<h1>{{ message }}</h1>
</div>
</template>
<style scoped>
.view {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
font-family: 'Roboto', sans-serif;
}
</style>
<script>
import gql from 'graphql-tag'
export default {
name: 'welcome',
apollo: {
message: {
query: gql`
{
message
}
`,
// note that we're using "prefetch" here
// it will let apolloProvider know that
// we also want to fetch this content server side
prefetch: true
}
},
data () {
return {
// default value for the message
message: null
}
}
}
</script>
The entry points of our app
To write universal code (for the server and the client) we need to create two entry points of our app. The client entry will be responsible for mounting the app to the DOM.
The server entry will be responsible for pre-fetching data. So, this is also the file where we will prefetch all the data fetched from our GraphQL server.
// The function that will eventually be called by the 'bundleRenderer'
// Since it will prefetch async data it returns a promise
export default (context) => {
return new Promise((resolve, reject) => {
// Get the app, apolloProvider and router from createApp()
const { app, apolloProvider, router } = createApp()
const now = Date.now()
const { url } = context
const fullPath = router.resolve(url).route.fullPath
if (fullPath !== url) {
reject({ url: fullPath })
}
// set router's location
router.push(url)
// wait until router has resolved possible async hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// no matched routes
if (!matchedComponents.length) {
reject({ code: 404 })
}
// prefetch all apollo queries
Promise.all([
apolloProvider.prefetchAll({
route: router.currentRoute,
}, matchedComponents)
]).then(() => {
// add function to context to embed apollo state to the DOM
context.renderApolloState = () => `
<script>${apolloProvider.exportStates()}</script>
`
resolve(app)
}).catch(reject)
}, reject)
})
}
Note that we’re adding a renderApolloState
function to context
here, this function will be available by the HTML template you’re using to render this application. It will add a JavaScript object to the DOM so the state of the application can be hydrated.
We’re adding state, scripts, styles and resource hints manually so we can control the order in which it’s rendered.
You can enable this behavior by providing an inject: false
option to your bundle renderer (see https://ssr.vuejs.org/en/api.html#inject).
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<link href="https://fonts.googleapis.com/css?family=Roboto:500" rel="stylesheet">
<meta charset="utf-8">
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
</head>
<body>
<!--vue-ssr-outlet-->
{{{ renderApolloState() }}}
{{{ renderState() }}}
{{{ renderScripts() }}}
</body>
</html>
Thanks for reading!
I hope you’ve enjoyed this article and in case you’ve missed it, all the code is hosted here https://github.com/jonaskuiler/universal-vue-app-with-graphql. If you have any questions or feedback, let me know.