feat(ssr): render portals (#714)
This commit is contained in:
parent
aa09f01a1e
commit
e495fa4a18
@ -5,11 +5,16 @@ import {
|
||||
withScopeId,
|
||||
resolveComponent,
|
||||
ComponentOptions,
|
||||
Portal,
|
||||
ref,
|
||||
defineComponent
|
||||
} from 'vue'
|
||||
import { escapeHtml, mockWarn } from '@vue/shared'
|
||||
import { renderToString, renderComponent } from '../src/renderToString'
|
||||
import {
|
||||
renderToString,
|
||||
renderComponent,
|
||||
SSRContext
|
||||
} from '../src/renderToString'
|
||||
import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
|
||||
|
||||
mockWarn()
|
||||
@ -508,6 +513,21 @@ describe('ssr: renderToString', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('portal', async () => {
|
||||
const ctx: SSRContext = {}
|
||||
await renderToString(
|
||||
h(
|
||||
Portal,
|
||||
{
|
||||
target: `#target`
|
||||
},
|
||||
h('span', 'hello')
|
||||
),
|
||||
ctx
|
||||
)
|
||||
expect(ctx.portals!['#target']).toBe('<span>hello</span>')
|
||||
})
|
||||
|
||||
describe('scopeId', () => {
|
||||
// note: here we are only testing scopeId handling for vdom serialization.
|
||||
// compiled srr render functions will include scopeId directly in strings.
|
||||
|
@ -16,7 +16,7 @@ export function ssrRenderSlot(
|
||||
slotProps: Props,
|
||||
fallbackRenderFn: (() => void) | null,
|
||||
push: PushFn,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const slotFn = slots[slotName]
|
||||
// template-compiled slots are always rendered as fragments
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
Portal,
|
||||
ssrUtils,
|
||||
Slots,
|
||||
warn
|
||||
warn,
|
||||
createApp
|
||||
} from 'vue'
|
||||
import {
|
||||
ShapeFlags,
|
||||
@ -47,9 +48,22 @@ const {
|
||||
type SSRBuffer = SSRBufferItem[]
|
||||
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
||||
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
||||
|
||||
export type PushFn = (item: SSRBufferItem) => void
|
||||
|
||||
export type Props = Record<string, unknown>
|
||||
|
||||
const ssrContextKey = Symbol()
|
||||
|
||||
export type SSRContext = {
|
||||
[key: string]: any
|
||||
portals?: Record<string, string>
|
||||
__portalBuffers?: Record<
|
||||
string,
|
||||
ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
||||
>
|
||||
}
|
||||
|
||||
function createBuffer() {
|
||||
let appendable = false
|
||||
let hasAsync = false
|
||||
@ -88,17 +102,33 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function renderToString(input: App | VNode): Promise<string> {
|
||||
export async function renderToString(
|
||||
input: App | VNode,
|
||||
context: SSRContext = {}
|
||||
): Promise<string> {
|
||||
let buffer: ResolvedSSRBuffer
|
||||
if (isVNode(input)) {
|
||||
// raw vnode, wrap with component
|
||||
buffer = await renderComponent({ render: () => input })
|
||||
// raw vnode, wrap with app (for context)
|
||||
return renderToString(createApp({ render: () => input }), context)
|
||||
} else {
|
||||
// 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)
|
||||
buffer = await renderComponentVNode(vnode)
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
}
|
||||
|
||||
return unrollBuffer(buffer)
|
||||
}
|
||||
|
||||
@ -132,7 +162,7 @@ function renderComponentVNode(
|
||||
}
|
||||
|
||||
type SSRRenderFunction = (
|
||||
ctx: any,
|
||||
context: any,
|
||||
push: (item: any) => void,
|
||||
parentInstance: ComponentInternalInstance
|
||||
) => void
|
||||
@ -206,7 +236,7 @@ function renderComponentSubTree(
|
||||
function renderVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const { type, shapeFlag, children } = vnode
|
||||
switch (type) {
|
||||
@ -222,7 +252,7 @@ function renderVNode(
|
||||
push(`<!---->`)
|
||||
break
|
||||
case Portal:
|
||||
// TODO
|
||||
renderPortal(vnode, parentComponent)
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
@ -244,7 +274,7 @@ function renderVNode(
|
||||
export function renderVNodeChildren(
|
||||
push: PushFn,
|
||||
children: VNodeArrayChildren,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
renderVNode(push, normalizeVNode(children[i]), parentComponent)
|
||||
@ -254,7 +284,7 @@ export function renderVNodeChildren(
|
||||
function renderElement(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
parentComponent: ComponentInternalInstance
|
||||
) {
|
||||
const tag = vnode.type as string
|
||||
const { props, children, shapeFlag, scopeId } = vnode
|
||||
@ -305,3 +335,35 @@ function renderElement(
|
||||
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 { 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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user