Skip to content

Commit 0e75fb9

Browse files
committed
use component name for SSR caching (also allow full externalization)
1 parent 1cde06b commit 0e75fb9

File tree

4 files changed

+90
-44
lines changed

4 files changed

+90
-44
lines changed

packages/vue-server-renderer/README.md

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
55
This package offers Node.js server-side rendering for Vue 2.0.
66

7+
- [Installation](#installation)
8+
- [API](#api)
9+
- [Renderer Options](#renderer-options)
10+
- [Why Use `bundleRenderer`?](#why-use-bundlerenderer)
11+
- [Creating the Server Bundle](#creating-the-server-bundle)
12+
- [Component Caching](#component-caching)
13+
- [Client Side Hydration](#client-side-hydration)
14+
715
## Installation
816

917
``` bash
@@ -100,31 +108,6 @@ bundleRenderer
100108
.pipe(writableStream)
101109
```
102110

103-
## Creating the Server Bundle
104-
105-
The application bundle can be generated by any build tool, so you can easily use Webpack + `vue-loader` with the bundleRenderer. You do need to use a slightly different webpack config and entry point for your server-side bundle, but the difference is rather minimal:
106-
107-
1. add `target: 'node'`, and use `output: { libraryTarget: 'commonjs2' }` for your webpack config.
108-
109-
2. In your server-side entry point, export a function. The function will receive the render context object (passed to `bundleRenderer.renderToString` or `bundleRenderer.renderToStream`), and should return a Promise, which should eventually resolve to the app's root Vue instance:
110-
111-
``` js
112-
// server-entry.js
113-
import Vue from 'vue'
114-
import App from './App.vue'
115-
116-
const app = new Vue(App)
117-
118-
// the default export should be a function
119-
// which will receive the context of the render call
120-
export default context => {
121-
// data pre-fetching
122-
return app.fetchServerData(context.url).then(() => {
123-
return app
124-
})
125-
}
126-
```
127-
128111
## Renderer Options
129112

130113
### directives
@@ -147,9 +130,7 @@ As an example, check out [`v-show`'s server-side implementation](https://github.
147130

148131
### cache
149132

150-
> Note: this option has changed and is different from versions <= 2.0.0-alpha.8.
151-
152-
Provide a cache implementation. The cache object must implement the following interface:
133+
Provide a [component cache](#component-caching) implementation. The cache object must implement the following interface:
153134

154135
``` js
155136
{
@@ -189,12 +170,63 @@ const renderer = createRenderer({
189170
})
190171
```
191172

192-
## Component-Level Caching
173+
## Why Use `bundleRenderer`?
174+
175+
In a typical Node.js app, the server is a long-running process. If we directly require our application code, the instantiated modules will be shared across every request. This imposes some inconvenient restrictions to the application structure: we will have to avoid any use of global stateful singletons (e.g. the store), otherwise state mutations caused by one request will affect the result of the next.
176+
177+
Instead, it's more straightforward to run our app "fresh" for each request, so that we don't have to think about avoiding state contamination across requests. This is exactly what `bundleRenderer` helps us achieve.
178+
179+
## Creating the Server Bundle
180+
181+
The application bundle can be generated by any build tool, so you can easily use Webpack + `vue-loader` with the bundleRenderer. You do need to use a slightly different webpack config and entry point for your server-side bundle, but the difference is rather minimal:
182+
183+
1. add `target: 'node'`, and use `output: { libraryTarget: 'commonjs2' }` for your webpack config. Also, it's probably a good idea to [externalize your dependencies](#externals).
184+
185+
2. In your server-side entry point, export a function. The function will receive the render context object (passed to `bundleRenderer.renderToString` or `bundleRenderer.renderToStream`), and should return a Promise, which should eventually resolve to the app's root Vue instance:
186+
187+
``` js
188+
// server-entry.js
189+
import Vue from 'vue'
190+
import App from './App.vue'
191+
192+
const app = new Vue(App)
193+
194+
// the default export should be a function
195+
// which will receive the context of the render call
196+
export default context => {
197+
// data pre-fetching
198+
return app.fetchServerData(context.url).then(() => {
199+
return app
200+
})
201+
}
202+
```
203+
204+
### Externals
205+
206+
When using the `bundleRenderer`, we will by default bundle every dependency of our app into the server bundle as well. This means on each request these depdencies will need to be parsed and evaluated again, which is unnecessary in most cases.
207+
208+
We can optimize this by externalizing dependencies from your bundle. During the render, any raw `require()` calls found in the bundle will return the actual Node module from your rendering process. With Webpack, we can simply list the modules we want to externalize using the [`externals` config option](https://webpack.github.io/docs/configuration.html#externals):
209+
210+
``` js
211+
// webpack.config.js
212+
module.exports = {
213+
// this will externalize all modules listed under "dependencies"
214+
// in your package.json
215+
externals: Object.keys(require('./package.json').dependencies)
216+
}
217+
```
218+
219+
### Externals Caveats
220+
221+
Since externalized modules will be shared across every request, you need to make sure that the dependency is **idempotent**. That is, using it across different requests should always yield the same result - it cannot have global state that may be changed by your application. Interactions between externalized modules are fine (e.g. using a Vue plugin).
222+
223+
## Component Caching
193224

194225
You can easily cache components during SSR by implementing the `serverCacheKey` function:
195226

196227
``` js
197228
export default {
229+
name: 'item', // required
198230
props: ['item'],
199231
serverCacheKey: props => props.item.id,
200232
render (h) {
@@ -203,10 +235,15 @@ export default {
203235
}
204236
```
205237

206-
The cache key is per-component, and it should contain sufficient information to represent the shape of the render result. The above is a good implementation because the render result is solely determined by `props.item.id`. However, if the render result also relies on another prop, then you need to modify your `getCacheKey` implementation to take that other prop into account.
238+
Note that cachable component **must also define a unique "name" option**. This is necessary for Vue to determine the identity of the component when using the
239+
bundle renderer.
240+
241+
With a unique name, the cache key is thus per-component: you don't need to worry about two components returning the same key. A cache key should contain sufficient information to represent the shape of the render result. The above is a good implementation if the render result is solely determined by `props.item.id`. However, if the item with the same id may change over time, or if render result also relies on another prop, then you need to modify your `getCacheKey` implementation to take those other variables into account.
207242

208243
Returning a constant will cause the component to always be cached, which is good for purely static components.
209244

245+
### When to use component caching
246+
210247
If the renderer hits a cache for a component during render, it will directly reuse the cached result for the entire sub tree. So **do not cache a component containing child components that rely on global state**.
211248

212249
In most cases, you shouldn't and don't need to cache single-instance components. The most common type of components that need caching are ones in big lists. Since these components are usually driven by objects in database collections, they can make use of a simple caching strategy: generate their cache keys using their unique id plus the last updated timestamp:
@@ -215,14 +252,6 @@ In most cases, you shouldn't and don't need to cache single-instance components.
215252
serverCacheKey: props => props.item.id + '::' + props.item.last_updated
216253
```
217254

218-
## Externals
219-
220-
By default, we will bundle every dependency of our app into the server bundle as well. V8 is very good at optimizing running the same code over and over again, so in most cases the cost of re-running it on every request is a worthwhile tradeoff in return for more freedom in application structure.
221-
222-
You can also further optimize the re-run cost by externalizing dependencies from your bundle. When running the bundle, any raw `require()` calls found in the bundle will return the actual module from your rendering process. With Webpack, you can simply list the modules you want to externalize using the `externals` config option. This avoids having to re-initialize the same module on each request and can also be beneficial for memory usage.
223-
224-
However, since the same module instance will be shared across every request, you need to make sure that the dependency is **idempotent**. That is, using it across different requests should always yield the same result - it cannot have global state that may be changed by your application. Because of this, you should avoid externalizing Vue itself and its plugins.
225-
226255
## Client Side Hydration
227256

228257
In server-rendered output, the root element will have the `server-rendered="true"` attribute. On the client, when you mount a Vue instance to an element with this attribute, it will attempt to "hydrate" the existing DOM instead of creating new DOM nodes.

src/server/render.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { encodeHTML } from 'entities'
44
import { compileToFunctions } from 'web/compiler/index'
55
import { createComponentInstanceForVnode } from 'core/vdom/create-component'
66

7+
let warned = Object.create(null)
8+
const warnOnce = msg => {
9+
if (!warned[msg]) {
10+
warned[msg] = true
11+
console.warn(`\n\u001b[31m${msg}\u001b[39m\n`)
12+
}
13+
}
14+
715
const normalizeAsync = (cache, method) => {
816
const fn = cache[method]
917
if (!fn) {
@@ -61,8 +69,9 @@ export function createRenderFunction (
6169
// check cache hit
6270
const Ctor = node.componentOptions.Ctor
6371
const getKey = Ctor.options.serverCacheKey
64-
if (getKey && cache) {
65-
const key = Ctor.cid + '::' + getKey(node.componentOptions.propsData)
72+
const name = Ctor.options.name
73+
if (getKey && cache && name) {
74+
const key = name + '::' + getKey(node.componentOptions.propsData)
6675
if (has) {
6776
has(key, hit => {
6877
if (hit) {
@@ -81,14 +90,20 @@ export function createRenderFunction (
8190
})
8291
}
8392
} else {
84-
if (getKey) {
85-
console.error(
93+
if (getKey && !cache) {
94+
warnOnce(
8695
`[vue-server-renderer] Component ${
8796
Ctor.options.name || '(anonymous)'
8897
} implemented serverCacheKey, ` +
8998
'but no cache was provided to the renderer.'
9099
)
91100
}
101+
if (getKey && !name) {
102+
warnOnce(
103+
`[vue-server-renderer] Components that implement "serverCacheKey" ` +
104+
`must also define a unique "name" option.`
105+
)
106+
}
92107
renderComponent(node, write, next, isRoot)
93108
}
94109
} else {
@@ -213,6 +228,7 @@ export function createRenderFunction (
213228
write: (text: string, next: Function) => void,
214229
done: Function
215230
) {
231+
warned = Object.create(null)
216232
activeInstance = component
217233
normalizeRender(component)
218234
renderNode(component._render(), write, done, true)

test/ssr/fixtures/cache.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Vue from '../../../dist/vue.common.js'
22

33
const app = {
4+
name: 'app',
45
props: ['id'],
56
serverCacheKey: props => props.id,
67
render (h) {

test/ssr/ssr-bundle-render.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ describe('SSR: bundle renderer', () => {
100100
}
101101
createRenderer('cache.js', renderer => {
102102
const expected = '<div server-rendered="true">&sol;test</div>'
103-
const key = '1::1'
103+
const key = 'app::1'
104104
renderer.renderToString((err, res) => {
105105
expect(err).toBeNull()
106106
expect(res).toBe(expected)
@@ -143,7 +143,7 @@ describe('SSR: bundle renderer', () => {
143143
}
144144
createRenderer('cache.js', renderer => {
145145
const expected = '<div server-rendered="true">&sol;test</div>'
146-
const key = '1::1'
146+
const key = 'app::1'
147147
renderer.renderToString((err, res) => {
148148
expect(err).toBeNull()
149149
expect(res).toBe(expected)

0 commit comments

Comments
 (0)