Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ee944ca

Browse files
committedFeb 18, 2019
update zh docs
1 parent 14e3169 commit ee944ca

File tree

3 files changed

+159
-140
lines changed

3 files changed

+159
-140
lines changed
 

‎docs/zh/api/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ bundleRenderer.renderToStream([context]): stream.Readable
8484
8585
### template
8686
87+
- **类型:**
88+
- `string`
89+
- `string | (() => string | Promise<string>)` (2.6开始)
90+
**使用字符串模板时:**
91+
8792
为整个页面的 HTML 提供一个模板。此模板应包含注释 `<!--vue-ssr-outlet-->`,作为渲染应用程序内容的占位符。
8893
8994
模板还支持使用渲染上下文 (render context) 进行基本插值:
@@ -101,13 +106,41 @@ bundleRenderer.renderToStream([context]): stream.Readable
101106
102107
在 2.5.0+ 版本中,嵌入式 script 也可以也可以在生产模式 (production mode) 下自行移除。
103108
109+
在 2.6.0+ 版本中,如果存在`context.nonce`,它将作为`nonce`属性添加到嵌入式脚本中。这将允许内联脚本使用nonce属性来保证CSP。
110+
104111
此外,当提供 `clientManifest` 时,模板会自动注入以下内容:
105112
106113
- 渲染当前页面所需的最优客户端 JavaScript 和 CSS 资源(支持自动推导异步代码分割所需的文件);
107114
- 为要渲染页面提供最佳的 `<link rel="preload/prefetch">` 资源提示 (resource hints)。
108115
109116
你也可以通过将 `inject: false` 传递给 renderer,来禁用所有自动注入。
110117
118+
**使用函数模板时:**
119+
120+
::: warning 警告
121+
函数模板仅支持在2.6+且配合`renderer.renderToString`时使用。 不支持在`renderer.renderToStream`时使用
122+
:::
123+
124+
`template`选项也可以是一个函数,返回最终呈现的HTML或返回可被解决为最终呈现的HTML的Promise。这允许您在模板呈现过程中使用原生字符串模板和异步操作。
125+
126+
该函数接收两个参数:
127+
1. 应用组件的渲染结果字符串;
128+
2. 渲染上下文对象。
129+
130+
示例:
131+
``` js
132+
const renderer = createRenderer({
133+
template: (result, context) => {
134+
return `<html>
135+
<head>${context.head}</head>
136+
<body>${result}</body>
137+
<html>`
138+
}
139+
})
140+
```
141+
142+
注意当时用自定义的函数模板时,不会有任何自动注入行为发生 - 你将完全控制最终呈现的HTML所包含的内容,但也需要自己去管理所有你需要引入的部分(例如,使用bundle渲染时所生成的资源链接)。
143+
111144
具体查看:
112145
113146
- [使用一个页面模板](../guide/#using-a-page-template)
@@ -245,6 +278,29 @@ const renderer = createRenderer({
245278
246279
例如,请查看 [`v-show` 的服务器端实现](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js)。
247280
281+
-----------------------------------
282+
### serializer
283+
> 2.6新增
284+
285+
`context.state`提供一个自定义序列化函数。 由于序列化状态将是最终HTML中的一部分,出于安全原因,使用适当的函数转义HTML字符显得非常重要。当设定`{ isJSON: true }`时,默认所使用的序列化器是[serialize-javascript](https://github.com/yahoo/serialize-javascript)
286+
287+
## 仅限服务器端使用的组件选项
288+
### serverCacheKey
289+
- **类型:** `(props) => any`
290+
291+
根据传入的属性(props),生成并返回组件缓存键(cache key)。这里并不允许访问`this`
292+
293+
从2.6开始, 你可以通过显式的返回`false`来避免缓存。
294+
295+
更多信息在 [组件级别缓存(Component-level Caching)](../guide/caching.html#component-level-caching).
296+
297+
### serverPrefetch
298+
- **类型:** `() => Promise<any>`
299+
300+
在服务端渲染的过程中获取异步数据。此函数需要获取到的数据保存在全局store中并返回一个Promise。服务端渲染将会在此钩子函数进行等待,直到Promise被解决。此钩子函数允许通过`this`访问组件实例。
301+
302+
更多信息在 [数据获取(Data Fetching)](../guide/data.html).
303+
248304
## webpack 插件
249305
250306
webpack 插件作为独立文件提供,并且应当直接 require:

‎docs/zh/guide/caching.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export default {
7373

7474
返回常量将导致组件始终被缓存,这对纯静态组件是有好处的。
7575

76+
::: tip 避免缓存
77+
从2.6.0开始,通过在`serverCacheKey`中显式返回`false`,将会避免缓存,重新将组件渲染
78+
:::
79+
7680
### 何时使用组件缓存
7781

7882
如果 renderer 在组件渲染过程中进行缓存命中,那么它将直接重新使用整个子树的缓存结果。这意味着在以下情况,你****应该缓存组件:

‎docs/zh/guide/data.md

Lines changed: 99 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
## 数据预取存储容器 (Data Store)
44

5-
在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,**那么在开始渲染过程之前,需要先预取和解析好这些数据**
5+
在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照"。在我们装载客户端应用之前,我们组件中所应用的异步数据需要处于可用状态 - 否则客户端应用会使用不同的状态进行渲染,并导致激活失败
66

7-
另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。
8-
9-
为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。
7+
为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。首先,在服务器端,我们可以在渲染时预取数据,并将数据填充到 store 中。此外
8+
,我们将在应用渲染完成后,在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。
109

1110
为此,我们将使用官方状态管理库 [Vuex](https://github.com/vuejs/vuex/)。我们先创建一个 `store.js` 文件,里面会模拟一些根据 id 获取 item 的逻辑:
1211

@@ -22,10 +21,12 @@ Vue.use(Vuex)
2221
import { fetchItem } from './api'
2322

2423
export function createStore () {
24+
// 重要: state必须是一个函数,
25+
// 这样模块才可以多次实例化
2526
return new Vuex.Store({
26-
state: {
27+
state: () => ({
2728
items: {}
28-
},
29+
}),
2930
actions: {
3031
fetchItem ({ commit }, id) {
3132
// `store.dispatch()` 会返回 Promise,
@@ -35,6 +36,7 @@ export function createStore () {
3536
})
3637
}
3738
},
39+
3840
mutations: {
3941
setItem (state, { id, item }) {
4042
Vue.set(state.items, id, item)
@@ -44,6 +46,12 @@ export function createStore () {
4446
}
4547
```
4648

49+
::: warning
50+
大多数情况下,你都应该将 `state` 包装成一个函数,这样它的状态便不会泄露到下一个服务端执行。
51+
[更多信息](./structure.md#avoid-stateful-singletons)
52+
:::
53+
54+
4755
然后修改 `app.js`
4856

4957
``` js
@@ -80,33 +88,65 @@ export function createApp () {
8088

8189
我们需要通过访问路由,来决定获取哪部分数据 - 这也决定了哪些组件需要渲染。事实上,给定路由所需的数据,也是在该路由上渲染组件时所需的数据。所以在路由组件中放置数据预取逻辑,是很自然的事情。
8290

83-
我们将在路由组件上暴露出一个自定义静态函数 `asyncData`。注意,由于此函数会在组件实例化之前调用,所以它无法访问 `this`。需要将 store 和路由信息作为参数传递进去:
91+
我们将在组件中使用 `serverPrefetch` 选项(2.6.0+中新增)。这个选项会被服务端渲染所识别,并且暂停渲染过程,直到它所返回的promise被解决。这允许我们在渲染过程中“等待”异步数据。
92+
93+
::: tip 提示
94+
你可以在任何组件中使用`serverPrefetch`,不仅仅局限在路由级别组件上
95+
:::
96+
97+
这里有一个`Item.vue`组件示例,它在路由匹配`'/item/:id'`时进行渲染。由于此时组件实例已经被创建,所以可以通过`this`进行访问:
8498

8599
``` html
86100
<!-- Item.vue -->
87101
<template>
88-
<div>{{ item.title }}</div>
102+
<div v-if="item">{{ item.title }}</div>
103+
<div v-else>...</div>
89104
</template>
90105

91106
<script>
92107
export default {
93-
asyncData ({ store, route }) {
94-
// 触发 action 后,会返回 Promise
95-
return store.dispatch('fetchItem', route.params.id)
96-
},
97108
computed: {
98109
// 从 store 的 state 对象中的获取 item。
99110
item () {
100111
return this.$store.state.items[this.$route.params.id]
101112
}
113+
},
114+
// 仅限服务端
115+
// 它将在服务端渲染时自动被调用
116+
serverPrefetch () {
117+
// 在执行后返回Promise
118+
// 以便组件在渲染执行之前等待
119+
return this.fetchItem()
120+
},
121+
// 仅限客户端
122+
mounted () {
123+
// 如果我们确定不在服务端执行
124+
// 那么在这里获取item(首先展示加载文字)
125+
if (!this.item) {
126+
this.fetchItem()
127+
}
128+
},
129+
methods: {
130+
fetchItem () {
131+
// 在执行后返回Promise
132+
return store.dispatch('fetchItem', this.$route.params.id)
133+
}
102134
}
103135
}
104136
</script>
105137
```
106138

107-
## 服务器端数据预取 (Server Data Fetching)
139+
::: warning 警告
140+
为了避免逻辑执行两次,你需要检查组件在`mounted`钩子触发时是否已完成服务端渲染
141+
:::
108142

109-
`entry-server.js` 中,我们可以通过路由获得与 `router.getMatchedComponents()` 相匹配的组件,如果组件暴露出 `asyncData`,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。
143+
::: tip 提示
144+
你会注意到,`fetchItem()`逻辑在每一个组件中被重复调用了多次 (在 `serverPrefetch`, `mounted``watch` 回调) - 建议您自己进行抽象(例如使用混合或插件机制)以简化此类代码
145+
:::
146+
147+
## 最终状态注入
148+
149+
现在我们知道了组件中,渲染过程会等待数据获取完成后继续进行,那么我们如何知道何时才是“完成”状态?为了实现此逻辑,我们需要在渲染上下文中附加一个`rendered`回调函数(同样是2.6新增),服务端渲染会在渲染过程完成时调用此回调。在这个时刻,全局store中保存的是应用的最终状态。此时我们可以在回调中将它注入到上下文当中:
110150

111151
``` js
112152
// entry-server.js
@@ -119,29 +159,17 @@ export default context => {
119159
router.push(context.url)
120160

121161
router.onReady(() => {
122-
const matchedComponents = router.getMatchedComponents()
123-
if (!matchedComponents.length) {
124-
return reject({ code: 404 })
125-
}
126-
127-
// 对所有匹配的路由组件调用 `asyncData()`
128-
Promise.all(matchedComponents.map(Component => {
129-
if (Component.asyncData) {
130-
return Component.asyncData({
131-
store,
132-
route: router.currentRoute
133-
})
134-
}
135-
})).then(() => {
136-
// 在所有预取钩子(preFetch hook) resolve 后,
137-
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
138-
// 当我们将状态附加到上下文,
139-
// 并且 `template` 选项用于 renderer 时,
140-
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
162+
// `rendered`钩子函数会在应用完成渲染时被调用
163+
context.rendered = () => {
164+
// 在应用渲染完成后,此时我们的store中
165+
// 填满了组件中所使用的方案状态。
166+
// 当我们将状态附加到上下文中,并且`template`选项
167+
// 被渲染器所使用时,状态会被自动序列化并以`window.__INITIAL_STATE__`
168+
// 的形式注入到HTML中
141169
context.state = store.state
170+
}
142171

143-
resolve(app)
144-
}).catch(reject)
172+
resolve(app)
145173
}, reject)
146174
})
147175
}
@@ -152,107 +180,14 @@ export default context => {
152180
``` js
153181
// entry-client.js
154182

155-
const { app, router, store } = createApp()
183+
const { app, store } = createApp()
156184

157185
if (window.__INITIAL_STATE__) {
186+
// 使用服务端注入的数据进行store的初始化工作
158187
store.replaceState(window.__INITIAL_STATE__)
159188
}
189+
app.$mount('#app')
160190
```
161-
162-
## 客户端数据预取 (Client Data Fetching)
163-
164-
在客户端,处理数据预取有两种不同方式:
165-
166-
1. **在路由导航之前解析数据:**
167-
168-
使用此策略,应用程序会等待视图所需数据全部解析之后,再传入数据并处理当前视图。好处在于,可以直接在数据准备就绪时,传入视图渲染完整内容,但是如果数据预取需要很长时间,用户在当前视图会感受到"明显卡顿"。因此,如果使用此策略,建议提供一个数据加载指示器 (data loading indicator)。
169-
170-
我们可以通过检查匹配的组件,并在全局路由钩子函数中执行 `asyncData` 函数,来在客户端实现此策略。注意,在初始路由准备就绪之后,我们应该注册此钩子,这样我们就不必再次获取服务器提取的数据。
171-
172-
``` js
173-
// entry-client.js
174-
175-
// ...忽略无关代码
176-
177-
router.onReady(() => {
178-
// 添加路由钩子函数,用于处理 asyncData.
179-
// 在初始路由 resolve 后执行,
180-
// 以便我们不会二次预取(double-fetch)已有的数据。
181-
// 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
182-
router.beforeResolve((to, from, next) => {
183-
const matched = router.getMatchedComponents(to)
184-
const prevMatched = router.getMatchedComponents(from)
185-
186-
// 我们只关心非预渲染的组件
187-
// 所以我们对比它们,找出两个匹配列表的差异组件
188-
let diffed = false
189-
const activated = matched.filter((c, i) => {
190-
return diffed || (diffed = (prevMatched[i] !== c))
191-
})
192-
193-
if (!activated.length) {
194-
return next()
195-
}
196-
197-
// 这里如果有加载指示器 (loading indicator),就触发
198-
199-
Promise.all(activated.map(c => {
200-
if (c.asyncData) {
201-
return c.asyncData({ store, route: to })
202-
}
203-
})).then(() => {
204-
205-
// 停止加载指示器(loading indicator)
206-
207-
next()
208-
}).catch(next)
209-
})
210-
211-
app.$mount('#app')
212-
})
213-
```
214-
215-
2. **匹配要渲染的视图后,再获取数据:**
216-
217-
此策略将客户端数据预取逻辑,放在视图组件的 `beforeMount` 函数中。当路由导航被触发时,可以立即切换视图,因此应用程序具有更快的响应速度。然而,传入视图在渲染时不会有完整的可用数据。因此,对于使用此策略的每个视图组件,都需要具有条件加载状态。
218-
219-
这可以通过纯客户端 (client-only) 的全局 mixin 来实现:
220-
221-
``` js
222-
Vue.mixin({
223-
beforeMount () {
224-
const { asyncData } = this.$options
225-
if (asyncData) {
226-
// 将获取数据操作分配给 promise
227-
// 以便在组件中,我们可以在数据准备就绪后
228-
// 通过运行 `this.dataPromise.then(...)` 来执行其他任务
229-
this.dataPromise = asyncData({
230-
store: this.$store,
231-
route: this.$route
232-
})
233-
}
234-
}
235-
})
236-
```
237-
238-
这两种策略是根本上不同的用户体验决策,应该根据你创建的应用程序的实际使用场景进行挑选。但是无论你选择哪种策略,当路由组件重用(同一路由,但是 params 或 query 已更改,例如,从 `user/1``user/2`)时,也应该调用 `asyncData` 函数。我们也可以通过纯客户端 (client-only) 的全局 mixin 来处理这个问题:
239-
240-
``` js
241-
Vue.mixin({
242-
beforeRouteUpdate (to, from, next) {
243-
const { asyncData } = this.$options
244-
if (asyncData) {
245-
asyncData({
246-
store: this.$store,
247-
route: to
248-
}).then(next).catch(next)
249-
} else {
250-
next()
251-
}
252-
}
253-
})
254-
```
255-
256191
## Store 代码拆分 (Store Code Splitting)
257192

258193
在大型应用程序中,我们的 Vuex store 可能会分为多个模块。当然,也可以将这些模块代码,分割到相应的路由组件 chunk 中。假设我们有以下 store 模块:
@@ -261,14 +196,17 @@ Vue.mixin({
261196
// store/modules/foo.js
262197
export default {
263198
namespaced: true,
199+
264200
// 重要信息:state 必须是一个函数,
265201
// 因此可以创建多个实例化该模块
266202
state: () => ({
267203
count: 0
268204
}),
205+
269206
actions: {
270207
inc: ({ commit }) => commit('inc')
271208
},
209+
272210
mutations: {
273211
inc: state => state.count++
274212
}
@@ -288,9 +226,26 @@ export default {
288226
import fooStoreModule from '../store/modules/foo'
289227
290228
export default {
291-
asyncData ({ store }) {
292-
store.registerModule('foo', fooStoreModule)
293-
return store.dispatch('foo/inc')
229+
computed: {
230+
fooCount () {
231+
return this.$store.state.foo.count
232+
}
233+
},
234+
// 仅限服务端
235+
serverPrefetch () {
236+
this.registerFoo()
237+
return this.fooInc()
238+
},
239+
// 仅限客户端
240+
mounted () {
241+
// 我们已经在服务端增加了'count'
242+
// 我们通过'foo'状态是否存在来进行检查
243+
const alreadyIncremented = !!this.$store.state.foo
244+
// 我们注册foo模块
245+
this.registerFoo()
246+
if (!alreadyIncremented) {
247+
this.fooInc()
248+
}
294249
},
295250
296251
// 重要信息:当多次访问路由时,
@@ -299,9 +254,13 @@ export default {
299254
this.$store.unregisterModule('foo')
300255
},
301256
302-
computed: {
303-
fooCount () {
304-
return this.$store.state.foo.count
257+
methods: {
258+
registerFoo () {
259+
// 如果状态在服务端已被注入,则保留之前的状态
260+
this.$store.registerModule('foo', fooStoreModule, { preserveState: true })
261+
},
262+
fooInc () {
263+
return this.$store.dispatch('foo/inc')
305264
}
306265
}
307266
}
@@ -310,6 +269,6 @@ export default {
310269

311270
由于模块现在是路由组件的依赖,所以它将被 webpack 移动到路由组件的异步 chunk 中。
312271

313-
---
314-
315-
哦?看起来要写很多代码!这是因为,通用数据预取可能是服务器渲染应用程序中最复杂的问题,我们正在为下一步开发做前期准备。一旦设定好模板示例,创建单独组件实际上会变得相当轻松。
272+
::: warning 警告
273+
不要忘记在`registerModule`时使用`preserveState: true`选项,这样我们就可以保持服务器端注入的状态了
274+
:::

0 commit comments

Comments
 (0)
Please sign in to comment.