]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(ssr): render portals (#714)
authorDmitry Sharshakov <d3dx12.xx@gmail.com>
Sat, 15 Feb 2020 22:41:20 +0000 (01:41 +0300)
committerGitHub <noreply@github.com>
Sat, 15 Feb 2020 22:41:20 +0000 (17:41 -0500)
packages/server-renderer/__tests__/renderToString.spec.ts
packages/server-renderer/src/helpers/ssrRenderSlot.ts
packages/server-renderer/src/renderToString.ts

index ed60dcaeb8d13f397f0cc964b5175d44742f4fcb..2ac929ac1c4a852170166bba23d24bea42a08778 100644 (file)
@@ -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.
index 3caaf4421e543e15c5e3c4724f1dc37573e137e5..d8826a03e1873b6b49ca3ce9b54f28b31be9bf4f 100644 (file)
@@ -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
index 599f6b34510914f7cb0fc5de1b0acfe22832580f..b7bd38d3135f139ff88094e975e213cee62412ab 100644 (file)
@@ -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)
+}