vue3-yuanma/packages/server-renderer/src/renderToString.ts

284 lines
7.3 KiB
TypeScript

import {
App,
Component,
ComponentInternalInstance,
VNode,
VNodeArrayChildren,
createVNode,
Text,
Comment,
Fragment,
Portal,
ShapeFlags,
ssrUtils,
Slot,
Slots
} from 'vue'
import {
isString,
isPromise,
isArray,
isFunction,
isVoidTag,
escapeHtml
} from '@vue/shared'
import { renderAttrs } from './helpers/renderAttrs'
const {
isVNode,
createComponentInstance,
setupComponent,
renderComponentRoot,
normalizeVNode
} = 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)
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
type PushFn = (item: SSRBufferItem) => void
type Props = Record<string, unknown>
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
}
export async function renderToString(input: App | VNode): Promise<string> {
let buffer: ResolvedSSRBuffer
if (isVNode(input)) {
// raw vnode, wrap with component
buffer = await renderComponent({ render: () => input })
} else {
// rendering an app
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
buffer = await renderComponentVNode(vnode)
}
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)
const res = setupComponent(
instance,
null /* parentSuspense (no need to track for SSR) */,
true /* isSSR */
)
if (isPromise(res)) {
return res.then(() => renderComponentSubTree(instance))
} else {
return renderComponentSubTree(instance)
}
}
function renderComponentSubTree(
instance: ComponentInternalInstance
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
const comp = instance.type as Component
const { buffer, push, hasAsync } = createBuffer()
if (isFunction(comp)) {
renderVNode(push, renderComponentRoot(instance), instance)
} else {
if (comp.ssrRender) {
// optimized
comp.ssrRender(instance.proxy, push, instance)
} else if (comp.render) {
renderVNode(push, renderComponentRoot(instance), instance)
} else {
// TODO on the fly template compilation support
throw new Error(
`Component ${
comp.name ? `${comp.name} ` : ``
} is missing render function.`
)
}
}
// 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)
}
function renderVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
) {
const { type, shapeFlag, children } = vnode
switch (type) {
case Text:
push(children as string)
break
case Comment:
push(children ? `<!--${children}-->` : `<!---->`)
break
case Fragment:
push(`<!---->`)
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
push(`<!---->`)
break
case Portal:
// TODO
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
renderElement(push, vnode, parentComponent)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent))
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
// TODO
} else {
console.warn(
'[@vue/server-renderer] Invalid VNode type:',
type,
`(${typeof type})`
)
}
}
}
function renderVNodeChildren(
push: PushFn,
children: VNodeArrayChildren,
parentComponent: ComponentInternalInstance | null = null
) {
for (let i = 0; i < children.length; i++) {
renderVNode(push, normalizeVNode(children[i]), parentComponent)
}
}
function renderElement(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
) {
const tag = vnode.type as string
const { props, children, shapeFlag, scopeId } = vnode
let openTag = `<${tag}`
// TODO directives
if (props !== null) {
openTag += renderAttrs(props, tag)
}
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) {
openTag += ` ${treeOwnerId}-s`
}
}
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
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}>`)
}
}
export type SSRSlots = Record<string, SSRSlot>
export type SSRSlot = (
props: Props,
push: PushFn,
parentComponent: ComponentInternalInstance | null
) => void
export function renderSlot(
slotFn: Slot | SSRSlot,
slotProps: Props,
push: PushFn,
parentComponent: ComponentInternalInstance | null = null
) {
// template-compiled slots are always rendered as fragments
push(`<!---->`)
if (slotFn.length > 1) {
// only ssr-optimized slot fns accept more than 1 arguments
slotFn(slotProps, push, parentComponent)
} else {
// normal slot
renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)
}
push(`<!---->`)
}