feat(ssr): renderToStream (#1197)
This commit is contained in:
committed by
GitHub
parent
e0d19a6953
commit
6bc0e0a31a
46
packages/server-renderer/src/helpers/ssrCompile.ts
Normal file
46
packages/server-renderer/src/helpers/ssrCompile.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ComponentInternalInstance, warn } from 'vue'
|
||||
import { compile } from '@vue/compiler-ssr'
|
||||
import { generateCodeFrame, NO } from '@vue/shared'
|
||||
import { CompilerError } from '@vue/compiler-core'
|
||||
import { PushFn } from '../render'
|
||||
|
||||
type SSRRenderFunction = (
|
||||
context: any,
|
||||
push: PushFn,
|
||||
parentInstance: ComponentInternalInstance
|
||||
) => void
|
||||
|
||||
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)
|
||||
|
||||
export function ssrCompile(
|
||||
template: string,
|
||||
instance: ComponentInternalInstance
|
||||
): SSRRenderFunction {
|
||||
const cached = compileCache[template]
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const { code } = compile(template, {
|
||||
isCustomElement: instance.appContext.config.isCustomElement || NO,
|
||||
isNativeTag: instance.appContext.config.isNativeTag || NO,
|
||||
onError(err: CompilerError) {
|
||||
if (__DEV__) {
|
||||
const message = `[@vue/server-renderer] Template compilation error: ${
|
||||
err.message
|
||||
}`
|
||||
const codeFrame =
|
||||
err.loc &&
|
||||
generateCodeFrame(
|
||||
template as string,
|
||||
err.loc.start.offset,
|
||||
err.loc.end.offset
|
||||
)
|
||||
warn(codeFrame ? `${message}\n${codeFrame}` : message)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
return (compileCache[template] = Function('require', code)(require))
|
||||
}
|
||||
15
packages/server-renderer/src/helpers/ssrRenderComponent.ts
Normal file
15
packages/server-renderer/src/helpers/ssrRenderComponent.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component, ComponentInternalInstance, createVNode, Slots } from 'vue'
|
||||
import { Props, renderComponentVNode, SSRBuffer } from '../render'
|
||||
import { SSRSlots } from './ssrRenderSlot'
|
||||
|
||||
export function ssrRenderComponent(
|
||||
comp: Component,
|
||||
props: Props | null = null,
|
||||
children: Slots | SSRSlots | null = null,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
): SSRBuffer | Promise<SSRBuffer> {
|
||||
return renderComponentVNode(
|
||||
createVNode(comp, props, children),
|
||||
parentComponent
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Props, PushFn, renderVNodeChildren } from '../renderToString'
|
||||
import { ComponentInternalInstance, Slot, Slots } from 'vue'
|
||||
import { Props, PushFn, renderVNodeChildren } from '../render'
|
||||
|
||||
export type SSRSlots = Record<string, SSRSlot>
|
||||
|
||||
export type SSRSlot = (
|
||||
props: Props,
|
||||
push: PushFn,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PushFn } from '../renderToString'
|
||||
import { PushFn } from '../render'
|
||||
|
||||
export async function ssrRenderSuspense(
|
||||
push: PushFn,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { ComponentInternalInstance, ssrContextKey } from 'vue'
|
||||
import {
|
||||
SSRContext,
|
||||
createBuffer,
|
||||
PushFn,
|
||||
SSRBufferItem
|
||||
} from '../renderToString'
|
||||
import { createBuffer, PushFn, SSRBufferItem, SSRContext } from '../render'
|
||||
|
||||
export function ssrRenderTeleport(
|
||||
parentPush: PushFn,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// public
|
||||
export { renderToString, SSRContext } from './renderToString'
|
||||
export { SSRContext } from './render'
|
||||
export { renderToString } from './renderToString'
|
||||
export { renderToStream } from './renderToStream'
|
||||
|
||||
// internal runtime helpers
|
||||
export { renderComponent as ssrRenderComponent } from './renderToString'
|
||||
export { ssrRenderComponent } from './helpers/ssrRenderComponent'
|
||||
export { ssrRenderSlot } from './helpers/ssrRenderSlot'
|
||||
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
|
||||
export {
|
||||
ssrRenderClass,
|
||||
ssrRenderStyle,
|
||||
@@ -13,7 +16,6 @@ export {
|
||||
} from './helpers/ssrRenderAttrs'
|
||||
export { ssrInterpolate } from './helpers/ssrInterpolate'
|
||||
export { ssrRenderList } from './helpers/ssrRenderList'
|
||||
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
|
||||
export { ssrRenderSuspense } from './helpers/ssrRenderSuspense'
|
||||
|
||||
// v-model helpers
|
||||
|
||||
286
packages/server-renderer/src/render.ts
Normal file
286
packages/server-renderer/src/render.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import {
|
||||
Comment,
|
||||
Component,
|
||||
ComponentInternalInstance,
|
||||
DirectiveBinding,
|
||||
Fragment,
|
||||
mergeProps,
|
||||
ssrUtils,
|
||||
Static,
|
||||
Text,
|
||||
VNode,
|
||||
VNodeArrayChildren,
|
||||
VNodeProps,
|
||||
warn
|
||||
} from 'vue'
|
||||
import {
|
||||
escapeHtml,
|
||||
escapeHtmlComment,
|
||||
isFunction,
|
||||
isPromise,
|
||||
isString,
|
||||
isVoidTag,
|
||||
ShapeFlags
|
||||
} from '@vue/shared'
|
||||
import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
|
||||
import { ssrCompile } from './helpers/ssrCompile'
|
||||
import { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
|
||||
|
||||
const {
|
||||
createComponentInstance,
|
||||
setCurrentRenderingInstance,
|
||||
setupComponent,
|
||||
renderComponentRoot,
|
||||
normalizeVNode,
|
||||
normalizeSuspenseChildren
|
||||
} = ssrUtils
|
||||
|
||||
export type SSRBuffer = SSRBufferItem[]
|
||||
export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
|
||||
export type PushFn = (item: SSRBufferItem) => void
|
||||
export type Props = Record<string, unknown>
|
||||
|
||||
export type SSRContext = {
|
||||
[key: string]: any
|
||||
teleports?: Record<string, string>
|
||||
__teleportBuffers?: Record<string, SSRBuffer>
|
||||
}
|
||||
|
||||
// Each component has a buffer array.
|
||||
// A buffer array can contain one of the following:
|
||||
// - plain string
|
||||
// - A resolved buffer (recursive arrays of strings that can be unrolled
|
||||
// synchronously)
|
||||
// - An async buffer (a Promise that resolves to a resolved buffer)
|
||||
export function createBuffer() {
|
||||
let appendable = false
|
||||
const buffer: SSRBuffer = []
|
||||
return {
|
||||
getBuffer(): SSRBuffer {
|
||||
// Return static buffer and await on items during unroll stage
|
||||
return buffer
|
||||
},
|
||||
push(item: SSRBufferItem) {
|
||||
const isStringItem = isString(item)
|
||||
if (appendable && isStringItem) {
|
||||
buffer[buffer.length - 1] += item as string
|
||||
} else {
|
||||
buffer.push(item)
|
||||
}
|
||||
appendable = isStringItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function renderComponentVNode(
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
): SSRBuffer | Promise<SSRBuffer> {
|
||||
const instance = createComponentInstance(vnode, parentComponent, null)
|
||||
const res = setupComponent(instance, true /* isSSR */)
|
||||
if (isPromise(res)) {
|
||||
return res
|
||||
.catch(err => {
|
||||
warn(`[@vue/server-renderer]: Uncaught error in async setup:\n`, err)
|
||||
})
|
||||
.then(() => renderComponentSubTree(instance))
|
||||
} else {
|
||||
return renderComponentSubTree(instance)
|
||||
}
|
||||
}
|
||||
|
||||
function renderComponentSubTree(
|
||||
instance: ComponentInternalInstance
|
||||
): SSRBuffer | Promise<SSRBuffer> {
|
||||
const comp = instance.type as Component
|
||||
const { getBuffer, push } = createBuffer()
|
||||
if (isFunction(comp)) {
|
||||
renderVNode(push, renderComponentRoot(instance), instance)
|
||||
} else {
|
||||
if (!instance.render && !comp.ssrRender && isString(comp.template)) {
|
||||
comp.ssrRender = ssrCompile(comp.template, instance)
|
||||
}
|
||||
|
||||
if (comp.ssrRender) {
|
||||
// optimized
|
||||
// set current rendering instance for asset resolution
|
||||
setCurrentRenderingInstance(instance)
|
||||
comp.ssrRender(instance.proxy, push, instance)
|
||||
setCurrentRenderingInstance(null)
|
||||
} else if (instance.render) {
|
||||
renderVNode(push, renderComponentRoot(instance), instance)
|
||||
} else {
|
||||
warn(
|
||||
`Component ${
|
||||
comp.name ? `${comp.name} ` : ``
|
||||
} is missing template or render function.`
|
||||
)
|
||||
push(`<!---->`)
|
||||
}
|
||||
}
|
||||
return getBuffer()
|
||||
}
|
||||
|
||||
function renderVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const { type, shapeFlag, children } = vnode
|
||||
switch (type) {
|
||||
case Text:
|
||||
push(escapeHtml(children as string))
|
||||
break
|
||||
case Comment:
|
||||
push(
|
||||
children ? `<!--${escapeHtmlComment(children as string)}-->` : `<!---->`
|
||||
)
|
||||
break
|
||||
case Static:
|
||||
push(children as string)
|
||||
break
|
||||
case Fragment:
|
||||
push(`<!--[-->`) // open
|
||||
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
|
||||
push(`<!--]-->`) // close
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
renderElementVNode(push, vnode, parentComponent)
|
||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||
push(renderComponentVNode(vnode, parentComponent))
|
||||
} else if (shapeFlag & ShapeFlags.TELEPORT) {
|
||||
renderTeleportVNode(push, vnode, parentComponent)
|
||||
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
renderVNode(
|
||||
push,
|
||||
normalizeSuspenseChildren(vnode).content,
|
||||
parentComponent
|
||||
)
|
||||
} else {
|
||||
warn(
|
||||
'[@vue/server-renderer] Invalid VNode type:',
|
||||
type,
|
||||
`(${typeof type})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function renderVNodeChildren(
|
||||
push: PushFn,
|
||||
children: VNodeArrayChildren,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
renderVNode(push, normalizeVNode(children[i]), parentComponent)
|
||||
}
|
||||
}
|
||||
|
||||
function renderElementVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const tag = vnode.type as string
|
||||
let { props, children, shapeFlag, scopeId, dirs } = vnode
|
||||
let openTag = `<${tag}`
|
||||
|
||||
if (dirs) {
|
||||
props = applySSRDirectives(vnode, props, dirs)
|
||||
}
|
||||
|
||||
if (props) {
|
||||
openTag += ssrRenderAttrs(props, tag)
|
||||
}
|
||||
|
||||
if (scopeId) {
|
||||
openTag += ` ${scopeId}`
|
||||
const treeOwnerId = parentComponent && parentComponent.type.__scopeId
|
||||
// vnode's own scopeId and the current rendering component's scopeId is
|
||||
// different - this is a slot content node.
|
||||
if (treeOwnerId && treeOwnerId !== scopeId) {
|
||||
openTag += ` ${treeOwnerId}-s`
|
||||
}
|
||||
}
|
||||
|
||||
push(openTag + `>`)
|
||||
if (!isVoidTag(tag)) {
|
||||
let hasChildrenOverride = false
|
||||
if (props) {
|
||||
if (props.innerHTML) {
|
||||
hasChildrenOverride = true
|
||||
push(props.innerHTML)
|
||||
} else if (props.textContent) {
|
||||
hasChildrenOverride = true
|
||||
push(escapeHtml(props.textContent))
|
||||
} else if (tag === 'textarea' && props.value) {
|
||||
hasChildrenOverride = true
|
||||
push(escapeHtml(props.value))
|
||||
}
|
||||
}
|
||||
if (!hasChildrenOverride) {
|
||||
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
push(escapeHtml(children as string))
|
||||
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
renderVNodeChildren(
|
||||
push,
|
||||
children as VNodeArrayChildren,
|
||||
parentComponent
|
||||
)
|
||||
}
|
||||
}
|
||||
push(`</${tag}>`)
|
||||
}
|
||||
}
|
||||
|
||||
function applySSRDirectives(
|
||||
vnode: VNode,
|
||||
rawProps: VNodeProps | null,
|
||||
dirs: DirectiveBinding[]
|
||||
): VNodeProps {
|
||||
const toMerge: VNodeProps[] = []
|
||||
for (let i = 0; i < dirs.length; i++) {
|
||||
const binding = dirs[i]
|
||||
const {
|
||||
dir: { getSSRProps }
|
||||
} = binding
|
||||
if (getSSRProps) {
|
||||
const props = getSSRProps(binding, vnode)
|
||||
if (props) toMerge.push(props)
|
||||
}
|
||||
}
|
||||
return mergeProps(rawProps || {}, ...toMerge)
|
||||
}
|
||||
|
||||
function renderTeleportVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const target = vnode.props && vnode.props.to
|
||||
const disabled = vnode.props && vnode.props.disabled
|
||||
if (!target) {
|
||||
warn(`[@vue/server-renderer] Teleport is missing target prop.`)
|
||||
return []
|
||||
}
|
||||
if (!isString(target)) {
|
||||
warn(
|
||||
`[@vue/server-renderer] Teleport target must be a query selector string.`
|
||||
)
|
||||
return []
|
||||
}
|
||||
ssrRenderTeleport(
|
||||
push,
|
||||
push => {
|
||||
renderVNodeChildren(
|
||||
push,
|
||||
vnode.children as VNodeArrayChildren,
|
||||
parentComponent
|
||||
)
|
||||
},
|
||||
target,
|
||||
disabled || disabled === '',
|
||||
parentComponent
|
||||
)
|
||||
}
|
||||
59
packages/server-renderer/src/renderToStream.ts
Normal file
59
packages/server-renderer/src/renderToStream.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
App,
|
||||
VNode,
|
||||
createVNode,
|
||||
ssrUtils,
|
||||
createApp,
|
||||
ssrContextKey
|
||||
} from 'vue'
|
||||
import { isString, isPromise } from '@vue/shared'
|
||||
import { renderComponentVNode, SSRBuffer, SSRContext } from './render'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
const { isVNode } = ssrUtils
|
||||
|
||||
async function unrollBuffer(
|
||||
buffer: SSRBuffer,
|
||||
stream: Readable
|
||||
): Promise<void> {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
let item = buffer[i]
|
||||
if (isPromise(item)) {
|
||||
item = await item
|
||||
}
|
||||
if (isString(item)) {
|
||||
stream.push(item)
|
||||
} else {
|
||||
await unrollBuffer(item, stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function renderToStream(
|
||||
input: App | VNode,
|
||||
context: SSRContext = {}
|
||||
): Readable {
|
||||
if (isVNode(input)) {
|
||||
// raw vnode, wrap with app (for context)
|
||||
return renderToStream(createApp({ render: () => input }), context)
|
||||
}
|
||||
|
||||
// rendering an app
|
||||
const vnode = createVNode(input._component, input._props)
|
||||
vnode.appContext = input._context
|
||||
// provide the ssr context to the tree
|
||||
input.provide(ssrContextKey, context)
|
||||
|
||||
const stream = new Readable()
|
||||
|
||||
Promise.resolve(renderComponentVNode(vnode))
|
||||
.then(buffer => unrollBuffer(buffer, stream))
|
||||
.then(() => {
|
||||
stream.push(null)
|
||||
})
|
||||
.catch(error => {
|
||||
stream.destroy(error)
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
@@ -1,109 +1,27 @@
|
||||
import {
|
||||
App,
|
||||
Component,
|
||||
ComponentInternalInstance,
|
||||
VNode,
|
||||
VNodeArrayChildren,
|
||||
createVNode,
|
||||
Text,
|
||||
Comment,
|
||||
Static,
|
||||
Fragment,
|
||||
ssrUtils,
|
||||
Slots,
|
||||
createApp,
|
||||
createVNode,
|
||||
ssrContextKey,
|
||||
warn,
|
||||
DirectiveBinding,
|
||||
VNodeProps,
|
||||
mergeProps
|
||||
ssrUtils,
|
||||
VNode
|
||||
} from 'vue'
|
||||
import {
|
||||
ShapeFlags,
|
||||
isString,
|
||||
isPromise,
|
||||
isArray,
|
||||
isFunction,
|
||||
isVoidTag,
|
||||
escapeHtml,
|
||||
NO,
|
||||
generateCodeFrame,
|
||||
escapeHtmlComment
|
||||
} from '@vue/shared'
|
||||
import { compile } from '@vue/compiler-ssr'
|
||||
import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
|
||||
import { SSRSlots } from './helpers/ssrRenderSlot'
|
||||
import { CompilerError } from '@vue/compiler-dom'
|
||||
import { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
|
||||
import { isPromise, isString } from '@vue/shared'
|
||||
import { SSRContext, renderComponentVNode, SSRBuffer } from './render'
|
||||
|
||||
const {
|
||||
isVNode,
|
||||
createComponentInstance,
|
||||
setCurrentRenderingInstance,
|
||||
setupComponent,
|
||||
renderComponentRoot,
|
||||
normalizeVNode,
|
||||
normalizeSuspenseChildren
|
||||
} = ssrUtils
|
||||
const { isVNode } = ssrUtils
|
||||
|
||||
// Each component has a buffer array.
|
||||
// A buffer array can contain one of the following:
|
||||
// - plain string
|
||||
// - A resolved buffer (recursive arrays of strings that can be unrolled
|
||||
// synchronously)
|
||||
// - An async buffer (a Promise that resolves to a resolved buffer)
|
||||
export type SSRBuffer = SSRBufferItem[]
|
||||
export type SSRBufferItem =
|
||||
| string
|
||||
| ResolvedSSRBuffer
|
||||
| Promise<ResolvedSSRBuffer>
|
||||
export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
||||
|
||||
export type PushFn = (item: SSRBufferItem) => void
|
||||
|
||||
export type Props = Record<string, unknown>
|
||||
|
||||
export type SSRContext = {
|
||||
[key: string]: any
|
||||
teleports?: Record<string, string>
|
||||
__teleportBuffers?: Record<string, SSRBuffer>
|
||||
}
|
||||
|
||||
export function createBuffer() {
|
||||
let appendable = false
|
||||
let hasAsync = false
|
||||
const buffer: SSRBuffer = []
|
||||
return {
|
||||
getBuffer(): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
// If the current component's buffer contains any Promise from async children,
|
||||
// then it must return a Promise too. Otherwise this is a component that
|
||||
// contains only sync children so we can avoid the async book-keeping overhead.
|
||||
return hasAsync ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
|
||||
},
|
||||
push(item: SSRBufferItem) {
|
||||
const isStringItem = isString(item)
|
||||
if (appendable && isStringItem) {
|
||||
buffer[buffer.length - 1] += item as string
|
||||
} else {
|
||||
buffer.push(item)
|
||||
}
|
||||
appendable = isStringItem
|
||||
if (!isStringItem && !isArray(item)) {
|
||||
// promise
|
||||
hasAsync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
|
||||
async function unrollBuffer(buffer: SSRBuffer): Promise<string> {
|
||||
let ret = ''
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
const item = buffer[i]
|
||||
let item = buffer[i]
|
||||
if (isPromise(item)) {
|
||||
item = await item
|
||||
}
|
||||
if (isString(item)) {
|
||||
ret += item
|
||||
} else {
|
||||
ret += unrollBuffer(item)
|
||||
ret += await unrollBuffer(item as SSRBuffer)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
@@ -127,272 +45,7 @@ export async function renderToString(
|
||||
|
||||
await resolveTeleports(context)
|
||||
|
||||
return unrollBuffer(buffer)
|
||||
}
|
||||
|
||||
export function renderComponent(
|
||||
comp: Component,
|
||||
props: Props | null = null,
|
||||
children: Slots | SSRSlots | null = null,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
return renderComponentVNode(
|
||||
createVNode(comp, props, children),
|
||||
parentComponent
|
||||
)
|
||||
}
|
||||
|
||||
function renderComponentVNode(
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
const instance = createComponentInstance(vnode, parentComponent, null)
|
||||
const res = setupComponent(instance, true /* isSSR */)
|
||||
if (isPromise(res)) {
|
||||
return res
|
||||
.catch(err => {
|
||||
warn(`[@vue/server-renderer]: Uncaught error in async setup:\n`, err)
|
||||
})
|
||||
.then(() => renderComponentSubTree(instance))
|
||||
} else {
|
||||
return renderComponentSubTree(instance)
|
||||
}
|
||||
}
|
||||
|
||||
function renderComponentSubTree(
|
||||
instance: ComponentInternalInstance
|
||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
const comp = instance.type as Component
|
||||
const { getBuffer, push } = createBuffer()
|
||||
if (isFunction(comp)) {
|
||||
renderVNode(push, renderComponentRoot(instance), instance)
|
||||
} else {
|
||||
if (!instance.render && !comp.ssrRender && isString(comp.template)) {
|
||||
comp.ssrRender = ssrCompile(comp.template, instance)
|
||||
}
|
||||
|
||||
if (comp.ssrRender) {
|
||||
// optimized
|
||||
// set current rendering instance for asset resolution
|
||||
setCurrentRenderingInstance(instance)
|
||||
comp.ssrRender(instance.proxy, push, instance)
|
||||
setCurrentRenderingInstance(null)
|
||||
} else if (instance.render) {
|
||||
renderVNode(push, renderComponentRoot(instance), instance)
|
||||
} else {
|
||||
warn(
|
||||
`Component ${
|
||||
comp.name ? `${comp.name} ` : ``
|
||||
} is missing template or render function.`
|
||||
)
|
||||
push(`<!---->`)
|
||||
}
|
||||
}
|
||||
return getBuffer()
|
||||
}
|
||||
|
||||
type SSRRenderFunction = (
|
||||
context: any,
|
||||
push: (item: any) => void,
|
||||
parentInstance: ComponentInternalInstance
|
||||
) => void
|
||||
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)
|
||||
|
||||
function ssrCompile(
|
||||
template: string,
|
||||
instance: ComponentInternalInstance
|
||||
): SSRRenderFunction {
|
||||
const cached = compileCache[template]
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const { code } = compile(template, {
|
||||
isCustomElement: instance.appContext.config.isCustomElement || NO,
|
||||
isNativeTag: instance.appContext.config.isNativeTag || NO,
|
||||
onError(err: CompilerError) {
|
||||
if (__DEV__) {
|
||||
const message = `[@vue/server-renderer] Template compilation error: ${
|
||||
err.message
|
||||
}`
|
||||
const codeFrame =
|
||||
err.loc &&
|
||||
generateCodeFrame(
|
||||
template as string,
|
||||
err.loc.start.offset,
|
||||
err.loc.end.offset
|
||||
)
|
||||
warn(codeFrame ? `${message}\n${codeFrame}` : message)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
return (compileCache[template] = Function('require', code)(require))
|
||||
}
|
||||
|
||||
function renderVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const { type, shapeFlag, children } = vnode
|
||||
switch (type) {
|
||||
case Text:
|
||||
push(escapeHtml(children as string))
|
||||
break
|
||||
case Comment:
|
||||
push(
|
||||
children ? `<!--${escapeHtmlComment(children as string)}-->` : `<!---->`
|
||||
)
|
||||
break
|
||||
case Static:
|
||||
push(children as string)
|
||||
break
|
||||
case Fragment:
|
||||
push(`<!--[-->`) // open
|
||||
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
|
||||
push(`<!--]-->`) // close
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
renderElementVNode(push, vnode, parentComponent)
|
||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||
push(renderComponentVNode(vnode, parentComponent))
|
||||
} else if (shapeFlag & ShapeFlags.TELEPORT) {
|
||||
renderTeleportVNode(push, vnode, parentComponent)
|
||||
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
renderVNode(
|
||||
push,
|
||||
normalizeSuspenseChildren(vnode).content,
|
||||
parentComponent
|
||||
)
|
||||
} else {
|
||||
warn(
|
||||
'[@vue/server-renderer] Invalid VNode type:',
|
||||
type,
|
||||
`(${typeof type})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function renderVNodeChildren(
|
||||
push: PushFn,
|
||||
children: VNodeArrayChildren,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
renderVNode(push, normalizeVNode(children[i]), parentComponent)
|
||||
}
|
||||
}
|
||||
|
||||
function renderElementVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const tag = vnode.type as string
|
||||
let { props, children, shapeFlag, scopeId, dirs } = vnode
|
||||
let openTag = `<${tag}`
|
||||
|
||||
if (dirs) {
|
||||
props = applySSRDirectives(vnode, props, dirs)
|
||||
}
|
||||
|
||||
if (props) {
|
||||
openTag += ssrRenderAttrs(props, tag)
|
||||
}
|
||||
|
||||
if (scopeId) {
|
||||
openTag += ` ${scopeId}`
|
||||
const treeOwnerId = parentComponent && parentComponent.type.__scopeId
|
||||
// vnode's own scopeId and the current rendering component's scopeId is
|
||||
// different - this is a slot content node.
|
||||
if (treeOwnerId && treeOwnerId !== scopeId) {
|
||||
openTag += ` ${treeOwnerId}-s`
|
||||
}
|
||||
}
|
||||
|
||||
push(openTag + `>`)
|
||||
if (!isVoidTag(tag)) {
|
||||
let hasChildrenOverride = false
|
||||
if (props) {
|
||||
if (props.innerHTML) {
|
||||
hasChildrenOverride = true
|
||||
push(props.innerHTML)
|
||||
} else if (props.textContent) {
|
||||
hasChildrenOverride = true
|
||||
push(escapeHtml(props.textContent))
|
||||
} else if (tag === 'textarea' && props.value) {
|
||||
hasChildrenOverride = true
|
||||
push(escapeHtml(props.value))
|
||||
}
|
||||
}
|
||||
if (!hasChildrenOverride) {
|
||||
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
push(escapeHtml(children as string))
|
||||
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
renderVNodeChildren(
|
||||
push,
|
||||
children as VNodeArrayChildren,
|
||||
parentComponent
|
||||
)
|
||||
}
|
||||
}
|
||||
push(`</${tag}>`)
|
||||
}
|
||||
}
|
||||
|
||||
function applySSRDirectives(
|
||||
vnode: VNode,
|
||||
rawProps: VNodeProps | null,
|
||||
dirs: DirectiveBinding[]
|
||||
): VNodeProps {
|
||||
const toMerge: VNodeProps[] = []
|
||||
for (let i = 0; i < dirs.length; i++) {
|
||||
const binding = dirs[i]
|
||||
const {
|
||||
dir: { getSSRProps }
|
||||
} = binding
|
||||
if (getSSRProps) {
|
||||
const props = getSSRProps(binding, vnode)
|
||||
if (props) toMerge.push(props)
|
||||
}
|
||||
}
|
||||
return mergeProps(rawProps || {}, ...toMerge)
|
||||
}
|
||||
|
||||
function renderTeleportVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const target = vnode.props && vnode.props.to
|
||||
const disabled = vnode.props && vnode.props.disabled
|
||||
if (!target) {
|
||||
warn(`[@vue/server-renderer] Teleport is missing target prop.`)
|
||||
return []
|
||||
}
|
||||
if (!isString(target)) {
|
||||
warn(
|
||||
`[@vue/server-renderer] Teleport target must be a query selector string.`
|
||||
)
|
||||
return []
|
||||
}
|
||||
ssrRenderTeleport(
|
||||
push,
|
||||
push => {
|
||||
renderVNodeChildren(
|
||||
push,
|
||||
vnode.children as VNodeArrayChildren,
|
||||
parentComponent
|
||||
)
|
||||
},
|
||||
target,
|
||||
disabled || disabled === '',
|
||||
parentComponent
|
||||
)
|
||||
return unrollBuffer(buffer as SSRBuffer)
|
||||
}
|
||||
|
||||
async function resolveTeleports(context: SSRContext) {
|
||||
@@ -401,9 +54,9 @@ async function resolveTeleports(context: SSRContext) {
|
||||
for (const key in context.__teleportBuffers) {
|
||||
// note: it's OK to await sequentially here because the Promises were
|
||||
// created eagerly in parallel.
|
||||
context.teleports[key] = unrollBuffer(
|
||||
await Promise.all(context.__teleportBuffers[key])
|
||||
)
|
||||
context.teleports[key] = await unrollBuffer((await Promise.all(
|
||||
context.__teleportBuffers[key]
|
||||
)) as SSRBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user