Skip to content

Commit 31d56cc

Browse files
committed
data fetching
1 parent f7ff7fa commit 31d56cc

File tree

5 files changed

+268
-11
lines changed

5 files changed

+268
-11
lines changed

en/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
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.
3+
> **Note:** this guide requires the following minimum versions of Vue and supporting libraries:
4+
> - vue & vue-server-renderer >= 2.3.0
5+
> - vue-router >= 2.5.0
6+
> - vue-loader >= 12.0.0 & vue-style-loader >= 3.0.0
47
58
## What is Server-Side Rendering (SSR)?
69

@@ -32,8 +35,8 @@ Before using SSR for your app, the first question you should ask it whether you
3235

3336
This guide is focused on server-rendered Single-Page Applications using Node.js as the server. Mixing Vue SSR with other backend setups is a topic of its own and is not covered in this guide.
3437

35-
This guide assumes you are already familiar with Vue.js itself, and have working knowledge of Node.js and webpack. We acknowledge that it could be quite challenging to build a server-rendered Vue app if you lack prior experience. If you prefer a higher-level solution that provides a smoother on-boarding experience, you should probably give [Nuxt.js](http://nuxtjs.org/) a try. It's built upon the same Vue stack but abstracts away a lot of the complexities, and provides some extra features such as static site generation. However, it may not suit your use case if you need more direct control of your app's structure. Regardless, it would still be beneficial to read through this guide to better understand how things work together.
38+
This guide will be very in-depth and assumes you are already familiar with Vue.js itself, and have decent working knowledge of Node.js and webpack. If you prefer a higher-level solution that provides a smooth out-of-the-box experience, you should probably give [Nuxt.js](http://nuxtjs.org/) a try. It's built upon the same Vue stack but abstracts away a lot of the boilerplate, and provides some extra features such as static site generation. However, it may not suit your use case if you need more direct control of your app's structure. Regardless, it would still be beneficial to read through this guide to better understand how things work together.
3639

3740
As you read along, it would be helpful to refer to the official [HackerNews Demo](https://github.com/vuejs/vue-hackernews-2.0/), which makes use of most of the techniques covered in this guide.
3841

39-
Finally, note that the solutions in this guide are not definitive - we've found them to be working well for us, but that doesn't mean they cannot be improved. Feel free to contribute ideas on how to solve them more elegantly!
42+
Finally, note that the solutions in this guide are not definitive - we've found them to be working well for us, but that doesn't mean they cannot be improved. They might get revised in the future - and feel free to contribute by submitting pull requests!

en/build-config.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# Build Configuration
22

3-
> The following build config is meant for those who are interested in assembling an SSR app from scratch - we are going to provide official vue-cli templates that pre-configures everything for you in the near future.
4-
53
We will assume you already know how to configure webpack for a client-only project. The config for an SSR project will be largely similar, but we suggest breaking the config into three files: *base*, *client* and *server*. The base config contains config shared for both environments, such as output path, aliases, and loaders. The server config and client config can simply extend the base config using [webpack-merge](https://github.com/survivejs/webpack-merge).
64

75
## Server Config

en/data.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
# Data Pre-Fetching and State
2+
3+
## Data Store
4+
5+
During SSR, we are essentially rendering a "snapshot" of our app, so if the app relies on some asynchronous data, **these data need to be pre-fetched and resolved before we start the rendering process**.
6+
7+
Another concern is that on the client, the same data needs to be available before we mount the client side app - otherwise the client app would render using different state and the hydration would fail.
8+
9+
To address this, the fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, we can pre-fetch and fill data into the store before rendering. In addition, we will serialize and inline the state in the HTML. The client-side store can directly pick up the inlined state before we mount the app.
10+
11+
We will be using the official state management library [Vuex](https://github.com/vuejs/vuex/) for this purpose. Let's create a `store.js` file, with some mocked logic for fetching an item based on an id:
12+
13+
``` js
14+
// store.js
15+
import Vue from 'vue'
16+
import Vuex from 'vuex'
17+
18+
Vue.use(Vuex)
19+
20+
// Assume we have a universal API that returns Promises
21+
// and ignore the implementation details
22+
import { fetchItem } from './api'
23+
24+
export function createStore () {
25+
return new Vuex.Store({
26+
state: {
27+
items: {}
28+
},
29+
actions: {
30+
fetchItem ({ commit }, id) {
31+
// return the Promise via store.dispatch() so that we know
32+
// when the data has been fetched
33+
return fetchItem(id).then(item => {
34+
commit('setItem', { id, item })
35+
})
36+
}
37+
},
38+
mutations: {
39+
setItem (state, { id, item }) {
40+
Vue.set(state.items, id, item)
41+
}
42+
}
43+
})
44+
}
45+
```
46+
47+
And update `app.js`:
48+
49+
``` js
50+
// app.js
51+
import Vue from 'vue'
52+
import App from './App.vue'
53+
import { createRouter } from './router'
54+
import { createStore } from './store'
55+
import { sync } from 'vuex-router-sync'
56+
57+
export function createApp () {
58+
// create router and store instances
59+
const router = createRouter()
60+
const store = createStore()
61+
62+
// sync so that route state is available as part of the store
63+
sync(store, router)
64+
65+
// create the app instance, injecting both the router and the store
66+
const app = new Vue({
67+
router,
68+
store,
69+
render: h => h(App)
70+
})
71+
72+
// expose the app, the router and the store.
73+
return { app, router, store }
74+
}
75+
```
76+
77+
## Logic Collocation with Components
78+
79+
So, where do we place the code that dispatches the data-fetching actions?
80+
81+
The data we need to fetch is determined by the route visited - which also determines what components are rendered. In fact, the data needed for a given route is also the data needed by the components rendered at that route. So it would be natural to place the data fetching logic inside route components.
82+
83+
We will expose a custom static function `asyncData` on our route components. Note because this function will be called before the components are instantiated, it doesn't have access to `this`. The store and route information needs to be passed in as arguments:
84+
85+
``` html
86+
<!-- Item.vue -->
87+
<template>
88+
<div>{{ item.title }}</div>
89+
</template>
90+
91+
<script>
92+
export default {
93+
asyncData ({ store, route }) {
94+
// return the Promise from the action
95+
return store.dispatch('fetchItem', route.params.id)
96+
},
97+
98+
computed: {
99+
// display the item from store state.
100+
items () {
101+
return this.$store.state.items[this.$route.params.id]
102+
}
103+
}
104+
}
105+
</script>
106+
```
107+
108+
## Server Data Fetching
109+
110+
In `entry-server.js` we can get the components matched by a route with `router.getMatchedComponents()`, and call `asyncData` if the component exposes it. Then we need to attach resolved state to the render context.
111+
112+
``` js
113+
// entry-server.js
114+
import { createApp } from './app'
115+
116+
export default context => {
117+
return new Promise((resolve, reject) => {
118+
const { app, router, store } = createApp()
119+
120+
router.push(context.url)
121+
122+
router.onReady(() => {
123+
const matchedComponents = router.getMatchedComponents()
124+
if (!matchedComponents.length) {
125+
reject({ code: 404 })
126+
}
127+
128+
// call asyncData() on all matched route components
129+
Promise.all(matchedComponents.map(Component => {
130+
if (Component.asyncData) {
131+
return Component.asyncData({
132+
store,
133+
route: router.currentRoute
134+
})
135+
}
136+
})).then(() => {
137+
// After all preFetch hooks are resolved, our store is now
138+
// filled with the state needed to render the app.
139+
// When we attach the state to the context, and the `template` option
140+
// is used for the renderer, the state will automatically be
141+
// serialized and injected into the HTML as window.__INITIAL_STATE__.
142+
context.state = store.state
143+
144+
resolve(app)
145+
}).catch(reject)
146+
}, reject)
147+
})
148+
}
149+
```
150+
151+
## Client Data Fetching
152+
153+
On the client, there are two different approaches for handling data fetching:
154+
155+
1. **Resolve data before route navigation:**
156+
157+
With this strategy, the app will stay on the current view until the data needed by the incoming view has been resolved. The benefit is that the incoming view can directly render the full content when it's ready, but if the data fetching takes a long time, the user will feel "stuck" on the current view. It is therefore recommended to provide a data loading indicator if using this strategy.
158+
159+
We can implement this strategy on the client by checking matched components and invoking their `asyncData` function inside a global route hook. Note we should register this hook after the initial route is ready so that we don't unnecessarily fetch the server-fetched data again.
160+
161+
``` js
162+
// entry-client.js
163+
164+
// ...omitting unrelated code
165+
166+
router.onReady(() => {
167+
// Add router hook for handling asyncData.
168+
// Doing it after initial route is resolved so that we don't double-fetch
169+
// the data that we already have. Using router.beforeResolve() so that all
170+
// async components are resolved.
171+
router.beforeResolve((to, from, next) => {
172+
const matched = router.getMatchedComponents(to)
173+
const prevMatched = router.getMatchedComponents(from)
174+
175+
// we only care about none-previously-rendered components,
176+
// so we compare them until the two matched lists differ
177+
let diffed = false
178+
const activated = matched.filter((c, i) => {
179+
return diffed || (diffed = (prevMatched[i] !== c))
180+
})
181+
182+
if (!activated.length) {
183+
return next()
184+
}
185+
186+
// this is where we should trigger a loading indicator if there is one
187+
188+
Promise.all(activated.map(c => {
189+
if (c.asyncData) {
190+
return c.asyncData({ store, route: to })
191+
}
192+
})).then(() => {
193+
194+
// stop loading indicator
195+
196+
next()
197+
}).catch(next)
198+
})
199+
200+
app.$mount('#app')
201+
})
202+
```
203+
204+
2. **Fetch data after the matched view is rendered:**
205+
206+
This strategy places the client-side data-fetching logic in a view component's `beforeMount` function. This allows the views to switch instantly when a route navigation is triggered, so the app feels a bit more responsive. However, the incoming view will not have the full data available when it's rendered. It is therefore necessary to have a conditional loading state for each view component that uses this strategy.
207+
208+
This can be achieved with a client-only global mixin:
209+
210+
``` js
211+
Vue.mixin({
212+
beforeMount () {
213+
const { asyncData } = this.$options
214+
if (asyncData) {
215+
// assign the fetch operation to a promise
216+
// so that in components we can do `this.dataPromise.then(...)` to
217+
// perform other tasks after data is ready
218+
this.dataPromise = asyncData({
219+
store: this.$store,
220+
route: this.$route
221+
})
222+
}
223+
}
224+
})
225+
```
226+
227+
The two strategies are ultimately different UX decisions and should be picked based on the actual scenario of the app you are building. But regardless of which strategy you pick, the `asyncData` function should also be called when a route component is reused (same route, but params or query changed. e.g. from `user/1` to `user/2`). We can also handle this with a client-only global mixin:
228+
229+
``` js
230+
Vue.mixin({
231+
beforeRouteUpdate (to, from, next) {
232+
const { asyncData } = this.$options
233+
if (asyncData) {
234+
asyncData({
235+
store: this.$store,
236+
route: to
237+
}).then(next).catch(next)
238+
} else {
239+
next()
240+
}
241+
}
242+
})
243+
```
244+
245+
---
246+
247+
Phew, that was a lot of code! This is because universal data-fetching is probably the most complex problem in a server-rendered app and we are laying the groundwork for easier further development. Once the boilerplate is set up, authoring individual components will be actually quite pleasant.

en/routing.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ export function createApp () {
3838
const app new Vue({
3939
// inject router into root Vue instance
4040
router,
41-
// use spread operator to mix in the App component
42-
...App
41+
render: h => h(App)
4342
})
4443

4544
// return both the app and the router
@@ -147,8 +146,7 @@ export function createRouter () {
147146
mode: 'history',
148147
routes: [
149148
{ path: '/', component: () => import('./components/Home.vue') },
150-
{ path: '/foo', component: () => import('./components/Foo.vue') },
151-
{ path: '/bar', component: () => import('./components/Bar.vue') }
149+
{ path: '/item/:id', component: () => import('./components/Item.vue') }
152150
]
153151
})
154152
}

en/structure.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Source Code Structure
22

3+
## Avoid Stateful Singletons
4+
5+
When writing client-only code, we are used to the fact that our code will be evaluated in a fresh context every time. However, a Node.js server is a long-running process. When our code is required into the process, it will be evaluated once and stays in memory. This means if you create a singleton object, it will be shared between every incoming request.
6+
37
As seen in the basic example, we are **creating a new root Vue instance for each request.** This is similar to how each user will be using a fresh instance of the app in their own browser. If we use a shared instance across multiple requests, it will easily lead to cross-request state pollution.
48

5-
Ideally, we want our Vue app code to be more decoupled from the server itself. The first step could be splitting our app code into a separate file:
9+
So, instead of directly creating an app instance, we should expose a factory function that can be repeatedly executed to create fresh app instances for each request:
610

711
``` js
812
// app.js
@@ -35,6 +39,10 @@ server.get('*', (req, res) => {
3539
})
3640
```
3741

42+
The same rule applies to router, store and event bus instances as well. Instead of exporting it directly from a module and importing it across your app, you need to create a fresh instance in `createApp` and inject it from the root Vue instance.
43+
44+
> This constraint can be eliminated when using the bundle renderer with `{ runInNewContext: true }`, however it comes with some significant performance cost because a new vm context needs to be created for each request.
45+
3846
## Introducing a Build Step
3947

4048
So far, we haven't discussed how to deliver the same Vue app to the client yet. To do that, we need to use webpack to bundle our Vue app. In fact, we probably want to use webpack to bundle the Vue app on the server as well, because:
@@ -76,7 +84,10 @@ import App from './App.vue'
7684
// export a factory function for creating fresh app, router and store
7785
// instances
7886
export function createApp () {
79-
const app = new Vue(App)
87+
const app = new Vue({
88+
// the root instance simply renders the App component.
89+
render: h => h(App)
90+
})
8091
return { app }
8192
}
8293
```

0 commit comments

Comments
 (0)