refactor: fix implementation of SFC :slotted id handling

fix #2892
This commit is contained in:
Evan You
2021-03-05 11:10:06 -05:00
parent cc975c1292
commit aea88c3280
36 changed files with 723 additions and 457 deletions

View File

@@ -2,13 +2,13 @@ import {
createApp,
h,
createCommentVNode,
withScopeId,
resolveComponent,
ComponentOptions,
ref,
defineComponent,
createTextVNode,
createStaticVNode
createStaticVNode,
withCtx
} from 'vue'
import { escapeHtml } from '@vue/shared'
import { renderToString } from '../src/renderToString'
@@ -634,34 +634,32 @@ function testRender(type: string, render: typeof renderToString) {
describe('scopeId', () => {
// note: here we are only testing scopeId handling for vdom serialization.
// compiled srr render functions will include scopeId directly in strings.
const withId = withScopeId('data-v-test')
const withChildId = withScopeId('data-v-child')
test('basic', async () => {
expect(
await render(
withId(() => {
return h('div')
})()
)
).toBe(`<div data-v-test></div>`)
const Foo = {
__scopeId: 'data-v-test',
render() {
return h('div')
}
}
expect(await render(h(Foo))).toBe(`<div data-v-test></div>`)
})
test('with slots', async () => {
const Child = {
__scopeId: 'data-v-child',
render: withChildId(function(this: any) {
render: function(this: any) {
return h('div', this.$slots.default())
})
}
}
const Parent = {
__scopeId: 'data-v-test',
render: withId(() => {
render: () => {
return h(Child, null, {
default: withId(() => h('span', 'slot'))
default: withCtx(() => h('span', 'slot'))
})
})
}
}
expect(await render(h(Parent))).toBe(

View File

@@ -1,11 +1,9 @@
import { createApp, withScopeId } from 'vue'
import { createApp, mergeProps, withCtx } from 'vue'
import { renderToString } from '../src/renderToString'
import { ssrRenderComponent, ssrRenderAttrs, ssrRenderSlot } from '../src'
describe('ssr: scoped id on component root', () => {
test('basic', async () => {
const withParentId = withScopeId('parent')
describe('ssr: scopedId runtime behavior', () => {
test('id on component root', async () => {
const Child = {
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
push(`<div${ssrRenderAttrs(attrs)}></div>`)
@@ -13,19 +11,19 @@ describe('ssr: scoped id on component root', () => {
}
const Comp = {
ssrRender: withParentId((ctx: any, push: any, parent: any) => {
__scopeId: 'parent',
ssrRender: (ctx: any, push: any, parent: any) => {
push(ssrRenderComponent(Child), null, null, parent)
})
}
}
const result = await renderToString(createApp(Comp))
expect(result).toBe(`<div parent></div>`)
})
test('inside slot', async () => {
const withParentId = withScopeId('parent')
test('id and :slotted on component root', async () => {
const Child = {
// <div></div>
ssrRender: (_: any, push: any, _parent: any, attrs: any) => {
push(`<div${ssrRenderAttrs(attrs)} child></div>`)
}
@@ -34,29 +32,126 @@ describe('ssr: scoped id on component root', () => {
const Wrapper = {
__scopeId: 'wrapper',
ssrRender: (ctx: any, push: any, parent: any) => {
ssrRenderSlot(ctx.$slots, 'default', {}, null, push, parent)
// <slot/>
ssrRenderSlot(
ctx.$slots,
'default',
{},
null,
push,
parent,
'wrapper-s'
)
}
}
const Comp = {
ssrRender: withParentId((_: any, push: any, parent: any) => {
__scopeId: 'parent',
ssrRender: (_: any, push: any, parent: any) => {
// <Wrapper><Child/></Wrapper>
push(
ssrRenderComponent(
Wrapper,
null,
{
default: withParentId((_: any, push: any, parent: any) => {
push(ssrRenderComponent(Child, null, null, parent))
}),
default: withCtx(
(_: any, push: any, parent: any, scopeId: string) => {
push(ssrRenderComponent(Child, null, null, parent, scopeId))
}
),
_: 1
} as any,
parent
)
)
})
}
}
const result = await renderToString(createApp(Comp))
expect(result).toBe(`<!--[--><div parent wrapper-s child></div><!--]-->`)
})
// #2892
test(':slotted on forwarded slots', async () => {
const Wrapper = {
__scopeId: 'wrapper',
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
// <div class="wrapper"><slot/></div>
push(
`<div${ssrRenderAttrs(
mergeProps({ class: 'wrapper' }, attrs)
)} wrapper>`
)
ssrRenderSlot(
ctx.$slots,
'default',
{},
null,
push,
parent,
'wrapper-s'
)
push(`</div>`)
}
}
const Slotted = {
__scopeId: 'slotted',
ssrRender: (ctx: any, push: any, parent: any, attrs: any) => {
// <Wrapper><slot/></Wrapper>
push(
ssrRenderComponent(
Wrapper,
attrs,
{
default: withCtx(
(_: any, push: any, parent: any, scopeId: string) => {
ssrRenderSlot(
ctx.$slots,
'default',
{},
null,
push,
parent,
'slotted-s' + scopeId
)
}
),
_: 1
} as any,
parent
)
)
}
}
const Root = {
__scopeId: 'root',
// <Slotted><div></div></Slotted>
ssrRender: (_: any, push: any, parent: any, attrs: any) => {
push(
ssrRenderComponent(
Slotted,
attrs,
{
default: withCtx(
(_: any, push: any, parent: any, scopeId: string) => {
push(`<div root${scopeId}></div>`)
}
),
_: 1
} as any,
parent
)
)
}
}
const result = await renderToString(createApp(Root))
expect(result).toBe(
`<div class="wrapper" root slotted wrapper>` +
`<!--[--><!--[--><div root slotted-s wrapper-s></div><!--]--><!--]-->` +
`</div>`
)
})
})

View File

@@ -6,10 +6,12 @@ export function ssrRenderComponent(
comp: Component,
props: Props | null = null,
children: Slots | SSRSlots | null = null,
parentComponent: ComponentInternalInstance | null = null
parentComponent: ComponentInternalInstance | null = null,
slotScopeId?: string
): SSRBuffer | Promise<SSRBuffer> {
return renderComponentVNode(
createVNode(comp, props, children),
parentComponent
parentComponent,
slotScopeId
)
}

View File

@@ -15,13 +15,13 @@ export function ssrRenderSlot(
slotProps: Props,
fallbackRenderFn: (() => void) | null,
push: PushFn,
parentComponent: ComponentInternalInstance
parentComponent: ComponentInternalInstance,
slotScopeId?: string | null
) {
// template-compiled slots are always rendered as fragments
push(`<!--[-->`)
const slotFn = slots[slotName]
if (slotFn) {
const scopeId = parentComponent && parentComponent.type.__scopeId
const slotBuffer: SSRBufferItem[] = []
const bufferedPush = (item: SSRBufferItem) => {
slotBuffer.push(item)
@@ -30,7 +30,7 @@ export function ssrRenderSlot(
slotProps,
bufferedPush,
parentComponent,
scopeId ? ` ${scopeId}-s` : ``
slotScopeId ? ' ' + slotScopeId : ''
)
if (Array.isArray(ret)) {
// normal slot

View File

@@ -80,7 +80,8 @@ export function createBuffer() {
export function renderComponentVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
parentComponent: ComponentInternalInstance | null = null,
slotScopeId?: string
): SSRBuffer | Promise<SSRBuffer> {
const instance = createComponentInstance(vnode, parentComponent, null)
const res = setupComponent(instance, true /* isSSR */)
@@ -97,14 +98,15 @@ export function renderComponentVNode(
warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err)
})
}
return p.then(() => renderComponentSubTree(instance))
return p.then(() => renderComponentSubTree(instance, slotScopeId))
} else {
return renderComponentSubTree(instance)
return renderComponentSubTree(instance, slotScopeId)
}
}
function renderComponentSubTree(
instance: ComponentInternalInstance
instance: ComponentInternalInstance,
slotScopeId?: string
): SSRBuffer | Promise<SSRBuffer> {
const comp = instance.type as Component
const { getBuffer, push } = createBuffer()
@@ -133,13 +135,10 @@ function renderComponentSubTree(
// inherited scopeId
const scopeId = instance.vnode.scopeId
const treeOwnerId = instance.parent && instance.parent.type.__scopeId
const slotScopeId =
treeOwnerId && treeOwnerId !== scopeId ? treeOwnerId + '-s' : null
if (scopeId || slotScopeId) {
attrs = { ...attrs }
if (scopeId) attrs[scopeId] = ''
if (slotScopeId) attrs[slotScopeId] = ''
if (slotScopeId) attrs[slotScopeId.trim()] = ''
}
// set current rendering instance for asset resolution