| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- import {
- App,
- Component,
- ComponentInternalInstance,
- VNode,
- VNodeArrayChildren,
- createVNode,
- Text,
- Comment,
- Fragment,
- ssrUtils,
- Slots,
- warn,
- createApp,
- ssrContextKey
- } from 'vue'
- import {
- ShapeFlags,
- isString,
- isPromise,
- isArray,
- isFunction,
- isVoidTag,
- escapeHtml,
- NO,
- generateCodeFrame
- } 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'
- const {
- isVNode,
- createComponentInstance,
- setCurrentRenderingInstance,
- 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)
- 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
- portals?: Record<string, string>
- __portalBuffers?: Record<
- string,
- ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
- >
- }
- 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 {
- 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,
- context: SSRContext = {}
- ): Promise<string> {
- if (isVNode(input)) {
- // raw vnode, wrap with app (for context)
- return renderToString(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 buffer = await renderComponentVNode(vnode)
- await resolvePortals(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)
- 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)
- }
- }
- 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 = `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)())
- }
- 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 {
- throw new Error(
- `Component ${
- comp.name ? `${comp.name} ` : ``
- } is missing template or render function.`
- )
- }
- }
- return getBuffer()
- }
- function renderVNode(
- push: PushFn,
- vnode: VNode,
- parentComponent: ComponentInternalInstance
- ) {
- const { type, shapeFlag, children } = vnode
- switch (type) {
- case Text:
- push(children as string)
- break
- case Comment:
- push(children ? `<!--${children}-->` : `<!---->`)
- break
- case Fragment:
- renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
- break
- default:
- if (shapeFlag & ShapeFlags.ELEMENT) {
- renderElement(push, vnode, parentComponent)
- } else if (shapeFlag & ShapeFlags.COMPONENT) {
- push(renderComponentVNode(vnode, parentComponent))
- } else if (shapeFlag & ShapeFlags.PORTAL) {
- renderPortal(vnode, parentComponent)
- } else if (shapeFlag & ShapeFlags.SUSPENSE) {
- // TODO
- } else {
- console.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 renderElement(
- push: PushFn,
- vnode: VNode,
- parentComponent: ComponentInternalInstance
- ) {
- const tag = vnode.type as string
- const { props, children, shapeFlag, scopeId } = vnode
- let openTag = `<${tag}`
- // TODO directives
- if (props !== null) {
- openTag += ssrRenderAttrs(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}>`)
- }
- }
- 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 { getBuffer, push } = 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] = 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])
- }
- }
- }
|