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

382 lines
10 KiB
TypeScript
Raw Normal View History

2020-01-27 22:23:42 +00:00
import {
App,
Component,
ComponentInternalInstance,
VNode,
VNodeArrayChildren,
2020-01-27 22:23:42 +00:00
createVNode,
2020-01-28 23:48:27 +00:00
Text,
Comment,
Fragment,
ssrUtils,
Slots,
2020-02-18 18:26:15 +00:00
createApp,
ssrContextKey,
warn
2020-01-27 22:23:42 +00:00
} from 'vue'
2020-01-28 23:48:27 +00:00
import {
ShapeFlags,
2020-01-28 23:48:27 +00:00
isString,
isPromise,
isArray,
isFunction,
isVoidTag,
escapeHtml,
NO,
generateCodeFrame
2020-01-28 23:48:27 +00:00
} from '@vue/shared'
import { compile } from '@vue/compiler-ssr'
2020-02-06 17:07:25 +00:00
import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
import { SSRSlots } from './helpers/ssrRenderSlot'
import { CompilerError } from '@vue/compiler-dom'
2020-01-27 22:23:42 +00:00
const {
isVNode,
createComponentInstance,
2020-02-06 04:07:23 +00:00
setCurrentRenderingInstance,
setupComponent,
renderComponentRoot,
normalizeVNode,
normalizeSuspenseChildren
} = 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)
export type SSRBuffer = SSRBufferItem[]
export type SSRBufferItem =
| string
| ResolvedSSRBuffer
| Promise<ResolvedSSRBuffer>
export 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>
>
}
export function createBuffer() {
2020-01-27 22:23:42 +00:00
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)
2020-01-27 22:23:42 +00:00
},
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> {
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-02-15 22:41:20 +00:00
// 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 buffer = await renderComponentVNode(vnode)
await resolvePortals(context)
2020-02-15 22:41:20 +00:00
return unrollBuffer(buffer)
2020-01-27 22:23:42 +00:00
}
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(
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)
const res = setupComponent(
instance,
null /* parentSuspense (no need to track for SSR) */,
true /* isSSR */
)
2020-01-27 22:23:42 +00:00
if (isPromise(res)) {
return res
.catch(err => {
warn(`[@vue/server-renderer]: Uncaught error in async setup:\n`, err)
})
.then(() => renderComponentSubTree(instance))
2020-01-27 22:23:42 +00:00
} else {
return renderComponentSubTree(instance)
2020-01-27 22:23:42 +00:00
}
}
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 = (
2020-02-15 22:41:20 +00:00
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))
2020-01-27 22:23:42 +00:00
}
2020-01-27 23:06:37 +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(`<!--[-->`) // open
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
push(`<!--]-->`) // close
2020-01-28 23:48:27 +00:00
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
renderElementVNode(push, vnode, parentComponent)
2020-01-28 23:48:27 +00:00
} else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent))
2020-02-15 16:40:09 +00:00
} else if (shapeFlag & ShapeFlags.PORTAL) {
renderPortalVNode(vnode, parentComponent)
2020-01-28 23:48:27 +00:00
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
renderVNode(
push,
normalizeSuspenseChildren(vnode).content,
parentComponent
)
2020-01-28 23:48:27 +00:00
} else {
warn(
2020-01-28 23:48:27 +00:00
'[@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,
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 renderElementVNode(
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 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) {
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
push(escapeHtml(props.textContent))
2020-01-28 23:48:27 +00:00
} else if (tag === 'textarea' && props.value) {
hasChildrenOverride = true
push(escapeHtml(props.value))
2020-01-28 23:48:27 +00:00
}
}
if (!hasChildrenOverride) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
push(escapeHtml(children as string))
2020-01-28 23:48:27 +00:00
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
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 renderPortalVNode(
2020-02-15 22:41:20 +00:00
vnode: VNode,
parentComponent: ComponentInternalInstance
) {
const target = vnode.props && vnode.props.target
if (!target) {
warn(`[@vue/server-renderer] Portal is missing target prop.`)
2020-02-15 22:41:20 +00:00
return []
}
if (!isString(target)) {
warn(
2020-02-15 22:41:20 +00:00
`[@vue/server-renderer] Portal target must be a query selector string.`
)
return []
}
const { getBuffer, push } = createBuffer()
2020-02-15 22:41:20 +00:00
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] = getBuffer()
}
async function resolvePortals(context: SSRContext) {
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-02-15 22:41:20 +00:00
}