2020-01-27 22:23:42 +00:00
|
|
|
import {
|
|
|
|
App,
|
|
|
|
Component,
|
|
|
|
ComponentInternalInstance,
|
|
|
|
VNode,
|
2020-01-29 03:58:02 +00:00
|
|
|
VNodeArrayChildren,
|
2020-01-27 22:23:42 +00:00
|
|
|
createVNode,
|
2020-01-28 23:48:27 +00:00
|
|
|
Text,
|
|
|
|
Comment,
|
|
|
|
Fragment,
|
2020-01-29 03:58:02 +00:00
|
|
|
ssrUtils,
|
2020-02-10 19:37:35 +00:00
|
|
|
Slots,
|
2020-02-15 22:41:20 +00:00
|
|
|
warn,
|
2020-02-18 18:26:15 +00:00
|
|
|
createApp,
|
|
|
|
ssrContextKey
|
2020-01-27 22:23:42 +00:00
|
|
|
} from 'vue'
|
2020-01-28 23:48:27 +00:00
|
|
|
import {
|
2020-02-14 06:36:42 +00:00
|
|
|
ShapeFlags,
|
2020-01-28 23:48:27 +00:00
|
|
|
isString,
|
|
|
|
isPromise,
|
|
|
|
isArray,
|
|
|
|
isFunction,
|
2020-02-03 23:16:09 +00:00
|
|
|
isVoidTag,
|
2020-02-10 19:37:35 +00:00
|
|
|
escapeHtml,
|
|
|
|
NO,
|
|
|
|
generateCodeFrame
|
2020-01-28 23:48:27 +00:00
|
|
|
} from '@vue/shared'
|
2020-02-10 19:37:35 +00:00
|
|
|
import { compile } from '@vue/compiler-ssr'
|
2020-02-06 17:07:25 +00:00
|
|
|
import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
|
|
|
|
import { SSRSlots } from './helpers/ssrRenderSlot'
|
2020-02-10 19:37:35 +00:00
|
|
|
import { CompilerError } from '@vue/compiler-dom'
|
2020-01-27 22:23:42 +00:00
|
|
|
|
2020-01-29 03:14:43 +00:00
|
|
|
const {
|
2020-01-29 22:36:06 +00:00
|
|
|
isVNode,
|
2020-01-29 03:14:43 +00:00
|
|
|
createComponentInstance,
|
2020-02-06 04:07:23 +00:00
|
|
|
setCurrentRenderingInstance,
|
2020-01-29 03:14:43 +00:00
|
|
|
setupComponent,
|
|
|
|
renderComponentRoot,
|
|
|
|
normalizeVNode
|
|
|
|
} = ssrUtils
|
|
|
|
|
2020-01-28 15:46:13 +00:00
|
|
|
// 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)
|
2020-01-27 23:06:37 +00:00
|
|
|
type SSRBuffer = SSRBufferItem[]
|
2020-01-28 15:46:13 +00:00
|
|
|
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
2020-01-27 23:06:37 +00:00
|
|
|
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
2020-02-15 22:41:20 +00:00
|
|
|
|
2020-02-06 02:04:40 +00:00
|
|
|
export type PushFn = (item: SSRBufferItem) => void
|
2020-02-15 22:41:20 +00:00
|
|
|
|
2020-02-06 02:04:40 +00:00
|
|
|
export type Props = Record<string, unknown>
|
2020-01-27 22:23:42 +00:00
|
|
|
|
2020-02-15 22:41:20 +00:00
|
|
|
export type SSRContext = {
|
|
|
|
[key: string]: any
|
|
|
|
portals?: Record<string, string>
|
|
|
|
__portalBuffers?: Record<
|
|
|
|
string,
|
|
|
|
ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
|
|
|
>
|
|
|
|
}
|
|
|
|
|
2020-01-27 22:23:42 +00:00
|
|
|
function createBuffer() {
|
|
|
|
let appendable = false
|
|
|
|
let hasAsync = false
|
|
|
|
const buffer: SSRBuffer = []
|
|
|
|
return {
|
|
|
|
buffer,
|
|
|
|
hasAsync() {
|
|
|
|
return hasAsync
|
|
|
|
},
|
|
|
|
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 {
|
|
|
|
let ret = ''
|
|
|
|
for (let i = 0; i < buffer.length; i++) {
|
|
|
|
const item = buffer[i]
|
|
|
|
if (isString(item)) {
|
|
|
|
ret += item
|
|
|
|
} else {
|
|
|
|
ret += unrollBuffer(item)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2020-02-15 22:41:20 +00:00
|
|
|
export async function renderToString(
|
|
|
|
input: App | VNode,
|
|
|
|
context: SSRContext = {}
|
|
|
|
): Promise<string> {
|
2020-01-30 17:20:23 +00:00
|
|
|
let buffer: ResolvedSSRBuffer
|
|
|
|
if (isVNode(input)) {
|
2020-02-15 22:41:20 +00:00
|
|
|
// raw vnode, wrap with app (for context)
|
|
|
|
return renderToString(createApp({ render: () => input }), context)
|
2020-01-30 17:20:23 +00:00
|
|
|
} else {
|
|
|
|
// rendering an app
|
|
|
|
const vnode = createVNode(input._component, input._props)
|
|
|
|
vnode.appContext = input._context
|
2020-02-15 22:41:20 +00:00
|
|
|
// provide the ssr context to the tree
|
|
|
|
input.provide(ssrContextKey, context)
|
2020-01-30 17:20:23 +00:00
|
|
|
buffer = await renderComponentVNode(vnode)
|
|
|
|
}
|
2020-02-15 22:41:20 +00:00
|
|
|
|
|
|
|
// resolve portals
|
|
|
|
if (context.__portalBuffers) {
|
|
|
|
context.portals = context.portals || {}
|
|
|
|
for (const key in context.__portalBuffers) {
|
|
|
|
// note: it's OK to await sequentially here because the Promises were
|
|
|
|
// created eagerly in parallel.
|
|
|
|
context.portals[key] = unrollBuffer(await context.__portalBuffers[key])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-30 17:20:23 +00:00
|
|
|
return unrollBuffer(buffer)
|
2020-01-27 22:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function renderComponent(
|
2020-01-29 03:58:02 +00:00
|
|
|
comp: Component,
|
2020-01-30 17:09:50 +00:00
|
|
|
props: Props | null = null,
|
2020-02-02 05:05:27 +00:00
|
|
|
children: Slots | SSRSlots | null = null,
|
2020-01-29 03:58:02 +00:00
|
|
|
parentComponent: ComponentInternalInstance | null = null
|
|
|
|
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
|
|
|
return renderComponentVNode(
|
|
|
|
createVNode(comp, props, children),
|
|
|
|
parentComponent
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderComponentVNode(
|
2020-01-28 23:48:27 +00:00
|
|
|
vnode: VNode,
|
2020-01-27 22:23:42 +00:00
|
|
|
parentComponent: ComponentInternalInstance | null = null
|
2020-01-28 15:46:13 +00:00
|
|
|
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
2020-01-27 22:23:42 +00:00
|
|
|
const instance = createComponentInstance(vnode, parentComponent)
|
2020-01-29 14:49:17 +00:00
|
|
|
const res = setupComponent(
|
|
|
|
instance,
|
2020-01-29 20:10:45 +00:00
|
|
|
null /* parentSuspense (no need to track for SSR) */,
|
2020-01-29 14:49:17 +00:00
|
|
|
true /* isSSR */
|
|
|
|
)
|
2020-01-27 22:23:42 +00:00
|
|
|
if (isPromise(res)) {
|
2020-01-29 03:58:02 +00:00
|
|
|
return res.then(() => renderComponentSubTree(instance))
|
2020-01-27 22:23:42 +00:00
|
|
|
} else {
|
2020-01-29 03:58:02 +00:00
|
|
|
return renderComponentSubTree(instance)
|
2020-01-27 22:23:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-10 19:37:35 +00:00
|
|
|
type SSRRenderFunction = (
|
2020-02-15 22:41:20 +00:00
|
|
|
context: any,
|
2020-02-10 19:37:35 +00:00
|
|
|
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 = `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(code)())
|
|
|
|
}
|
|
|
|
|
2020-01-29 03:58:02 +00:00
|
|
|
function renderComponentSubTree(
|
2020-01-27 22:23:42 +00:00
|
|
|
instance: ComponentInternalInstance
|
2020-01-28 15:46:13 +00:00
|
|
|
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
2020-01-28 23:48:27 +00:00
|
|
|
const comp = instance.type as Component
|
2020-01-27 22:23:42 +00:00
|
|
|
const { buffer, push, hasAsync } = createBuffer()
|
|
|
|
if (isFunction(comp)) {
|
2020-01-28 23:48:27 +00:00
|
|
|
renderVNode(push, renderComponentRoot(instance), instance)
|
2020-01-27 22:23:42 +00:00
|
|
|
} else {
|
2020-02-15 16:11:55 +00:00
|
|
|
if (!instance.render && !comp.ssrRender && isString(comp.template)) {
|
2020-02-10 19:37:35 +00:00
|
|
|
comp.ssrRender = ssrCompile(comp.template, instance)
|
|
|
|
}
|
|
|
|
|
2020-01-27 22:23:42 +00:00
|
|
|
if (comp.ssrRender) {
|
|
|
|
// optimized
|
2020-02-06 23:31:36 +00:00
|
|
|
// set current rendering instance for asset resolution
|
2020-02-06 04:07:23 +00:00
|
|
|
setCurrentRenderingInstance(instance)
|
2020-01-29 21:46:18 +00:00
|
|
|
comp.ssrRender(instance.proxy, push, instance)
|
2020-02-06 04:07:23 +00:00
|
|
|
setCurrentRenderingInstance(null)
|
2020-02-15 16:11:55 +00:00
|
|
|
} else if (instance.render) {
|
2020-01-28 23:48:27 +00:00
|
|
|
renderVNode(push, renderComponentRoot(instance), instance)
|
2020-01-27 22:23:42 +00:00
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`Component ${
|
|
|
|
comp.name ? `${comp.name} ` : ``
|
2020-02-10 19:37:35 +00:00
|
|
|
} is missing template or render function.`
|
2020-01-27 22:23:42 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 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.
|
2020-01-28 15:46:13 +00:00
|
|
|
return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
|
2020-01-27 22:23:42 +00:00
|
|
|
}
|
2020-01-27 23:06:37 +00:00
|
|
|
|
2020-01-29 22:36:06 +00:00
|
|
|
function renderVNode(
|
2020-01-28 23:48:27 +00:00
|
|
|
push: PushFn,
|
|
|
|
vnode: VNode,
|
2020-02-15 22:41:20 +00:00
|
|
|
parentComponent: ComponentInternalInstance
|
2020-01-28 23:48:27 +00:00
|
|
|
) {
|
|
|
|
const { type, shapeFlag, children } = vnode
|
|
|
|
switch (type) {
|
|
|
|
case Text:
|
|
|
|
push(children as string)
|
|
|
|
break
|
|
|
|
case Comment:
|
|
|
|
push(children ? `<!--${children}-->` : `<!---->`)
|
|
|
|
break
|
|
|
|
case Fragment:
|
|
|
|
push(`<!---->`)
|
2020-01-29 03:58:02 +00:00
|
|
|
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
|
2020-01-28 23:48:27 +00:00
|
|
|
push(`<!---->`)
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
if (shapeFlag & ShapeFlags.ELEMENT) {
|
|
|
|
renderElement(push, vnode, parentComponent)
|
|
|
|
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
2020-01-29 03:58:02 +00:00
|
|
|
push(renderComponentVNode(vnode, parentComponent))
|
2020-02-15 16:40:09 +00:00
|
|
|
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
|
|
|
renderPortal(vnode, parentComponent)
|
2020-01-28 23:48:27 +00:00
|
|
|
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
|
|
|
|
// TODO
|
|
|
|
} else {
|
|
|
|
console.warn(
|
|
|
|
'[@vue/server-renderer] Invalid VNode type:',
|
|
|
|
type,
|
|
|
|
`(${typeof type})`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-06 02:04:40 +00:00
|
|
|
export function renderVNodeChildren(
|
2020-01-28 23:48:27 +00:00
|
|
|
push: PushFn,
|
2020-01-29 03:58:02 +00:00
|
|
|
children: VNodeArrayChildren,
|
2020-02-15 22:41:20 +00:00
|
|
|
parentComponent: ComponentInternalInstance
|
2020-01-28 23:48:27 +00:00
|
|
|
) {
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
renderVNode(push, normalizeVNode(children[i]), parentComponent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderElement(
|
|
|
|
push: PushFn,
|
|
|
|
vnode: VNode,
|
2020-02-15 22:41:20 +00:00
|
|
|
parentComponent: ComponentInternalInstance
|
2020-01-28 23:48:27 +00:00
|
|
|
) {
|
|
|
|
const tag = vnode.type as string
|
|
|
|
const { props, children, shapeFlag, scopeId } = vnode
|
|
|
|
let openTag = `<${tag}`
|
|
|
|
|
|
|
|
// TODO directives
|
|
|
|
|
|
|
|
if (props !== null) {
|
2020-02-06 17:07:25 +00:00
|
|
|
openTag += ssrRenderAttrs(props, tag)
|
2020-01-28 23:48:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (scopeId !== null) {
|
|
|
|
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 != null && treeOwnerId !== scopeId) {
|
2020-01-30 02:13:34 +00:00
|
|
|
openTag += ` ${treeOwnerId}-s`
|
2020-01-28 23:48:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
push(openTag + `>`)
|
|
|
|
if (!isVoidTag(tag)) {
|
|
|
|
let hasChildrenOverride = false
|
|
|
|
if (props !== null) {
|
|
|
|
if (props.innerHTML) {
|
|
|
|
hasChildrenOverride = true
|
|
|
|
push(props.innerHTML)
|
|
|
|
} else if (props.textContent) {
|
|
|
|
hasChildrenOverride = true
|
2020-01-29 20:10:45 +00:00
|
|
|
push(escapeHtml(props.textContent))
|
2020-01-28 23:48:27 +00:00
|
|
|
} else if (tag === 'textarea' && props.value) {
|
|
|
|
hasChildrenOverride = true
|
2020-01-29 20:10:45 +00:00
|
|
|
push(escapeHtml(props.value))
|
2020-01-28 23:48:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!hasChildrenOverride) {
|
|
|
|
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
2020-01-29 20:10:45 +00:00
|
|
|
push(escapeHtml(children as string))
|
2020-01-28 23:48:27 +00:00
|
|
|
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
2020-01-29 03:58:02 +00:00
|
|
|
renderVNodeChildren(
|
|
|
|
push,
|
|
|
|
children as VNodeArrayChildren,
|
|
|
|
parentComponent
|
|
|
|
)
|
2020-01-28 23:48:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
push(`</${tag}>`)
|
|
|
|
}
|
2020-01-27 23:06:37 +00:00
|
|
|
}
|
2020-02-15 22:41:20 +00:00
|
|
|
|
|
|
|
function renderPortal(
|
|
|
|
vnode: VNode,
|
|
|
|
parentComponent: ComponentInternalInstance
|
|
|
|
) {
|
|
|
|
const target = vnode.props && vnode.props.target
|
|
|
|
if (!target) {
|
|
|
|
console.warn(`[@vue/server-renderer] Portal is missing target prop.`)
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
if (!isString(target)) {
|
|
|
|
console.warn(
|
|
|
|
`[@vue/server-renderer] Portal target must be a query selector string.`
|
|
|
|
)
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
const { buffer, push, hasAsync } = createBuffer()
|
|
|
|
renderVNodeChildren(
|
|
|
|
push,
|
|
|
|
vnode.children as VNodeArrayChildren,
|
|
|
|
parentComponent
|
|
|
|
)
|
|
|
|
const context = parentComponent.appContext.provides[
|
|
|
|
ssrContextKey as any
|
|
|
|
] as SSRContext
|
|
|
|
const portalBuffers =
|
|
|
|
context.__portalBuffers || (context.__portalBuffers = {})
|
|
|
|
portalBuffers[target] = hasAsync()
|
|
|
|
? Promise.all(buffer)
|
|
|
|
: (buffer as ResolvedSSRBuffer)
|
|
|
|
}
|