UNPKG

11.8 kBJavaScriptView Raw
1/* @flow */
2
3import { escape } from 'web/server/util'
4import { SSR_ATTR } from 'shared/constants'
5import { RenderContext } from './render-context'
6import { resolveAsset } from 'core/util/options'
7import { generateComponentTrace } from 'core/util/debug'
8import { ssrCompileToFunctions } from 'web/server/compiler'
9import { installSSRHelpers } from './optimizing-compiler/runtime-helpers'
10
11import { isDef, isUndef, isTrue } from 'shared/util'
12
13import {
14 createComponent,
15 createComponentInstanceForVnode
16} from 'core/vdom/create-component'
17
18let warned = Object.create(null)
19const warnOnce = msg => {
20 if (!warned[msg]) {
21 warned[msg] = true
22 // eslint-disable-next-line no-console
23 console.warn(`\n\u001b[31m${msg}\u001b[39m\n`)
24 }
25}
26
27const onCompilationError = (err, vm) => {
28 const trace = vm ? generateComponentTrace(vm) : ''
29 throw new Error(`\n\u001b[31m${err}${trace}\u001b[39m\n`)
30}
31
32const normalizeRender = vm => {
33 const { render, template, _scopeId } = vm.$options
34 if (isUndef(render)) {
35 if (template) {
36 const compiled = ssrCompileToFunctions(template, {
37 scopeId: _scopeId,
38 warn: onCompilationError
39 }, vm)
40
41 vm.$options.render = compiled.render
42 vm.$options.staticRenderFns = compiled.staticRenderFns
43 } else {
44 throw new Error(
45 `render function or template not defined in component: ${
46 vm.$options.name || vm.$options._componentTag || 'anonymous'
47 }`
48 )
49 }
50 }
51}
52
53function waitForServerPrefetch (vm, resolve, reject) {
54 let handlers = vm.$options.serverPrefetch
55 if (isDef(handlers)) {
56 if (!Array.isArray(handlers)) handlers = [handlers]
57 try {
58 const promises = []
59 for (let i = 0, j = handlers.length; i < j; i++) {
60 const result = handlers[i].call(vm, vm)
61 if (result && typeof result.then === 'function') {
62 promises.push(result)
63 }
64 }
65 Promise.all(promises).then(resolve).catch(reject)
66 return
67 } catch (e) {
68 reject(e)
69 }
70 }
71 resolve()
72}
73
74function renderNode (node, isRoot, context) {
75 if (node.isString) {
76 renderStringNode(node, context)
77 } else if (isDef(node.componentOptions)) {
78 renderComponent(node, isRoot, context)
79 } else if (isDef(node.tag)) {
80 renderElement(node, isRoot, context)
81 } else if (isTrue(node.isComment)) {
82 if (isDef(node.asyncFactory)) {
83 // async component
84 renderAsyncComponent(node, isRoot, context)
85 } else {
86 context.write(`<!--${node.text}-->`, context.next)
87 }
88 } else {
89 context.write(
90 node.raw ? node.text : escape(String(node.text)),
91 context.next
92 )
93 }
94}
95
96function registerComponentForCache (options, write) {
97 // exposed by vue-loader, need to call this if cache hit because
98 // component lifecycle hooks will not be called.
99 const register = options._ssrRegister
100 if (write.caching && isDef(register)) {
101 write.componentBuffer[write.componentBuffer.length - 1].add(register)
102 }
103 return register
104}
105
106function renderComponent (node, isRoot, context) {
107 const { write, next, userContext } = context
108
109 // check cache hit
110 const Ctor = node.componentOptions.Ctor
111 const getKey = Ctor.options.serverCacheKey
112 const name = Ctor.options.name
113 const cache = context.cache
114 const registerComponent = registerComponentForCache(Ctor.options, write)
115
116 if (isDef(getKey) && isDef(cache) && isDef(name)) {
117 const rawKey = getKey(node.componentOptions.propsData)
118 if (rawKey === false) {
119 renderComponentInner(node, isRoot, context)
120 return
121 }
122 const key = name + '::' + rawKey
123 const { has, get } = context
124 if (isDef(has)) {
125 has(key, hit => {
126 if (hit === true && isDef(get)) {
127 get(key, res => {
128 if (isDef(registerComponent)) {
129 registerComponent(userContext)
130 }
131 res.components.forEach(register => register(userContext))
132 write(res.html, next)
133 })
134 } else {
135 renderComponentWithCache(node, isRoot, key, context)
136 }
137 })
138 } else if (isDef(get)) {
139 get(key, res => {
140 if (isDef(res)) {
141 if (isDef(registerComponent)) {
142 registerComponent(userContext)
143 }
144 res.components.forEach(register => register(userContext))
145 write(res.html, next)
146 } else {
147 renderComponentWithCache(node, isRoot, key, context)
148 }
149 })
150 }
151 } else {
152 if (isDef(getKey) && isUndef(cache)) {
153 warnOnce(
154 `[vue-server-renderer] Component ${
155 Ctor.options.name || '(anonymous)'
156 } implemented serverCacheKey, ` +
157 'but no cache was provided to the renderer.'
158 )
159 }
160 if (isDef(getKey) && isUndef(name)) {
161 warnOnce(
162 `[vue-server-renderer] Components that implement "serverCacheKey" ` +
163 `must also define a unique "name" option.`
164 )
165 }
166 renderComponentInner(node, isRoot, context)
167 }
168}
169
170function renderComponentWithCache (node, isRoot, key, context) {
171 const write = context.write
172 write.caching = true
173 const buffer = write.cacheBuffer
174 const bufferIndex = buffer.push('') - 1
175 const componentBuffer = write.componentBuffer
176 componentBuffer.push(new Set())
177 context.renderStates.push({
178 type: 'ComponentWithCache',
179 key,
180 buffer,
181 bufferIndex,
182 componentBuffer
183 })
184 renderComponentInner(node, isRoot, context)
185}
186
187function renderComponentInner (node, isRoot, context) {
188 const prevActive = context.activeInstance
189 // expose userContext on vnode
190 node.ssrContext = context.userContext
191 const child = context.activeInstance = createComponentInstanceForVnode(
192 node,
193 context.activeInstance
194 )
195 normalizeRender(child)
196
197 const resolve = () => {
198 const childNode = child._render()
199 childNode.parent = node
200 context.renderStates.push({
201 type: 'Component',
202 prevActive
203 })
204 renderNode(childNode, isRoot, context)
205 }
206
207 const reject = context.done
208
209 waitForServerPrefetch(child, resolve, reject)
210}
211
212function renderAsyncComponent (node, isRoot, context) {
213 const factory = node.asyncFactory
214
215 const resolve = comp => {
216 if (comp.__esModule && comp.default) {
217 comp = comp.default
218 }
219 const { data, children, tag } = node.asyncMeta
220 const nodeContext = node.asyncMeta.context
221 const resolvedNode: any = createComponent(
222 comp,
223 data,
224 nodeContext,
225 children,
226 tag
227 )
228 if (resolvedNode) {
229 if (resolvedNode.componentOptions) {
230 // normal component
231 renderComponent(resolvedNode, isRoot, context)
232 } else if (!Array.isArray(resolvedNode)) {
233 // single return node from functional component
234 renderNode(resolvedNode, isRoot, context)
235 } else {
236 // multiple return nodes from functional component
237 context.renderStates.push({
238 type: 'Fragment',
239 children: resolvedNode,
240 rendered: 0,
241 total: resolvedNode.length
242 })
243 context.next()
244 }
245 } else {
246 // invalid component, but this does not throw on the client
247 // so render empty comment node
248 context.write(`<!---->`, context.next)
249 }
250 }
251
252 if (factory.resolved) {
253 resolve(factory.resolved)
254 return
255 }
256
257 const reject = context.done
258 let res
259 try {
260 res = factory(resolve, reject)
261 } catch (e) {
262 reject(e)
263 }
264 if (res) {
265 if (typeof res.then === 'function') {
266 res.then(resolve, reject).catch(reject)
267 } else {
268 // new syntax in 2.3
269 const comp = res.component
270 if (comp && typeof comp.then === 'function') {
271 comp.then(resolve, reject).catch(reject)
272 }
273 }
274 }
275}
276
277function renderStringNode (el, context) {
278 const { write, next } = context
279 if (isUndef(el.children) || el.children.length === 0) {
280 write(el.open + (el.close || ''), next)
281 } else {
282 const children: Array<VNode> = el.children
283 context.renderStates.push({
284 type: 'Element',
285 children,
286 rendered: 0,
287 total: children.length,
288 endTag: el.close
289 })
290 write(el.open, next)
291 }
292}
293
294function renderElement (el, isRoot, context) {
295 const { write, next } = context
296
297 if (isTrue(isRoot)) {
298 if (!el.data) el.data = {}
299 if (!el.data.attrs) el.data.attrs = {}
300 el.data.attrs[SSR_ATTR] = 'true'
301 }
302
303 if (el.fnOptions) {
304 registerComponentForCache(el.fnOptions, write)
305 }
306
307 const startTag = renderStartingTag(el, context)
308 const endTag = `</${el.tag}>`
309 if (context.isUnaryTag(el.tag)) {
310 write(startTag, next)
311 } else if (isUndef(el.children) || el.children.length === 0) {
312 write(startTag + endTag, next)
313 } else {
314 const children: Array<VNode> = el.children
315 context.renderStates.push({
316 type: 'Element',
317 children,
318 rendered: 0,
319 total: children.length,
320 endTag
321 })
322 write(startTag, next)
323 }
324}
325
326function hasAncestorData (node: VNode) {
327 const parentNode = node.parent
328 return isDef(parentNode) && (isDef(parentNode.data) || hasAncestorData(parentNode))
329}
330
331function getVShowDirectiveInfo (node: VNode): ?VNodeDirective {
332 let dir: VNodeDirective
333 let tmp
334
335 while (isDef(node)) {
336 if (node.data && node.data.directives) {
337 tmp = node.data.directives.find(dir => dir.name === 'show')
338 if (tmp) {
339 dir = tmp
340 }
341 }
342 node = node.parent
343 }
344 return dir
345}
346
347function renderStartingTag (node: VNode, context) {
348 let markup = `<${node.tag}`
349 const { directives, modules } = context
350
351 // construct synthetic data for module processing
352 // because modules like style also produce code by parent VNode data
353 if (isUndef(node.data) && hasAncestorData(node)) {
354 node.data = {}
355 }
356 if (isDef(node.data)) {
357 // check directives
358 const dirs = node.data.directives
359 if (dirs) {
360 for (let i = 0; i < dirs.length; i++) {
361 const name = dirs[i].name
362 if (name !== 'show') {
363 const dirRenderer = resolveAsset(context, 'directives', name)
364 if (dirRenderer) {
365 // directives mutate the node's data
366 // which then gets rendered by modules
367 dirRenderer(node, dirs[i])
368 }
369 }
370 }
371 }
372
373 // v-show directive needs to be merged from parent to child
374 const vshowDirectiveInfo = getVShowDirectiveInfo(node)
375 if (vshowDirectiveInfo) {
376 directives.show(node, vshowDirectiveInfo)
377 }
378
379 // apply other modules
380 for (let i = 0; i < modules.length; i++) {
381 const res = modules[i](node)
382 if (res) {
383 markup += res
384 }
385 }
386 }
387 // attach scoped CSS ID
388 let scopeId
389 const activeInstance = context.activeInstance
390 if (isDef(activeInstance) &&
391 activeInstance !== node.context &&
392 isDef(scopeId = activeInstance.$options._scopeId)
393 ) {
394 markup += ` ${(scopeId: any)}`
395 }
396 if (isDef(node.fnScopeId)) {
397 markup += ` ${node.fnScopeId}`
398 } else {
399 while (isDef(node)) {
400 if (isDef(scopeId = node.context.$options._scopeId)) {
401 markup += ` ${scopeId}`
402 }
403 node = node.parent
404 }
405 }
406 return markup + '>'
407}
408
409export function createRenderFunction (
410 modules: Array<(node: VNode) => ?string>,
411 directives: Object,
412 isUnaryTag: Function,
413 cache: any
414) {
415 return function render (
416 component: Component,
417 write: (text: string, next: Function) => void,
418 userContext: ?Object,
419 done: Function
420 ) {
421 warned = Object.create(null)
422 const context = new RenderContext({
423 activeInstance: component,
424 userContext,
425 write, done, renderNode,
426 isUnaryTag, modules, directives,
427 cache
428 })
429 installSSRHelpers(component)
430 normalizeRender(component)
431
432 const resolve = () => {
433 renderNode(component._render(), true, context)
434 }
435 waitForServerPrefetch(component, resolve, done)
436 }
437}