Skip to content

Commit f7ff7fa

Browse files
committed
routing
1 parent 1d2cdac commit f7ff7fa

File tree

6 files changed

+206
-44
lines changed

6 files changed

+206
-44
lines changed

en/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Vue.js Server-Side Rendering Guide
22

3+
> **Note:** this guide is written based on the latest versions of `vue`, `vue-server-renderer` (2.3.0+) and `vue-loader` (12.0.0+). It also has some recommendations that are different from 2.2 usage.
4+
35
## What is Server-Side Rendering (SSR)?
46

57
Vue.js is a framework for building client-side applications. By default, Vue components produce and manipulate DOM in the browser as output. However, it is also possible to render the same components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client.

en/SUMMARY.md

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
- [Source Code Structure](structure.md)
44
- [Routing and Code-Splitting](routing.md)
55
- [Data Pre-fetching and State](data.md)
6-
- [Using a Page Template](template.md)
76
- [Introducing Bundle Renderer](bundle-renderer.md)
87
- [Build Configuration](build-config.md)
98
- [CSS Management](css.md)

en/basic.md

+38-1
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,41 @@ server.get('*', (req, res) => {
7373
server.listen(8080)
7474
```
7575

76-
This is the most basic API to render a Vue app on the server. However, this is far from sufficient for a real world server-rendered app. In the following chapters we will cover the common issues encountered and how to deal with them.
76+
## Using a Page Template
77+
78+
When you render a Vue app, the renderer only generates the markup of the app. In the example we had to wrap the output with an extra HTML page shell.
79+
80+
To simplify this, you can directly provide a page template when creating the renderer. Most of the time we will put the page template in its own file, e.g. `index.template.html`:
81+
82+
``` html
83+
<!DOCTYPE html>
84+
<html lang="en">
85+
<head><title>Hello</title></head>
86+
<body>
87+
<!--vue-ssr-outlet-->
88+
</body>
89+
</html>
90+
```
91+
92+
Notice the `<!--vue-ssr-outlet-->` comment -- this is where your app's markup will be injected.
93+
94+
We can then read and pass the file to the Vue renderer:
95+
96+
``` js
97+
const renderer = createRenderer({
98+
template: require('fs').readFileSync('./index.template.html', 'utf-8')
99+
})
100+
101+
renderer.renderToString(app, (err, html) => {
102+
console.log(html) // will be the full page with app content injected.
103+
})
104+
```
105+
106+
The template also supports many advanced features like:
107+
108+
- Interpolation using a render context;
109+
- Auto injection of critical CSS when using `*.vue` components;
110+
- Auto injection of asset links and resource hints when using `clientManifest`;
111+
- Auto injection and XSS prevention when embedding Vuex state for client-side hydration.
112+
113+
We will discuss these when we introduce the associated concepts later in the guide.

en/routing.md

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Routing and Code-Splitting
2+
3+
## Routing with `vue-router`
4+
5+
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!
6+
7+
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:
8+
9+
``` js
10+
// router.js
11+
import Vue from 'vue'
12+
import Router from 'vue-router'
13+
14+
Vue.use(Router)
15+
16+
export function createRouter () {
17+
return new Router({
18+
mode: 'history',
19+
routes: [
20+
// ...
21+
]
22+
})
23+
}
24+
```
25+
26+
And update `app.js`:
27+
28+
``` js
29+
// app.js
30+
import Vue from 'vue'
31+
import App from './App.vue'
32+
import { createRouter } from './router'
33+
34+
export function createApp () {
35+
// create router instance
36+
const router = createRouter()
37+
38+
const app new Vue({
39+
// inject router into root Vue instance
40+
router,
41+
// use spread operator to mix in the App component
42+
...App
43+
})
44+
45+
// return both the app and the router
46+
return { app, router }
47+
}
48+
```
49+
50+
Now we need to implement the server-side routing logic in `entry-server.js`:
51+
52+
``` js
53+
// entry-server.js
54+
import { createApp } from './app'
55+
56+
export default context => {
57+
// since there could potentially be asynchronous route hooks or components,
58+
// we will be returning a Promise so that the server can wait until
59+
// everything is ready before rendering.
60+
return new Promise((resolve, reject) => {
61+
const { app, router } = createApp()
62+
63+
// set server-side router's location
64+
router.push(context.url)
65+
66+
// wait until router has resolved possible async components and hooks
67+
router.onReady(() => {
68+
const matchedComponents = router.getMatchedComponents()
69+
// no matched routes, reject with 404
70+
if (!matchedComponents.length) {
71+
reject({ code: 404 })
72+
}
73+
74+
// the Promise should resolve to the app instance so it can be rendered
75+
resolve(app)
76+
}, reject)
77+
})
78+
}
79+
```
80+
81+
Assuming the server bundle is already built (again, ignoring build setup for now), the server usage would look like this:
82+
83+
``` js
84+
// server.js
85+
const createApp = require('/path/to/built-server-bundle.js')
86+
87+
server.get('*', (req, res) => {
88+
const context = { url: req.url }
89+
90+
createApp(context).then(app => {
91+
renderer.renderToString(app, (err, html) => {
92+
if (err) {
93+
if (err.code === 404) {
94+
res.status(404).end('Page not found')
95+
} else {
96+
res.status(500).end('Internal Server Error')
97+
}
98+
} else {
99+
res.end(html)
100+
}
101+
})
102+
})
103+
})
104+
```
105+
106+
## Code-Splitting
107+
108+
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.
109+
110+
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](https://webpack.js.org/guides/code-splitting-async/), all you need to do is:
111+
112+
``` js
113+
// changing this...
114+
import Foo from './Foo.vue'
115+
116+
// to this:
117+
const Foo = () => import('./Foo.vue')
118+
```
119+
120+
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.
121+
122+
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:
123+
124+
``` js
125+
// entry-client.js
126+
127+
import { createApp } from './app'
128+
129+
const { app, router } = createApp()
130+
131+
router.onReady(() => {
132+
app.$mount('#app')
133+
})
134+
```
135+
136+
An example route config with async route components:
137+
138+
``` js
139+
// router.js
140+
import Vue from 'vue'
141+
import Router from 'vue-router'
142+
143+
Vue.use(Router)
144+
145+
export function createRouter () {
146+
return new Router({
147+
mode: 'history',
148+
routes: [
149+
{ path: '/', component: () => import('./components/Home.vue') },
150+
{ path: '/foo', component: () => import('./components/Foo.vue') },
151+
{ path: '/bar', component: () => import('./components/Bar.vue') }
152+
]
153+
})
154+
}
155+
```

en/structure.md

+11-4
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ import App from './App.vue'
7676
// export a factory function for creating fresh app, router and store
7777
// instances
7878
export function createApp () {
79-
return new Vue(App)
79+
const app = new Vue(App)
80+
return { app }
8081
}
8182
```
8283

@@ -89,13 +90,19 @@ import { createApp } from './app'
8990

9091
// client-specific bootstrapping logic...
9192

92-
createApp().$mount('#app')
93+
const { app } = createApp()
94+
app.$mount('#app')
9395
```
9496

9597
### `entry-server.js`:
9698

97-
At this moment, our server entry doesn't do anything other than exporting the same `createApp` function - but later when we will perform server-side route matching and data pre-fetching logic here.
99+
The server entry uses a default export which is a function that can be called repeatedly for each render. At this moment, it doesn't do much other than creating and returning the app instance - but later when we will perform server-side route matching and data pre-fetching logic here.
98100

99101
``` js
100-
export { createApp } from './app'
102+
import { createApp } from './app'
103+
104+
export default context => {
105+
const { app } = createApp()
106+
return app
107+
}
101108
```

en/template.md

-38
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,3 @@
1-
# Using a Page Template
2-
3-
When you render a Vue app, the renderer only generates the markup of the app. We also need to wrap the output with an extra HTML page shell.
4-
5-
You can directly provide a page template when creating the renderer. Most of the time we will put the page template in its own file, e.g. `index.template.html`:
6-
7-
``` html
8-
<!DOCTYPE html>
9-
<html lang="en">
10-
<head><title>Hello</title></head>
11-
<body>
12-
<!--vue-ssr-outlet-->
13-
</body>
14-
</html>
15-
```
16-
17-
Notice the `<!--vue-ssr-outlet-->` comment -- this is where your app's markup will be injected.
18-
19-
We can then read and pass the file to the Vue renderer:
20-
21-
``` js
22-
const fs = require('fs')
23-
const Vue = require('vue')
24-
const { createRenderer } = require('vue-server-renderer')
25-
26-
const renderer = createRenderer({
27-
template: fs.readFileSync('./index.template.html', 'utf-8')
28-
})
29-
30-
const app = new Vue({
31-
template: '<div>hello</div>'
32-
})
33-
34-
renderer.renderToString(app, (err, html) => {
35-
console.log(html) // will be the full page with app content injected.
36-
})
37-
```
38-
391
## Interpolation Using the Render Context
402

413
> requires version 2.3.0+

0 commit comments

Comments
 (0)