feat(ssr/suspense): suspense hydration
In order to support hydration of async components, server-rendered fragments must be explicitly marked with comment nodes.
This commit is contained in:
parent
b3d7d64931
commit
a3cc970030
@ -98,7 +98,7 @@ describe('SSR hydration', () => {
|
|||||||
const msg = ref('foo')
|
const msg = ref('foo')
|
||||||
const fn = jest.fn()
|
const fn = jest.fn()
|
||||||
const { vnode, container } = mountWithHydration(
|
const { vnode, container } = mountWithHydration(
|
||||||
'<div><span>foo</span><span class="foo"></span></div>',
|
'<div><!----><span>foo</span><!----><span class="foo"></span><!----><!----></div>',
|
||||||
() =>
|
() =>
|
||||||
h('div', [
|
h('div', [
|
||||||
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
|
[h('span', msg.value), [h('span', { class: msg.value, onClick: fn })]]
|
||||||
@ -136,7 +136,9 @@ describe('SSR hydration', () => {
|
|||||||
|
|
||||||
msg.value = 'bar'
|
msg.value = 'bar'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
|
expect(vnode.el.innerHTML).toBe(
|
||||||
|
`<!----><span>bar</span><!----><span class="bar"></span><!----><!---->`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('portal', async () => {
|
test('portal', async () => {
|
||||||
|
@ -219,11 +219,11 @@ describe('ssr: components', () => {
|
|||||||
foo: ({ list }, _push, _parent, _scopeId) => {
|
foo: ({ list }, _push, _parent, _scopeId) => {
|
||||||
if (_push) {
|
if (_push) {
|
||||||
if (_ctx.ok) {
|
if (_ctx.ok) {
|
||||||
_push(\`<div\${_scopeId}>\`)
|
_push(\`<div\${_scopeId}><!--1-->\`)
|
||||||
_ssrRenderList(list, (i) => {
|
_ssrRenderList(list, (i) => {
|
||||||
_push(\`<span\${_scopeId}></span>\`)
|
_push(\`<span\${_scopeId}></span>\`)
|
||||||
})
|
})
|
||||||
_push(\`</div>\`)
|
_push(\`<!--0--></div>\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
@ -242,11 +242,11 @@ describe('ssr: components', () => {
|
|||||||
bar: ({ ok }, _push, _parent, _scopeId) => {
|
bar: ({ ok }, _push, _parent, _scopeId) => {
|
||||||
if (_push) {
|
if (_push) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
_push(\`<div\${_scopeId}>\`)
|
_push(\`<div\${_scopeId}><!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<span\${_scopeId}></span>\`)
|
_push(\`<span\${_scopeId}></span>\`)
|
||||||
})
|
})
|
||||||
_push(\`</div>\`)
|
_push(\`<!--0--></div>\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
@ -281,7 +281,7 @@ describe('ssr: components', () => {
|
|||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"
|
"
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<!--1--><div></div><!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -6,9 +6,11 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -19,9 +21,11 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div>foo<span>bar</span></div>\`)
|
_push(\`<div>foo<span>bar</span></div>\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -37,8 +41,9 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (row, i) => {
|
_ssrRenderList(_ctx.list, (row, i) => {
|
||||||
_push(\`<div>\`)
|
_push(\`<div><!--1-->\`)
|
||||||
_ssrRenderList(row, (j) => {
|
_ssrRenderList(row, (j) => {
|
||||||
_push(\`<div>\${
|
_push(\`<div>\${
|
||||||
_ssrInterpolate(i)
|
_ssrInterpolate(i)
|
||||||
@ -46,8 +51,9 @@ describe('ssr: v-for', () => {
|
|||||||
_ssrInterpolate(j)
|
_ssrInterpolate(j)
|
||||||
}</div>\`)
|
}</div>\`)
|
||||||
})
|
})
|
||||||
_push(\`</div>\`)
|
_push(\`<!--0--></div>\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -58,9 +64,11 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`\${_ssrInterpolate(i)}\`)
|
_push(\`<!--1-->\${_ssrInterpolate(i)}<!--0-->\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -73,9 +81,11 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<span>\${_ssrInterpolate(i)}</span>\`)
|
_push(\`<span>\${_ssrInterpolate(i)}</span>\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -89,13 +99,15 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<span>\${
|
_push(\`<!--1--><span>\${
|
||||||
_ssrInterpolate(i)
|
_ssrInterpolate(i)
|
||||||
}</span><span>\${
|
}</span><span>\${
|
||||||
_ssrInterpolate(i + 1)
|
_ssrInterpolate(i + 1)
|
||||||
}</span>\`)
|
}</span><!--0-->\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -111,9 +123,11 @@ describe('ssr: v-for', () => {
|
|||||||
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
"const { ssrInterpolate: _ssrInterpolate, ssrRenderList: _ssrRenderList } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, ({ foo }, index) => {
|
_ssrRenderList(_ctx.list, ({ foo }, index) => {
|
||||||
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
|
_push(\`<div>\${_ssrInterpolate(foo + _ctx.bar + index)}</div>\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
@ -80,7 +80,7 @@ describe('ssr: v-if', () => {
|
|||||||
"
|
"
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`hello\`)
|
_push(\`<!--1-->hello<!--0-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ describe('ssr: v-if', () => {
|
|||||||
"
|
"
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div>hi</div><div>ho</div>\`)
|
_push(\`<!--1--><div>hi</div><div>ho</div><!--0-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
@ -126,9 +126,11 @@ describe('ssr: v-if', () => {
|
|||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
|
_push(\`<!--1-->\`)
|
||||||
_ssrRenderList(_ctx.list, (i) => {
|
_ssrRenderList(_ctx.list, (i) => {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
})
|
})
|
||||||
|
_push(\`<!--0-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<!---->\`)
|
_push(\`<!---->\`)
|
||||||
}
|
}
|
||||||
@ -145,7 +147,7 @@ describe('ssr: v-if', () => {
|
|||||||
"
|
"
|
||||||
return function ssrRender(_ctx, _push, _parent) {
|
return function ssrRender(_ctx, _push, _parent) {
|
||||||
if (_ctx.foo) {
|
if (_ctx.foo) {
|
||||||
_push(\`<div>hi</div><div>ho</div>\`)
|
_push(\`<!--1--><div>hi</div><div>ho</div><!--0-->\`)
|
||||||
} else {
|
} else {
|
||||||
_push(\`<div></div>\`)
|
_push(\`<div></div>\`)
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ import {
|
|||||||
createBlockStatement,
|
createBlockStatement,
|
||||||
CompilerOptions,
|
CompilerOptions,
|
||||||
IfStatement,
|
IfStatement,
|
||||||
CallExpression
|
CallExpression,
|
||||||
|
isText
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { isString, escapeHtml } from '@vue/shared'
|
import { isString, escapeHtml } from '@vue/shared'
|
||||||
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
|
||||||
@ -28,7 +29,9 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
|
|||||||
|
|
||||||
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
|
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
|
||||||
const context = createSSRTransformContext(ast, options)
|
const context = createSSRTransformContext(ast, options)
|
||||||
processChildren(ast.children, context)
|
const isFragment =
|
||||||
|
ast.children.length > 1 && ast.children.some(c => !isText(c))
|
||||||
|
processChildren(ast.children, context, isFragment)
|
||||||
ast.codegenNode = createBlockStatement(context.body)
|
ast.codegenNode = createBlockStatement(context.body)
|
||||||
|
|
||||||
// Finalize helpers.
|
// Finalize helpers.
|
||||||
@ -104,8 +107,12 @@ function createChildContext(
|
|||||||
|
|
||||||
export function processChildren(
|
export function processChildren(
|
||||||
children: TemplateChildNode[],
|
children: TemplateChildNode[],
|
||||||
context: SSRTransformContext
|
context: SSRTransformContext,
|
||||||
|
asFragment = false
|
||||||
) {
|
) {
|
||||||
|
if (asFragment) {
|
||||||
|
context.pushStringPart(`<!--1-->`)
|
||||||
|
}
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
const child = children[i]
|
const child = children[i]
|
||||||
if (child.type === NodeTypes.ELEMENT) {
|
if (child.type === NodeTypes.ELEMENT) {
|
||||||
@ -128,14 +135,18 @@ export function processChildren(
|
|||||||
ssrProcessFor(child, context)
|
ssrProcessFor(child, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (asFragment) {
|
||||||
|
context.pushStringPart(`<!--0-->`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processChildrenAsStatement(
|
export function processChildrenAsStatement(
|
||||||
children: TemplateChildNode[],
|
children: TemplateChildNode[],
|
||||||
parentContext: SSRTransformContext,
|
parentContext: SSRTransformContext,
|
||||||
|
asFragment = false,
|
||||||
withSlotScopeId = parentContext.withSlotScopeId
|
withSlotScopeId = parentContext.withSlotScopeId
|
||||||
): BlockStatement {
|
): BlockStatement {
|
||||||
const childContext = createChildContext(parentContext, withSlotScopeId)
|
const childContext = createChildContext(parentContext, withSlotScopeId)
|
||||||
processChildren(children, childContext)
|
processChildren(children, childContext, asFragment)
|
||||||
return createBlockStatement(childContext.body)
|
return createBlockStatement(childContext.body)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,8 @@ import {
|
|||||||
traverseNode,
|
traverseNode,
|
||||||
ExpressionNode,
|
ExpressionNode,
|
||||||
TemplateNode,
|
TemplateNode,
|
||||||
SUSPENSE
|
SUSPENSE,
|
||||||
|
TRANSITION_GROUP
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
|
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
|
||||||
import {
|
import {
|
||||||
@ -151,7 +152,7 @@ export function ssrProcessComponent(
|
|||||||
return ssrProcessSuspense(node, context)
|
return ssrProcessSuspense(node, context)
|
||||||
} else {
|
} else {
|
||||||
// real fall-through (e.g. KeepAlive): just render its children.
|
// real fall-through (e.g. KeepAlive): just render its children.
|
||||||
processChildren(node.children, context)
|
processChildren(node.children, context, component === TRANSITION_GROUP)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// finish up slot function expressions from the 1st pass.
|
// finish up slot function expressions from the 1st pass.
|
||||||
@ -167,6 +168,7 @@ export function ssrProcessComponent(
|
|||||||
processChildrenAsStatement(
|
processChildrenAsStatement(
|
||||||
children,
|
children,
|
||||||
context,
|
context,
|
||||||
|
false,
|
||||||
true /* withSlotScopeId */
|
true /* withSlotScopeId */
|
||||||
),
|
),
|
||||||
vnodeBranch
|
vnodeBranch
|
||||||
|
@ -4,7 +4,8 @@ import {
|
|||||||
processFor,
|
processFor,
|
||||||
createCallExpression,
|
createCallExpression,
|
||||||
createFunctionExpression,
|
createFunctionExpression,
|
||||||
createForLoopParams
|
createForLoopParams,
|
||||||
|
NodeTypes
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import {
|
import {
|
||||||
SSRTransformContext,
|
SSRTransformContext,
|
||||||
@ -21,14 +22,23 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
|
|||||||
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
// This is called during the 2nd transform pass to construct the SSR-sepcific
|
||||||
// codegen nodes.
|
// codegen nodes.
|
||||||
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
|
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
|
||||||
|
const needFragmentWrapper =
|
||||||
|
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
|
||||||
const renderLoop = createFunctionExpression(
|
const renderLoop = createFunctionExpression(
|
||||||
createForLoopParams(node.parseResult)
|
createForLoopParams(node.parseResult)
|
||||||
)
|
)
|
||||||
renderLoop.body = processChildrenAsStatement(node.children, context)
|
renderLoop.body = processChildrenAsStatement(
|
||||||
|
node.children,
|
||||||
|
context,
|
||||||
|
needFragmentWrapper
|
||||||
|
)
|
||||||
|
// v-for always renders a fragment
|
||||||
|
context.pushStringPart(`<!--1-->`)
|
||||||
context.pushStatement(
|
context.pushStatement(
|
||||||
createCallExpression(context.helper(SSR_RENDER_LIST), [
|
createCallExpression(context.helper(SSR_RENDER_LIST), [
|
||||||
node.source,
|
node.source,
|
||||||
renderLoop
|
renderLoop
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
context.pushStringPart(`<!--0-->`)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ import {
|
|||||||
IfNode,
|
IfNode,
|
||||||
createIfStatement,
|
createIfStatement,
|
||||||
createBlockStatement,
|
createBlockStatement,
|
||||||
createCallExpression
|
createCallExpression,
|
||||||
|
IfBranchNode,
|
||||||
|
BlockStatement,
|
||||||
|
NodeTypes
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import {
|
import {
|
||||||
SSRTransformContext,
|
SSRTransformContext,
|
||||||
@ -23,17 +26,14 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
|
|||||||
const [rootBranch] = node.branches
|
const [rootBranch] = node.branches
|
||||||
const ifStatement = createIfStatement(
|
const ifStatement = createIfStatement(
|
||||||
rootBranch.condition!,
|
rootBranch.condition!,
|
||||||
processChildrenAsStatement(rootBranch.children, context)
|
processIfBranch(rootBranch, context)
|
||||||
)
|
)
|
||||||
context.pushStatement(ifStatement)
|
context.pushStatement(ifStatement)
|
||||||
|
|
||||||
let currentIf = ifStatement
|
let currentIf = ifStatement
|
||||||
for (let i = 1; i < node.branches.length; i++) {
|
for (let i = 1; i < node.branches.length; i++) {
|
||||||
const branch = node.branches[i]
|
const branch = node.branches[i]
|
||||||
const branchBlockStatement = processChildrenAsStatement(
|
const branchBlockStatement = processIfBranch(branch, context)
|
||||||
branch.children,
|
|
||||||
context
|
|
||||||
)
|
|
||||||
if (branch.condition) {
|
if (branch.condition) {
|
||||||
// else-if
|
// else-if
|
||||||
currentIf = currentIf.alternate = createIfStatement(
|
currentIf = currentIf.alternate = createIfStatement(
|
||||||
@ -52,3 +52,15 @@ export function ssrProcessIf(node: IfNode, context: SSRTransformContext) {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processIfBranch(
|
||||||
|
branch: IfBranchNode,
|
||||||
|
context: SSRTransformContext
|
||||||
|
): BlockStatement {
|
||||||
|
const { children } = branch
|
||||||
|
const needFragmentWrapper =
|
||||||
|
(children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
|
||||||
|
// optimize away nested fragments when the only child is a ForNode
|
||||||
|
!(children.length === 1 && children[0].type === NodeTypes.FOR)
|
||||||
|
return processChildrenAsStatement(children, context, needFragmentWrapper)
|
||||||
|
}
|
||||||
|
@ -144,7 +144,6 @@ export interface ComponentInternalInstance {
|
|||||||
|
|
||||||
// suspense related
|
// suspense related
|
||||||
asyncDep: Promise<any> | null
|
asyncDep: Promise<any> | null
|
||||||
asyncResult: unknown
|
|
||||||
asyncResolved: boolean
|
asyncResolved: boolean
|
||||||
|
|
||||||
// storage for any extra properties
|
// storage for any extra properties
|
||||||
@ -215,7 +214,6 @@ export function createComponentInstance(
|
|||||||
|
|
||||||
// async dependency management
|
// async dependency management
|
||||||
asyncDep: null,
|
asyncDep: null,
|
||||||
asyncResult: null,
|
|
||||||
asyncResolved: false,
|
asyncResolved: false,
|
||||||
|
|
||||||
// user namespace for storing whatever the user assigns to `this`
|
// user namespace for storing whatever the user assigns to `this`
|
||||||
@ -367,7 +365,7 @@ function setupStatefulComponent(
|
|||||||
if (isPromise(setupResult)) {
|
if (isPromise(setupResult)) {
|
||||||
if (isSSR) {
|
if (isSSR) {
|
||||||
// return the promise so server-renderer can wait on it
|
// return the promise so server-renderer can wait on it
|
||||||
return setupResult.then(resolvedResult => {
|
return setupResult.then((resolvedResult: unknown) => {
|
||||||
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
|
handleSetupResult(instance, resolvedResult, parentSuspense, isSSR)
|
||||||
})
|
})
|
||||||
} else if (__FEATURE_SUSPENSE__) {
|
} else if (__FEATURE_SUSPENSE__) {
|
||||||
|
@ -5,8 +5,8 @@ import { Slots } from '../componentSlots'
|
|||||||
import { RendererInternals, MoveType, SetupRenderEffectFn } from '../renderer'
|
import { RendererInternals, MoveType, SetupRenderEffectFn } from '../renderer'
|
||||||
import { queuePostFlushCb, queueJob } from '../scheduler'
|
import { queuePostFlushCb, queueJob } from '../scheduler'
|
||||||
import { updateHOCHostEl } from '../componentRenderUtils'
|
import { updateHOCHostEl } from '../componentRenderUtils'
|
||||||
import { handleError, ErrorCodes } from '../errorHandling'
|
|
||||||
import { pushWarningContext, popWarningContext } from '../warning'
|
import { pushWarningContext, popWarningContext } from '../warning'
|
||||||
|
import { handleError, ErrorCodes } from '../errorHandling'
|
||||||
|
|
||||||
export interface SuspenseProps {
|
export interface SuspenseProps {
|
||||||
onResolve?: () => void
|
onResolve?: () => void
|
||||||
@ -59,7 +59,8 @@ export const SuspenseImpl = {
|
|||||||
rendererInternals
|
rendererInternals
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
hydrate: hydrateSuspense
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force-casted public typing for h and TSX props inference
|
// Force-casted public typing for h and TSX props inference
|
||||||
@ -97,14 +98,10 @@ function mountSuspense(
|
|||||||
rendererInternals
|
rendererInternals
|
||||||
))
|
))
|
||||||
|
|
||||||
const { content, fallback } = normalizeSuspenseChildren(n2)
|
|
||||||
suspense.subTree = content
|
|
||||||
suspense.fallbackTree = fallback
|
|
||||||
|
|
||||||
// start mounting the content subtree in an off-dom container
|
// start mounting the content subtree in an off-dom container
|
||||||
patch(
|
patch(
|
||||||
null,
|
null,
|
||||||
content,
|
suspense.subTree,
|
||||||
hiddenContainer,
|
hiddenContainer,
|
||||||
null,
|
null,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
@ -117,7 +114,7 @@ function mountSuspense(
|
|||||||
// mount the fallback tree
|
// mount the fallback tree
|
||||||
patch(
|
patch(
|
||||||
null,
|
null,
|
||||||
fallback,
|
suspense.fallbackTree,
|
||||||
container,
|
container,
|
||||||
anchor,
|
anchor,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
@ -125,7 +122,7 @@ function mountSuspense(
|
|||||||
isSVG,
|
isSVG,
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
n2.el = fallback.el
|
n2.el = suspense.fallbackTree.el
|
||||||
} else {
|
} else {
|
||||||
// Suspense has no async deps. Just resolve.
|
// Suspense has no async deps. Just resolve.
|
||||||
suspense.resolve()
|
suspense.resolve()
|
||||||
@ -209,6 +206,7 @@ export interface SuspenseBoundary<
|
|||||||
subTree: HostVNode
|
subTree: HostVNode
|
||||||
fallbackTree: HostVNode
|
fallbackTree: HostVNode
|
||||||
deps: number
|
deps: number
|
||||||
|
isHydrating: boolean
|
||||||
isResolved: boolean
|
isResolved: boolean
|
||||||
isUnmounted: boolean
|
isUnmounted: boolean
|
||||||
effects: Function[]
|
effects: Function[]
|
||||||
@ -235,7 +233,8 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
anchor: HostNode | null,
|
anchor: HostNode | null,
|
||||||
isSVG: boolean,
|
isSVG: boolean,
|
||||||
optimized: boolean,
|
optimized: boolean,
|
||||||
rendererInternals: RendererInternals<HostNode, HostElement>
|
rendererInternals: RendererInternals<HostNode, HostElement>,
|
||||||
|
isHydrating = false
|
||||||
): SuspenseBoundary<HostNode, HostElement> {
|
): SuspenseBoundary<HostNode, HostElement> {
|
||||||
const {
|
const {
|
||||||
p: patch,
|
p: patch,
|
||||||
@ -245,6 +244,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
o: { parentNode }
|
o: { parentNode }
|
||||||
} = rendererInternals
|
} = rendererInternals
|
||||||
|
|
||||||
|
const getCurrentTree = () =>
|
||||||
|
suspense.isResolved || suspense.isHydrating
|
||||||
|
? suspense.subTree
|
||||||
|
: suspense.fallbackTree
|
||||||
|
|
||||||
|
const { content, fallback } = normalizeSuspenseChildren(vnode)
|
||||||
const suspense: SuspenseBoundary<HostNode, HostElement> = {
|
const suspense: SuspenseBoundary<HostNode, HostElement> = {
|
||||||
vnode,
|
vnode,
|
||||||
parent,
|
parent,
|
||||||
@ -255,8 +260,9 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
hiddenContainer,
|
hiddenContainer,
|
||||||
anchor,
|
anchor,
|
||||||
deps: 0,
|
deps: 0,
|
||||||
subTree: (null as unknown) as VNode, // will be set immediately after creation
|
subTree: content,
|
||||||
fallbackTree: (null as unknown) as VNode, // will be set immediately after creation
|
fallbackTree: fallback,
|
||||||
|
isHydrating,
|
||||||
isResolved: false,
|
isResolved: false,
|
||||||
isUnmounted: false,
|
isUnmounted: false,
|
||||||
effects: [],
|
effects: [],
|
||||||
@ -283,6 +289,9 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
container
|
container
|
||||||
} = suspense
|
} = suspense
|
||||||
|
|
||||||
|
if (suspense.isHydrating) {
|
||||||
|
suspense.isHydrating = false
|
||||||
|
} else {
|
||||||
// this is initial anchor on mount
|
// this is initial anchor on mount
|
||||||
let { anchor } = suspense
|
let { anchor } = suspense
|
||||||
// unmount fallback tree
|
// unmount fallback tree
|
||||||
@ -294,6 +303,8 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
}
|
}
|
||||||
// move content from off-dom container to actual container
|
// move content from off-dom container to actual container
|
||||||
move(subTree, container, anchor, MoveType.ENTER)
|
move(subTree, container, anchor, MoveType.ENTER)
|
||||||
|
}
|
||||||
|
|
||||||
const el = (vnode.el = subTree.el!)
|
const el = (vnode.el = subTree.el!)
|
||||||
// suspense as the root node of a component...
|
// suspense as the root node of a component...
|
||||||
if (parentComponent && parentComponent.subTree === vnode) {
|
if (parentComponent && parentComponent.subTree === vnode) {
|
||||||
@ -367,19 +378,12 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
},
|
},
|
||||||
|
|
||||||
move(container, anchor, type) {
|
move(container, anchor, type) {
|
||||||
move(
|
move(getCurrentTree(), container, anchor, type)
|
||||||
suspense.isResolved ? suspense.subTree : suspense.fallbackTree,
|
|
||||||
container,
|
|
||||||
anchor,
|
|
||||||
type
|
|
||||||
)
|
|
||||||
suspense.container = container
|
suspense.container = container
|
||||||
},
|
},
|
||||||
|
|
||||||
next() {
|
next() {
|
||||||
return next(
|
return next(getCurrentTree())
|
||||||
suspense.isResolved ? suspense.subTree : suspense.fallbackTree
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
registerDep(instance, setupRenderEffect) {
|
registerDep(instance, setupRenderEffect) {
|
||||||
@ -392,6 +396,7 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hydratedEl = instance.vnode.el
|
||||||
suspense.deps++
|
suspense.deps++
|
||||||
instance
|
instance
|
||||||
.asyncDep!.catch(err => {
|
.asyncDep!.catch(err => {
|
||||||
@ -411,14 +416,23 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
pushWarningContext(vnode)
|
pushWarningContext(vnode)
|
||||||
}
|
}
|
||||||
handleSetupResult(instance, asyncSetupResult, suspense, false)
|
handleSetupResult(instance, asyncSetupResult, suspense, false)
|
||||||
// unset placeholder, otherwise this will be treated as a hydration mount
|
if (hydratedEl) {
|
||||||
vnode.el = null
|
// vnode may have been replaced if an update happened before the
|
||||||
|
// async dep is reoslved.
|
||||||
|
vnode.el = hydratedEl
|
||||||
|
}
|
||||||
setupRenderEffect(
|
setupRenderEffect(
|
||||||
instance,
|
instance,
|
||||||
vnode,
|
vnode,
|
||||||
// component may have been moved before resolve
|
// component may have been moved before resolve.
|
||||||
parentNode(instance.subTree.el)!,
|
// if this is not a hydration, instance.subTree will be the comment
|
||||||
next(instance.subTree),
|
// placeholder.
|
||||||
|
hydratedEl
|
||||||
|
? parentNode(hydratedEl)!
|
||||||
|
: parentNode(instance.subTree.el)!,
|
||||||
|
// anchor will not be used if this is hydration, so only need to
|
||||||
|
// consider the comment placeholder case.
|
||||||
|
hydratedEl ? null : next(instance.subTree),
|
||||||
suspense,
|
suspense,
|
||||||
isSVG
|
isSVG
|
||||||
)
|
)
|
||||||
@ -449,6 +463,53 @@ function createSuspenseBoundary<HostNode, HostElement>(
|
|||||||
return suspense
|
return suspense
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hydrateSuspense(
|
||||||
|
node: Node,
|
||||||
|
vnode: VNode,
|
||||||
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
|
isSVG: boolean,
|
||||||
|
optimized: boolean,
|
||||||
|
rendererInternals: RendererInternals,
|
||||||
|
hydrateNode: (
|
||||||
|
node: Node,
|
||||||
|
vnode: VNode,
|
||||||
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
|
optimized: boolean
|
||||||
|
) => Node | null
|
||||||
|
): Node | null {
|
||||||
|
const suspense = (vnode.suspense = createSuspenseBoundary(
|
||||||
|
vnode,
|
||||||
|
parentSuspense,
|
||||||
|
parentComponent,
|
||||||
|
node.parentNode,
|
||||||
|
document.createElement('div'),
|
||||||
|
null,
|
||||||
|
isSVG,
|
||||||
|
optimized,
|
||||||
|
rendererInternals,
|
||||||
|
true /* hydrating */
|
||||||
|
))
|
||||||
|
// there are two possible scenarios for server-rendered suspense:
|
||||||
|
// - success: ssr content should be fully resolved
|
||||||
|
// - failure: ssr content should be the fallback branch.
|
||||||
|
// however, on the client we don't really know if it has failed or not
|
||||||
|
// attempt to hydrate the DOM assuming it has succeeded, but we still
|
||||||
|
// need to construct a suspense boundary first
|
||||||
|
const result = hydrateNode(
|
||||||
|
node,
|
||||||
|
suspense.subTree,
|
||||||
|
parentComponent,
|
||||||
|
suspense,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
|
if (suspense.deps === 0) {
|
||||||
|
suspense.resolve()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeSuspenseChildren(
|
export function normalizeSuspenseChildren(
|
||||||
vnode: VNode
|
vnode: VNode
|
||||||
): {
|
): {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode'
|
import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode'
|
||||||
import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
|
import { flushPostFlushCbs } from './scheduler'
|
||||||
import { ComponentInternalInstance } from './component'
|
import { ComponentInternalInstance } from './component'
|
||||||
import { invokeDirectiveHook } from './directives'
|
import { invokeDirectiveHook } from './directives'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
@ -11,6 +11,11 @@ import {
|
|||||||
isString
|
isString
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { RendererInternals } from './renderer'
|
import { RendererInternals } from './renderer'
|
||||||
|
import {
|
||||||
|
SuspenseImpl,
|
||||||
|
SuspenseBoundary,
|
||||||
|
queueEffectWithSuspense
|
||||||
|
} from './components/Suspense'
|
||||||
|
|
||||||
export type RootHydrateFunction = (
|
export type RootHydrateFunction = (
|
||||||
vnode: VNode<Node, Element>,
|
vnode: VNode<Node, Element>,
|
||||||
@ -25,16 +30,27 @@ const enum DOMNodeTypes {
|
|||||||
|
|
||||||
let hasMismatch = false
|
let hasMismatch = false
|
||||||
|
|
||||||
|
const isSVGContainer = (container: Element) =>
|
||||||
|
/svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
|
||||||
|
|
||||||
|
const isComment = (node: Node): node is Comment =>
|
||||||
|
node.nodeType === DOMNodeTypes.COMMENT
|
||||||
|
|
||||||
// Note: hydration is DOM-specific
|
// Note: hydration is DOM-specific
|
||||||
// But we have to place it in core due to tight coupling with core - splitting
|
// But we have to place it in core due to tight coupling with core - splitting
|
||||||
// it out creates a ton of unnecessary complexity.
|
// it out creates a ton of unnecessary complexity.
|
||||||
// Hydration also depends on some renderer internal logic which needs to be
|
// Hydration also depends on some renderer internal logic which needs to be
|
||||||
// passed in via arguments.
|
// passed in via arguments.
|
||||||
export function createHydrationFunctions({
|
export function createHydrationFunctions(
|
||||||
|
rendererInternals: RendererInternals<Node, Element>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
mt: mountComponent,
|
mt: mountComponent,
|
||||||
p: patch,
|
p: patch,
|
||||||
o: { patchProp, createText }
|
n: next,
|
||||||
}: RendererInternals<Node, Element>) {
|
o: { patchProp, nextSibling, parentNode }
|
||||||
|
} = rendererInternals
|
||||||
|
|
||||||
const hydrate: RootHydrateFunction = (vnode, container) => {
|
const hydrate: RootHydrateFunction = (vnode, container) => {
|
||||||
if (__DEV__ && !container.hasChildNodes()) {
|
if (__DEV__ && !container.hasChildNodes()) {
|
||||||
warn(
|
warn(
|
||||||
@ -45,7 +61,7 @@ export function createHydrationFunctions({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
hasMismatch = false
|
hasMismatch = false
|
||||||
hydrateNode(container.firstChild!, vnode)
|
hydrateNode(container.firstChild!, vnode, null, null)
|
||||||
flushPostFlushCbs()
|
flushPostFlushCbs()
|
||||||
if (hasMismatch && !__TEST__) {
|
if (hasMismatch && !__TEST__) {
|
||||||
// this error should show up in production
|
// this error should show up in production
|
||||||
@ -56,7 +72,8 @@ export function createHydrationFunctions({
|
|||||||
const hydrateNode = (
|
const hydrateNode = (
|
||||||
node: Node,
|
node: Node,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null = null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
optimized = false
|
optimized = false
|
||||||
): Node | null => {
|
): Node | null => {
|
||||||
const { type, shapeFlag } = vnode
|
const { type, shapeFlag } = vnode
|
||||||
@ -67,7 +84,7 @@ export function createHydrationFunctions({
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case Text:
|
case Text:
|
||||||
if (domType !== DOMNodeTypes.TEXT) {
|
if (domType !== DOMNodeTypes.TEXT) {
|
||||||
return handleMismtach(node, vnode, parentComponent)
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
}
|
}
|
||||||
if ((node as Text).data !== vnode.children) {
|
if ((node as Text).data !== vnode.children) {
|
||||||
hasMismatch = true
|
hasMismatch = true
|
||||||
@ -79,48 +96,83 @@ export function createHydrationFunctions({
|
|||||||
)
|
)
|
||||||
;(node as Text).data = vnode.children as string
|
;(node as Text).data = vnode.children as string
|
||||||
}
|
}
|
||||||
return node.nextSibling
|
return nextSibling(node)
|
||||||
case Comment:
|
case Comment:
|
||||||
if (domType !== DOMNodeTypes.COMMENT) {
|
if (domType !== DOMNodeTypes.COMMENT) {
|
||||||
return handleMismtach(node, vnode, parentComponent)
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
}
|
}
|
||||||
return node.nextSibling
|
return nextSibling(node)
|
||||||
case Static:
|
case Static:
|
||||||
if (domType !== DOMNodeTypes.ELEMENT) {
|
if (domType !== DOMNodeTypes.ELEMENT) {
|
||||||
return handleMismtach(node, vnode, parentComponent)
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
}
|
}
|
||||||
return node.nextSibling
|
return nextSibling(node)
|
||||||
case Fragment:
|
case Fragment:
|
||||||
return hydrateFragment(node, vnode, parentComponent, optimized)
|
if (domType !== DOMNodeTypes.COMMENT) {
|
||||||
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
|
}
|
||||||
|
return hydrateFragment(
|
||||||
|
node as Comment,
|
||||||
|
vnode,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||||
if (
|
if (
|
||||||
domType !== DOMNodeTypes.ELEMENT ||
|
domType !== DOMNodeTypes.ELEMENT ||
|
||||||
vnode.type !== (node as Element).tagName.toLowerCase()
|
vnode.type !== (node as Element).tagName.toLowerCase()
|
||||||
) {
|
) {
|
||||||
return handleMismtach(node, vnode, parentComponent)
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
}
|
}
|
||||||
return hydrateElement(
|
return hydrateElement(
|
||||||
node as Element,
|
node as Element,
|
||||||
vnode,
|
vnode,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||||
// when setting up the render effect, if the initial vnode already
|
// when setting up the render effect, if the initial vnode already
|
||||||
// has .el set, the component will perform hydration instead of mount
|
// has .el set, the component will perform hydration instead of mount
|
||||||
// on its sub-tree.
|
// on its sub-tree.
|
||||||
mountComponent(vnode, null, null, parentComponent, null, false)
|
const container = parentNode(node)!
|
||||||
|
mountComponent(
|
||||||
|
vnode,
|
||||||
|
container,
|
||||||
|
null,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isSVGContainer(container)
|
||||||
|
)
|
||||||
const subTree = vnode.component!.subTree
|
const subTree = vnode.component!.subTree
|
||||||
return (subTree.anchor || subTree.el).nextSibling
|
if (subTree) {
|
||||||
|
return next(subTree)
|
||||||
|
} else {
|
||||||
|
// no subTree means this is an async component
|
||||||
|
// try to locate the ending node
|
||||||
|
return isComment(node) && node.data === '1'
|
||||||
|
? locateClosingAsyncAnchor(node)
|
||||||
|
: nextSibling(node)
|
||||||
|
}
|
||||||
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
} else if (shapeFlag & ShapeFlags.PORTAL) {
|
||||||
if (domType !== DOMNodeTypes.COMMENT) {
|
if (domType !== DOMNodeTypes.COMMENT) {
|
||||||
return handleMismtach(node, vnode, parentComponent)
|
return handleMismtach(node, vnode, parentComponent, parentSuspense)
|
||||||
}
|
}
|
||||||
hydratePortal(vnode, parentComponent, optimized)
|
hydratePortal(vnode, parentComponent, parentSuspense, optimized)
|
||||||
return node.nextSibling
|
return nextSibling(node)
|
||||||
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
|
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
|
||||||
// TODO Suspense
|
return (vnode.type as typeof SuspenseImpl).hydrate(
|
||||||
|
node,
|
||||||
|
vnode,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isSVGContainer(parentNode(node)!),
|
||||||
|
optimized,
|
||||||
|
rendererInternals,
|
||||||
|
hydrateNode
|
||||||
|
)
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
warn('Invalid HostVNode type:', type, `(${typeof type})`)
|
warn('Invalid HostVNode type:', type, `(${typeof type})`)
|
||||||
}
|
}
|
||||||
@ -132,6 +184,7 @@ export function createHydrationFunctions({
|
|||||||
el: Element,
|
el: Element,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
optimized: boolean
|
optimized: boolean
|
||||||
) => {
|
) => {
|
||||||
optimized = optimized || vnode.dynamicChildren !== null
|
optimized = optimized || vnode.dynamicChildren !== null
|
||||||
@ -161,9 +214,9 @@ export function createHydrationFunctions({
|
|||||||
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
|
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
|
||||||
}
|
}
|
||||||
if (onVnodeMounted != null) {
|
if (onVnodeMounted != null) {
|
||||||
queuePostFlushCb(() => {
|
queueEffectWithSuspense(() => {
|
||||||
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
|
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
|
||||||
})
|
}, parentSuspense)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// children
|
// children
|
||||||
@ -177,6 +230,7 @@ export function createHydrationFunctions({
|
|||||||
vnode,
|
vnode,
|
||||||
el,
|
el,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
let hasWarned = false
|
let hasWarned = false
|
||||||
@ -215,6 +269,7 @@ export function createHydrationFunctions({
|
|||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
container: Element,
|
container: Element,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
optimized: boolean
|
optimized: boolean
|
||||||
): Node | null => {
|
): Node | null => {
|
||||||
optimized = optimized || vnode.dynamicChildren !== null
|
optimized = optimized || vnode.dynamicChildren !== null
|
||||||
@ -226,7 +281,13 @@ export function createHydrationFunctions({
|
|||||||
? children[i]
|
? children[i]
|
||||||
: (children[i] = normalizeVNode(children[i]))
|
: (children[i] = normalizeVNode(children[i]))
|
||||||
if (node) {
|
if (node) {
|
||||||
node = hydrateNode(node, vnode, parentComponent, optimized)
|
node = hydrateNode(
|
||||||
|
node,
|
||||||
|
vnode,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
optimized
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
hasMismatch = true
|
hasMismatch = true
|
||||||
if (__DEV__ && !hasWarned) {
|
if (__DEV__ && !hasWarned) {
|
||||||
@ -237,34 +298,43 @@ export function createHydrationFunctions({
|
|||||||
hasWarned = true
|
hasWarned = true
|
||||||
}
|
}
|
||||||
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
|
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
|
||||||
patch(null, vnode, container)
|
patch(
|
||||||
|
null,
|
||||||
|
vnode,
|
||||||
|
container,
|
||||||
|
null,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isSVGContainer(container)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateFragment = (
|
const hydrateFragment = (
|
||||||
node: Node,
|
node: Comment,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
optimized: boolean
|
optimized: boolean
|
||||||
) => {
|
) => {
|
||||||
const parent = node.parentNode as Element
|
return nextSibling(
|
||||||
parent.insertBefore((vnode.el = createText('')), node)
|
(vnode.anchor = hydrateChildren(
|
||||||
const next = hydrateChildren(
|
nextSibling(node)!,
|
||||||
node,
|
|
||||||
vnode,
|
vnode,
|
||||||
parent,
|
parentNode(node)!,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
optimized
|
optimized
|
||||||
|
)!)
|
||||||
)
|
)
|
||||||
parent.insertBefore((vnode.anchor = createText('')), next)
|
|
||||||
return next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydratePortal = (
|
const hydratePortal = (
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null,
|
||||||
optimized: boolean
|
optimized: boolean
|
||||||
) => {
|
) => {
|
||||||
const targetSelector = vnode.props && vnode.props.target
|
const targetSelector = vnode.props && vnode.props.target
|
||||||
@ -277,6 +347,7 @@ export function createHydrationFunctions({
|
|||||||
vnode,
|
vnode,
|
||||||
target,
|
target,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
optimized
|
optimized
|
||||||
)
|
)
|
||||||
} else if (__DEV__) {
|
} else if (__DEV__) {
|
||||||
@ -290,7 +361,8 @@ export function createHydrationFunctions({
|
|||||||
const handleMismtach = (
|
const handleMismtach = (
|
||||||
node: Node,
|
node: Node,
|
||||||
vnode: VNode,
|
vnode: VNode,
|
||||||
parentComponent: ComponentInternalInstance | null
|
parentComponent: ComponentInternalInstance | null,
|
||||||
|
parentSuspense: SuspenseBoundary | null
|
||||||
) => {
|
) => {
|
||||||
hasMismatch = true
|
hasMismatch = true
|
||||||
__DEV__ &&
|
__DEV__ &&
|
||||||
@ -298,16 +370,43 @@ export function createHydrationFunctions({
|
|||||||
`Hydration node mismatch:\n- Client vnode:`,
|
`Hydration node mismatch:\n- Client vnode:`,
|
||||||
vnode.type,
|
vnode.type,
|
||||||
`\n- Server rendered DOM:`,
|
`\n- Server rendered DOM:`,
|
||||||
node
|
node,
|
||||||
|
node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
|
||||||
)
|
)
|
||||||
vnode.el = null
|
vnode.el = null
|
||||||
const next = node.nextSibling
|
const next = nextSibling(node)
|
||||||
const container = node.parentNode as Element
|
const container = parentNode(node)!
|
||||||
container.removeChild(node)
|
container.removeChild(node)
|
||||||
// TODO Suspense and SVG
|
// TODO Suspense
|
||||||
patch(null, vnode, container, next, parentComponent)
|
patch(
|
||||||
|
null,
|
||||||
|
vnode,
|
||||||
|
container,
|
||||||
|
next,
|
||||||
|
parentComponent,
|
||||||
|
parentSuspense,
|
||||||
|
isSVGContainer(container)
|
||||||
|
)
|
||||||
return next
|
return next
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
|
||||||
|
let match = 0
|
||||||
|
while (node) {
|
||||||
|
node = nextSibling(node)
|
||||||
|
if (node && isComment(node)) {
|
||||||
|
if (node.data === '1') match++
|
||||||
|
if (node.data === '0') {
|
||||||
|
if (match === 0) {
|
||||||
|
return nextSibling(node)
|
||||||
|
} else {
|
||||||
|
match--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
return [hydrate, hydrateNode] as const
|
return [hydrate, hydrateNode] as const
|
||||||
}
|
}
|
||||||
|
@ -202,7 +202,7 @@ type UnmountChildrenFn<HostNode, HostElement> = (
|
|||||||
|
|
||||||
export type MountComponentFn<HostNode, HostElement> = (
|
export type MountComponentFn<HostNode, HostElement> = (
|
||||||
initialVNode: VNode<HostNode, HostElement>,
|
initialVNode: VNode<HostNode, HostElement>,
|
||||||
container: HostElement | null, // only null during hydration
|
container: HostElement,
|
||||||
anchor: HostNode | null,
|
anchor: HostNode | null,
|
||||||
parentComponent: ComponentInternalInstance | null,
|
parentComponent: ComponentInternalInstance | null,
|
||||||
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
|
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
|
||||||
@ -219,7 +219,7 @@ type ProcessTextOrCommentFn<HostNode, HostElement> = (
|
|||||||
export type SetupRenderEffectFn<HostNode, HostElement> = (
|
export type SetupRenderEffectFn<HostNode, HostElement> = (
|
||||||
instance: ComponentInternalInstance,
|
instance: ComponentInternalInstance,
|
||||||
initialVNode: VNode<HostNode, HostElement>,
|
initialVNode: VNode<HostNode, HostElement>,
|
||||||
container: HostElement | null, // only null during hydration
|
container: HostElement,
|
||||||
anchor: HostNode | null,
|
anchor: HostNode | null,
|
||||||
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
|
parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
|
||||||
isSVG: boolean
|
isSVG: boolean
|
||||||
@ -991,7 +991,7 @@ function baseCreateRenderer<
|
|||||||
|
|
||||||
const mountComponent: MountComponentFn<HostNode, HostElement> = (
|
const mountComponent: MountComponentFn<HostNode, HostElement> = (
|
||||||
initialVNode,
|
initialVNode,
|
||||||
container, // only null during hydration
|
container,
|
||||||
anchor,
|
anchor,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
parentSuspense,
|
parentSuspense,
|
||||||
@ -1031,9 +1031,10 @@ function baseCreateRenderer<
|
|||||||
parentSuspense.registerDep(instance, setupRenderEffect)
|
parentSuspense.registerDep(instance, setupRenderEffect)
|
||||||
|
|
||||||
// Give it a placeholder if this is not hydration
|
// Give it a placeholder if this is not hydration
|
||||||
|
if (!initialVNode.el) {
|
||||||
const placeholder = (instance.subTree = createVNode(Comment))
|
const placeholder = (instance.subTree = createVNode(Comment))
|
||||||
processCommentNode(null, placeholder, container!, anchor)
|
processCommentNode(null, placeholder, container!, anchor)
|
||||||
initialVNode.el = placeholder.el
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1069,12 +1070,17 @@ function baseCreateRenderer<
|
|||||||
}
|
}
|
||||||
if (initialVNode.el && hydrateNode) {
|
if (initialVNode.el && hydrateNode) {
|
||||||
// vnode has adopted host node - perform hydration instead of mount.
|
// vnode has adopted host node - perform hydration instead of mount.
|
||||||
hydrateNode(initialVNode.el as Node, subTree, instance)
|
hydrateNode(
|
||||||
|
initialVNode.el as Node,
|
||||||
|
subTree,
|
||||||
|
instance,
|
||||||
|
parentSuspense
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
patch(
|
patch(
|
||||||
null,
|
null,
|
||||||
subTree,
|
subTree,
|
||||||
container!, // container is only null during hydration
|
container,
|
||||||
anchor,
|
anchor,
|
||||||
instance,
|
instance,
|
||||||
parentSuspense,
|
parentSuspense,
|
||||||
|
@ -212,7 +212,8 @@ export function createVNode(
|
|||||||
): VNode {
|
): VNode {
|
||||||
if (!type) {
|
if (!type) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
warn(`Invalid vnode type when creating vnode: ${type}.`)
|
debugger
|
||||||
|
warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
|
||||||
}
|
}
|
||||||
type = Comment
|
type = Comment
|
||||||
}
|
}
|
||||||
|
@ -257,7 +257,7 @@ describe('ssr: renderToString', () => {
|
|||||||
)
|
)
|
||||||
).toBe(
|
).toBe(
|
||||||
`<div>parent<div class="child">` +
|
`<div>parent<div class="child">` +
|
||||||
`<span>from slot</span>` +
|
`<!--1--><span>from slot</span><!--0-->` +
|
||||||
`</div></div>`
|
`</div></div>`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -272,7 +272,9 @@ describe('ssr: renderToString', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).toBe(`<div>parent<div class="child">fallback</div></div>`)
|
).toBe(
|
||||||
|
`<div>parent<div class="child"><!--1-->fallback<!--0--></div></div>`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nested components with vnode slots', async () => {
|
test('nested components with vnode slots', async () => {
|
||||||
@ -316,7 +318,7 @@ describe('ssr: renderToString', () => {
|
|||||||
)
|
)
|
||||||
).toBe(
|
).toBe(
|
||||||
`<div>parent<div class="child">` +
|
`<div>parent<div class="child">` +
|
||||||
`<span>from slot</span>` +
|
`<!--1--><span>from slot</span><!--0-->` +
|
||||||
`</div></div>`
|
`</div></div>`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -328,13 +330,13 @@ describe('ssr: renderToString', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
|
components: { Child },
|
||||||
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
|
template: `<div>parent<Child v-slot="{ msg }"><span>{{ msg }}</span></Child></div>`
|
||||||
})
|
})
|
||||||
app.component('Child', Child)
|
|
||||||
|
|
||||||
expect(await renderToString(app)).toBe(
|
expect(await renderToString(app)).toBe(
|
||||||
`<div>parent<div class="child">` +
|
`<div>parent<div class="child">` +
|
||||||
`<span>from slot</span>` +
|
`<!--1--><span>from slot</span><!--0-->` +
|
||||||
`</div></div>`
|
`</div></div>`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -360,6 +362,7 @@ describe('ssr: renderToString', () => {
|
|||||||
|
|
||||||
expect(await renderToString(app)).toBe(
|
expect(await renderToString(app)).toBe(
|
||||||
`<div>parent<div class="child">` +
|
`<div>parent<div class="child">` +
|
||||||
|
// no comment anchors because slot is used directly as element children
|
||||||
`<span>from slot</span>` +
|
`<span>from slot</span>` +
|
||||||
`</div></div>`
|
`</div></div>`
|
||||||
)
|
)
|
||||||
@ -456,7 +459,9 @@ describe('ssr: renderToString', () => {
|
|||||||
createCommentVNode('qux')
|
createCommentVNode('qux')
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
).toBe(`<div>foo<span>bar</span><span>baz</span><!--qux--></div>`)
|
).toBe(
|
||||||
|
`<div>foo<span>bar</span><!--1--><span>baz</span><!--0--><!--qux--></div>`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('void elements', async () => {
|
test('void elements', async () => {
|
||||||
|
@ -33,7 +33,7 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await renderToString(app)).toBe(`<div>async</div>`)
|
expect(await renderToString(app)).toBe(`<!--1--><div>async</div><!--0-->`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('with async component', async () => {
|
test('with async component', async () => {
|
||||||
@ -49,7 +49,7 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await renderToString(app)).toBe(`<div>async</div>`)
|
expect(await renderToString(app)).toBe(`<!--1--><div>async</div><!--0-->`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('fallback', async () => {
|
test('fallback', async () => {
|
||||||
@ -68,7 +68,9 @@ describe('SSR Suspense', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await renderToString(app)).toBe(`<div>fallback</div>`)
|
expect(await renderToString(app)).toBe(
|
||||||
|
`<!--1--><div>fallback</div><!--0-->`
|
||||||
|
)
|
||||||
expect('Uncaught error in async setup').toHaveBeenWarned()
|
expect('Uncaught error in async setup').toHaveBeenWarned()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -18,6 +18,8 @@ export function ssrRenderSlot(
|
|||||||
push: PushFn,
|
push: PushFn,
|
||||||
parentComponent: ComponentInternalInstance
|
parentComponent: ComponentInternalInstance
|
||||||
) {
|
) {
|
||||||
|
// template-compiled slots are always rendered as fragments
|
||||||
|
push(`<!--1-->`)
|
||||||
const slotFn = slots[slotName]
|
const slotFn = slots[slotName]
|
||||||
if (slotFn) {
|
if (slotFn) {
|
||||||
if (slotFn.length > 1) {
|
if (slotFn.length > 1) {
|
||||||
@ -31,4 +33,5 @@ export function ssrRenderSlot(
|
|||||||
} else if (fallbackRenderFn) {
|
} else if (fallbackRenderFn) {
|
||||||
fallbackRenderFn()
|
fallbackRenderFn()
|
||||||
}
|
}
|
||||||
|
push(`<!--0-->`)
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString'
|
import { PushFn, ResolvedSSRBuffer, createBuffer } from '../renderToString'
|
||||||
import { NOOP } from '@vue/shared'
|
|
||||||
|
|
||||||
type ContentRenderFn = (push: PushFn) => void
|
type ContentRenderFn = (push: PushFn) => void
|
||||||
|
|
||||||
export async function ssrRenderSuspense({
|
export async function ssrRenderSuspense({
|
||||||
default: renderContent = NOOP,
|
default: renderContent,
|
||||||
fallback: renderFallback = NOOP
|
fallback: renderFallback
|
||||||
}: Record<string, ContentRenderFn | undefined>): Promise<ResolvedSSRBuffer> {
|
}: Record<string, ContentRenderFn | undefined>): Promise<ResolvedSSRBuffer> {
|
||||||
try {
|
try {
|
||||||
|
if (renderContent) {
|
||||||
const { push, getBuffer } = createBuffer()
|
const { push, getBuffer } = createBuffer()
|
||||||
|
push(`<!--1-->`)
|
||||||
renderContent(push)
|
renderContent(push)
|
||||||
|
push(`<!--0-->`)
|
||||||
return await getBuffer()
|
return await getBuffer()
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
if (renderFallback) {
|
||||||
const { push, getBuffer } = createBuffer()
|
const { push, getBuffer } = createBuffer()
|
||||||
|
push(`<!--1-->`)
|
||||||
renderFallback(push)
|
renderFallback(push)
|
||||||
|
push(`<!--0-->`)
|
||||||
return getBuffer()
|
return getBuffer()
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +256,9 @@ function renderVNode(
|
|||||||
push(children ? `<!--${children}-->` : `<!---->`)
|
push(children ? `<!--${children}-->` : `<!---->`)
|
||||||
break
|
break
|
||||||
case Fragment:
|
case Fragment:
|
||||||
|
push(`<!--1-->`) // open
|
||||||
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
|
renderVNodeChildren(push, children as VNodeArrayChildren, parentComponent)
|
||||||
|
push(`<!--0-->`) // close
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||||
|
@ -106,7 +106,12 @@ function createConfig(format, output, plugins = []) {
|
|||||||
format === 'esm-bundler-runtime' ? `src/runtime.ts` : `src/index.ts`
|
format === 'esm-bundler-runtime' ? `src/runtime.ts` : `src/index.ts`
|
||||||
|
|
||||||
const external =
|
const external =
|
||||||
isGlobalBuild || isRawESMBuild ? [] : Object.keys(pkg.dependencies || {})
|
isGlobalBuild || isRawESMBuild
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
...Object.keys(pkg.dependencies || {}),
|
||||||
|
...Object.keys(pkg.peerDependencies || {})
|
||||||
|
]
|
||||||
|
|
||||||
const nodePlugins = packageOptions.enableNonBrowserBranches
|
const nodePlugins = packageOptions.enableNonBrowserBranches
|
||||||
? [
|
? [
|
||||||
|
Loading…
Reference in New Issue
Block a user