feat(ssr): render portals (#714)

This commit is contained in:
Dmitry Sharshakov 2020-02-16 01:41:20 +03:00 committed by GitHub
parent aa09f01a1e
commit e495fa4a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 93 additions and 11 deletions

View File

@ -5,11 +5,16 @@ import {
withScopeId, withScopeId,
resolveComponent, resolveComponent,
ComponentOptions, ComponentOptions,
Portal,
ref, ref,
defineComponent defineComponent
} from 'vue' } from 'vue'
import { escapeHtml, mockWarn } from '@vue/shared' 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' import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
mockWarn() 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', () => { describe('scopeId', () => {
// note: here we are only testing scopeId handling for vdom serialization. // note: here we are only testing scopeId handling for vdom serialization.
// compiled srr render functions will include scopeId directly in strings. // compiled srr render functions will include scopeId directly in strings.

View File

@ -16,7 +16,7 @@ export function ssrRenderSlot(
slotProps: Props, slotProps: Props,
fallbackRenderFn: (() => void) | null, fallbackRenderFn: (() => void) | null,
push: PushFn, push: PushFn,
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance
) { ) {
const slotFn = slots[slotName] const slotFn = slots[slotName]
// template-compiled slots are always rendered as fragments // template-compiled slots are always rendered as fragments

View File

@ -11,7 +11,8 @@ import {
Portal, Portal,
ssrUtils, ssrUtils,
Slots, Slots,
warn warn,
createApp
} from 'vue' } from 'vue'
import { import {
ShapeFlags, ShapeFlags,
@ -47,9 +48,22 @@ const {
type SSRBuffer = SSRBufferItem[] type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[] type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
export type PushFn = (item: SSRBufferItem) => void export type PushFn = (item: SSRBufferItem) => void
export type Props = Record<string, unknown> 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() { function createBuffer() {
let appendable = false let appendable = false
let hasAsync = false let hasAsync = false
@ -88,17 +102,33 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
return ret 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 let buffer: ResolvedSSRBuffer
if (isVNode(input)) { if (isVNode(input)) {
// raw vnode, wrap with component // raw vnode, wrap with app (for context)
buffer = await renderComponent({ render: () => input }) return renderToString(createApp({ render: () => input }), context)
} else { } else {
// rendering an app // rendering an app
const vnode = createVNode(input._component, input._props) const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context vnode.appContext = input._context
// provide the ssr context to the tree
input.provide(ssrContextKey, context)
buffer = await renderComponentVNode(vnode) 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) return unrollBuffer(buffer)
} }
@ -132,7 +162,7 @@ function renderComponentVNode(
} }
type SSRRenderFunction = ( type SSRRenderFunction = (
ctx: any, context: any,
push: (item: any) => void, push: (item: any) => void,
parentInstance: ComponentInternalInstance parentInstance: ComponentInternalInstance
) => void ) => void
@ -206,7 +236,7 @@ function renderComponentSubTree(
function renderVNode( function renderVNode(
push: PushFn, push: PushFn,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance
) { ) {
const { type, shapeFlag, children } = vnode const { type, shapeFlag, children } = vnode
switch (type) { switch (type) {
@ -222,7 +252,7 @@ function renderVNode(
push(`<!---->`) push(`<!---->`)
break break
case Portal: case Portal:
// TODO renderPortal(vnode, parentComponent)
break break
default: default:
if (shapeFlag & ShapeFlags.ELEMENT) { if (shapeFlag & ShapeFlags.ELEMENT) {
@ -244,7 +274,7 @@ function renderVNode(
export function renderVNodeChildren( export function renderVNodeChildren(
push: PushFn, push: PushFn,
children: VNodeArrayChildren, children: VNodeArrayChildren,
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance
) { ) {
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
renderVNode(push, normalizeVNode(children[i]), parentComponent) renderVNode(push, normalizeVNode(children[i]), parentComponent)
@ -254,7 +284,7 @@ export function renderVNodeChildren(
function renderElement( function renderElement(
push: PushFn, push: PushFn,
vnode: VNode, vnode: VNode,
parentComponent: ComponentInternalInstance | null = null parentComponent: ComponentInternalInstance
) { ) {
const tag = vnode.type as string const tag = vnode.type as string
const { props, children, shapeFlag, scopeId } = vnode const { props, children, shapeFlag, scopeId } = vnode
@ -305,3 +335,35 @@ function renderElement(
push(`</${tag}>`) 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)
}