Routing and Code-Splitting
Routing with vue-router
You may have noticed that our server code uses a *
handler which accepts arbitrary URLs. This allows us to pass the visited URL into our Vue app, and reuse the same routing config for both client and server!
It is recommended to use the official vue-router
for this purpose. Let's first create a file where we create the router. Note similar to createApp
, we also need a fresh router instance for each request, so the file exports a createRouter
function:
// router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
// ...
]
})
}
And update app.js
:
// app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
export function createApp () {
// create router instance
const router = createRouter()
const app new Vue({
// inject router into root Vue instance
router,
render: h => h(App)
})
// return both the app and the router
return { app, router }
}
Now we need to implement the server-side routing logic in entry-server.js
:
// entry-server.js
import { createApp } from './app'
export default context => {
// since there could potentially be asynchronous route hooks or components,
// we will be returning a Promise so that the server can wait until
// everything is ready before rendering.
return new Promise((resolve, reject) => {
const { app, router } = createApp()
// set server-side router's location
router.push(context.url)
// wait until router has resolved possible async components and hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// no matched routes, reject with 404
if (!matchedComponents.length) {
reject({ code: 404 })
}
// the Promise should resolve to the app instance so it can be rendered
resolve(app)
}, reject)
})
}
Assuming the server bundle is already built (again, ignoring build setup for now), the server usage would look like this:
// server.js
const createApp = require('/path/to/built-server-bundle.js')
server.get('*', (req, res) => {
const context = { url: req.url }
createApp(context).then(app => {
renderer.renderToString(app, (err, html) => {
if (err) {
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else {
res.end(html)
}
})
})
})
Code-Splitting
Code-splitting, or lazy-loading part of your app, helps reducing the amount of assets that need to be downloaded by the browser for the initial render, and can greatly improve TTI (time-to-interactive) for apps with large bundles. The key is "loading just what is needed" for the initial screen.
Vue provides async components as a first-class concept, combining it with webpack 2's support for using dynamic import as a code-split point, all you need to do is:
// changing this...
import Foo from './Foo.vue'
// to this:
const Foo = () => import('./Foo.vue')
This would work under any scenario if you are building a pure client-side Vue app. However, there are some limitations when using this in SSR. First, you need to resolve all the async components upfront on the server before starting the render, because otherwise you will just get an empty placeholder in the markup. On the client, you also need to do this before starting the hydration, otherwise the client will run into content mismatch errors.
This makes it a bit tricky to use async components at arbitrary locations in your app (we will likely improve this in the future). However, it works seamlessly if you do it at the route level - i.e. use async components in your route configuration - because vue-router
will automatically resolve matched async components when resolving a route. What you need to do is make sure to use router.onReady
on both server and client. We already did that in our server entry, and now we just need to update the client entry:
// entry-client.js
import { createApp } from './app'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
An example route config with async route components:
// router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: () => import('./components/Home.vue') },
{ path: '/item/:id', component: () => import('./components/Item.vue') }
]
})
}