wip(ssr): ssr slot vnode fallback

This commit is contained in:
Evan You 2020-02-07 01:06:51 -05:00
parent 31f3383a02
commit b7a74d0439
15 changed files with 308 additions and 131 deletions

View File

@ -51,7 +51,8 @@ export const enum NodeTypes {
JS_BLOCK_STATEMENT, JS_BLOCK_STATEMENT,
JS_TEMPLATE_LITERAL, JS_TEMPLATE_LITERAL,
JS_IF_STATEMENT, JS_IF_STATEMENT,
JS_ASSIGNMENT_EXPRESSION JS_ASSIGNMENT_EXPRESSION,
JS_RETURN_STATEMENT
} }
export const enum ElementTypes { export const enum ElementTypes {
@ -294,7 +295,7 @@ export interface FunctionExpression extends Node {
type: NodeTypes.JS_FUNCTION_EXPRESSION type: NodeTypes.JS_FUNCTION_EXPRESSION
params: ExpressionNode | string | (ExpressionNode | string)[] | undefined params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
body?: BlockStatement body?: BlockStatement | IfStatement
newline: boolean newline: boolean
// so that codegen knows it needs to generate ScopeId wrapper // so that codegen knows it needs to generate ScopeId wrapper
isSlot: boolean isSlot: boolean
@ -322,7 +323,12 @@ export interface CacheExpression extends Node {
// SSR-specific Node Types ----------------------------------------------------- // SSR-specific Node Types -----------------------------------------------------
export type SSRCodegenNode = BlockStatement | TemplateLiteral | IfStatement export type SSRCodegenNode =
| BlockStatement
| TemplateLiteral
| IfStatement
| AssignmentExpression
| ReturnStatement
export interface BlockStatement extends Node { export interface BlockStatement extends Node {
type: NodeTypes.JS_BLOCK_STATEMENT type: NodeTypes.JS_BLOCK_STATEMENT
@ -338,7 +344,7 @@ export interface IfStatement extends Node {
type: NodeTypes.JS_IF_STATEMENT type: NodeTypes.JS_IF_STATEMENT
test: ExpressionNode test: ExpressionNode
consequent: BlockStatement consequent: BlockStatement
alternate: IfStatement | BlockStatement | undefined alternate: IfStatement | BlockStatement | ReturnStatement | undefined
} }
export interface AssignmentExpression extends Node { export interface AssignmentExpression extends Node {
@ -347,6 +353,11 @@ export interface AssignmentExpression extends Node {
right: JSChildNode right: JSChildNode
} }
export interface ReturnStatement extends Node {
type: NodeTypes.JS_RETURN_STATEMENT
returns: TemplateChildNode | TemplateChildNode[] | JSChildNode
}
// Codegen Node Types ---------------------------------------------------------- // Codegen Node Types ----------------------------------------------------------
// createVNode(...) // createVNode(...)
@ -733,3 +744,13 @@ export function createAssignmentExpression(
loc: locStub loc: locStub
} }
} }
export function createReturnStatement(
returns: ReturnStatement['returns']
): ReturnStatement {
return {
type: NodeTypes.JS_RETURN_STATEMENT,
returns,
loc: locStub
}
}

View File

@ -22,7 +22,8 @@ import {
SSRCodegenNode, SSRCodegenNode,
TemplateLiteral, TemplateLiteral,
IfStatement, IfStatement,
AssignmentExpression AssignmentExpression,
ReturnStatement
} from './ast' } from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map' import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { import {
@ -44,8 +45,7 @@ import {
CREATE_TEXT, CREATE_TEXT,
PUSH_SCOPE_ID, PUSH_SCOPE_ID,
POP_SCOPE_ID, POP_SCOPE_ID,
WITH_SCOPE_ID, WITH_SCOPE_ID
CREATE_BLOCK
} from './runtimeHelpers' } from './runtimeHelpers'
import { ImportItem } from './transform' import { ImportItem } from './transform'
@ -334,24 +334,12 @@ function genModulePreamble(
context: CodegenContext, context: CodegenContext,
genScopeId: boolean genScopeId: boolean
) { ) {
const { push, helper, newline, scopeId, runtimeModuleName, ssr } = context const { push, helper, newline, scopeId, runtimeModuleName } = context
if (!__BROWSER__) { if (!__BROWSER__ && genScopeId) {
// in ssr mode, `withId` helper is only needed if the template contains ast.helpers.push(WITH_SCOPE_ID)
// de-optimized component slots (which uses the createVNode helper) if (ast.hoists.length) {
if ( ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
ssr &&
!(
ast.helpers.includes(CREATE_VNODE) || ast.helpers.includes(CREATE_BLOCK)
)
) {
genScopeId = false
}
if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID)
if (ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
} }
} }
@ -572,6 +560,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_ASSIGNMENT_EXPRESSION: case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
!__BROWSER__ && genAssignmentExpression(node, context) !__BROWSER__ && genAssignmentExpression(node, context)
break break
case NodeTypes.JS_RETURN_STATEMENT:
!__BROWSER__ && genReturnStatement(node, context)
break
/* istanbul ignore next */ /* istanbul ignore next */
default: default:
@ -851,3 +842,15 @@ function genAssignmentExpression(
context.push(` = `) context.push(` = `)
genNode(node.right, context) genNode(node.right, context)
} }
function genReturnStatement(
{ returns }: ReturnStatement,
context: CodegenContext
) {
context.push(`return `)
if (isArray(returns)) {
genNodeListAsArray(returns, context)
} else {
genNode(returns, context)
}
}

View File

@ -1,6 +1,6 @@
import { CompilerOptions } from './options' import { CompilerOptions } from './options'
import { baseParse } from './parse' import { baseParse } from './parse'
import { transform } from './transform' import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen' import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast' import { RootNode } from './ast'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
@ -17,6 +17,39 @@ import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
export type TransformPreset = [
NodeTransform[],
Record<string, DirectiveTransform>
]
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformFor,
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
// we name it `baseCompile` so that higher order compilers like // we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else. // @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile( export function baseCompile(
@ -44,30 +77,18 @@ export function baseCompile(
} }
const ast = isString(template) ? baseParse(template, options) : template const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
transform(ast, { transform(ast, {
...options, ...options,
prefixIdentifiers, prefixIdentifiers,
nodeTransforms: [ nodeTransforms: [
transformOnce, ...nodeTransforms,
transformIf,
transformFor,
...(prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText,
...(options.nodeTransforms || []) // user transforms ...(options.nodeTransforms || []) // user transforms
], ],
directiveTransforms: { directiveTransforms: {
on: transformOn, ...directiveTransforms,
bind: transformBind,
model: transformModel,
...(options.directiveTransforms || {}) // user transforms ...(options.directiveTransforms || {}) // user transforms
} }
}) })

View File

@ -10,8 +10,8 @@ export {
export { baseParse, TextModes } from './parse' export { baseParse, TextModes } from './parse'
export { export {
transform, transform,
createStructuralDirectiveTransform,
TransformContext, TransformContext,
createStructuralDirectiveTransform,
NodeTransform, NodeTransform,
StructuralDirectiveTransform, StructuralDirectiveTransform,
DirectiveTransform DirectiveTransform
@ -23,18 +23,16 @@ export {
CompilerError, CompilerError,
createCompilerError createCompilerError
} from './errors' } from './errors'
export * from './ast' export * from './ast'
export * from './utils' export * from './utils'
export { registerRuntimeHelpers } from './runtimeHelpers' export * from './runtimeHelpers'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
// expose transforms so higher-order compilers can import and extend them export { getBaseTransformPreset, TransformPreset } from './compile'
export { transformModel } from './transforms/vModel' export { transformModel } from './transforms/vModel'
export { transformOn } from './transforms/vOn' export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind' export { transformBind } from './transforms/vBind'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
// exported for compiler-ssr
export * from './runtimeHelpers'
export { processIf } from './transforms/vIf' export { processIf } from './transforms/vIf'
export { processFor, createForLoopParams } from './transforms/vFor' export { processFor, createForLoopParams } from './transforms/vFor'
export { export {

View File

@ -85,8 +85,8 @@ export interface TransformContext extends Required<TransformOptions> {
components: Set<string> components: Set<string>
directives: Set<string> directives: Set<string>
hoists: JSChildNode[] hoists: JSChildNode[]
temps: number
imports: Set<ImportItem> imports: Set<ImportItem>
temps: number
cached: number cached: number
identifiers: { [name: string]: number | undefined } identifiers: { [name: string]: number | undefined }
scopes: { scopes: {
@ -141,8 +141,8 @@ function createTransformContext(
components: new Set(), components: new Set(),
directives: new Set(), directives: new Set(),
hoists: [], hoists: [],
temps: 0,
imports: new Set(), imports: new Set(),
temps: 0,
cached: 0, cached: 0,
identifiers: {}, identifiers: {},
scopes: { scopes: {

View File

@ -77,7 +77,7 @@ export const transformText: NodeTransform = (node, context) => {
callArgs.push(child) callArgs.push(child)
} }
// mark dynamic text with flag so it gets patched inside a block // mark dynamic text with flag so it gets patched inside a block
if (child.type !== NodeTypes.TEXT) { if (!context.ssr && child.type !== NodeTypes.TEXT) {
callArgs.push( callArgs.push(
`${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */` `${PatchFlags.TEXT} /* ${PatchFlagNames[PatchFlags.TEXT]} */`
) )

View File

@ -6,7 +6,9 @@ import {
isBuiltInType, isBuiltInType,
ParserOptions, ParserOptions,
RootNode, RootNode,
noopDirectiveTransform noopDirectiveTransform,
TransformPreset,
getBaseTransformPreset
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { parserOptionsMinimal } from './parserOptionsMinimal' import { parserOptionsMinimal } from './parserOptionsMinimal'
import { parserOptionsStandard } from './parserOptionsStandard' import { parserOptionsStandard } from './parserOptionsStandard'
@ -31,25 +33,43 @@ export const isBuiltInDOMComponent = (tag: string): symbol | undefined => {
} }
} }
export function compile( export function getDOMTransformPreset(
template: string, prefixIdentifiers?: boolean
options: CompilerOptions = {} ): TransformPreset {
): CodegenResult { const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
return baseCompile(template, { prefixIdentifiers
...parserOptions, )
...options, return [
nodeTransforms: [ [
...nodeTransforms,
transformStyle, transformStyle,
...(__DEV__ ? [warnTransitionChildren] : []), ...(__DEV__ ? [warnTransitionChildren] : [])
...(options.nodeTransforms || [])
], ],
directiveTransforms: { {
...directiveTransforms,
cloak: noopDirectiveTransform, cloak: noopDirectiveTransform,
html: transformVHtml, html: transformVHtml,
text: transformVText, text: transformVText,
model: transformModel, // override compiler-core model: transformModel, // override compiler-core
on: transformOn, on: transformOn, // override compiler-core
show: transformShow, show: transformShow
}
]
}
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
const [nodeTransforms, directiveTransforms] = getDOMTransformPreset(
options.prefixIdentifiers
)
return baseCompile(template, {
...parserOptions,
...options,
nodeTransforms: [...nodeTransforms, ...(options.nodeTransforms || [])],
directiveTransforms: {
...directiveTransforms,
...(options.directiveTransforms || {}) ...(options.directiveTransforms || {})
}, },
isBuiltInComponent: isBuiltInDOMComponent isBuiltInComponent: isBuiltInDOMComponent

View File

@ -57,10 +57,13 @@ describe('ssr: components', () => {
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {
if (_scopeId) { if (_push) {
_push(\`hello<div \${_scopeId}></div>\`) _push(\`hello<div\${_scopeId}></div>\`)
} else { } else {
_push(\`hello<div></div>\`) return [
createTextVNode(\\"hello\\"),
createVNode(\\"div\\")
]
} }
}, },
_compiled: true _compiled: true
@ -80,7 +83,13 @@ describe('ssr: components', () => {
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: ({ msg }, _push, _parent, _scopeId) => { default: ({ msg }, _push, _parent, _scopeId) => {
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`) if (_push) {
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
} else {
return [
createTextVNode(toDisplayString(_ctx.msg + _ctx.outer))
]
}
}, },
_compiled: true _compiled: true
}, _parent)) }, _parent))
@ -103,10 +112,22 @@ describe('ssr: components', () => {
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {
_push(\`foo\`) if (_push) {
_push(\`foo\`)
} else {
return [
createTextVNode(\\"foo\\")
]
}
}, },
named: (_, _push, _parent, _scopeId) => { named: (_, _push, _parent, _scopeId) => {
_push(\`bar\`) if (_push) {
_push(\`bar\`)
} else {
return [
createTextVNode(\\"bar\\")
]
}
}, },
_compiled: true _compiled: true
}, _parent)) }, _parent))
@ -131,7 +152,13 @@ describe('ssr: components', () => {
? { ? {
name: \\"named\\", name: \\"named\\",
fn: (_, _push, _parent, _scopeId) => { fn: (_, _push, _parent, _scopeId) => {
_push(\`foo\`) if (_push) {
_push(\`foo\`)
} else {
return [
createTextVNode(\\"foo\\")
]
}
} }
} }
: undefined : undefined

View File

@ -31,7 +31,13 @@ describe('ssr: scopeId', () => {
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {
_push(\`foo\`) if (_push) {
_push(\`foo\`)
} else {
return [
createTextVNode(\\"foo\\")
]
}
}, },
_compiled: true _compiled: true
}, _parent)) }, _parent))
@ -53,10 +59,12 @@ describe('ssr: scopeId', () => {
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {
if (_scopeId) { if (_push) {
_push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`) _push(\`<span data-v-xxxxxxx\${_scopeId}>hello</span>\`)
} else { } else {
_push(\`<span data-v-xxxxxxx>hello</span>\`) return [
createVNode(\\"span\\", null, \\"hello\\")
]
} }
}, },
_compiled: true _compiled: true
@ -80,30 +88,30 @@ describe('ssr: scopeId', () => {
_push(_ssrRenderComponent(_component_foo, null, { _push(_ssrRenderComponent(_component_foo, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {
if (_scopeId) { if (_push) {
_push(\`<span data-v-xxxxxxx \${_scopeId}>hello</span>\`) _push(\`<span data-v-xxxxxxx\${_scopeId}>hello</span>\`)
_push(_ssrRenderComponent(_component_bar, null, { _push(_ssrRenderComponent(_component_bar, null, {
default: (_, _push, _parent, _scopeId) => { default: (_, _push, _parent, _scopeId) => {
if (_scopeId) { if (_push) {
_push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`) _push(\`<span data-v-xxxxxxx\${_scopeId}></span>\`)
} else { } else {
_push(\`<span data-v-xxxxxxx></span>\`) return [
createVNode(\\"span\\")
]
} }
}, },
_compiled: true _compiled: true
}, _parent)) }, _parent))
} else { } else {
_push(\`<span data-v-xxxxxxx>hello</span>\`) return [
_push(_ssrRenderComponent(_component_bar, null, { createVNode(\\"span\\", null, \\"hello\\"),
default: (_, _push, _parent, _scopeId) => { createVNode(_component_bar, null, {
if (_scopeId) { default: () => [
_push(\`<span data-v-xxxxxxx \${_scopeId}></span>\`) createVNode(\\"span\\")
} else { ],
_push(\`<span data-v-xxxxxxx></span>\`) _compiled: true
} })
}, ]
_compiled: true
}, _parent))
} }
}, },
_compiled: true _compiled: true

View File

@ -28,7 +28,7 @@ import { ssrProcessElement } from './transforms/ssrTransformElement'
// passing it to codegen. // passing it to codegen.
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) { export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext(options) const context = createSSRTransformContext(ast, options)
const isFragment = const isFragment =
ast.children.length > 1 && ast.children.some(c => !isText(c)) ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment) processChildren(ast.children, context, isFragment)
@ -46,6 +46,7 @@ export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
export type SSRTransformContext = ReturnType<typeof createSSRTransformContext> export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
function createSSRTransformContext( function createSSRTransformContext(
root: RootNode,
options: CompilerOptions, options: CompilerOptions,
helpers: Set<symbol> = new Set(), helpers: Set<symbol> = new Set(),
withSlotScopeId = false withSlotScopeId = false
@ -54,6 +55,7 @@ function createSSRTransformContext(
let currentString: TemplateLiteral | null = null let currentString: TemplateLiteral | null = null
return { return {
root,
options, options,
body, body,
helpers, helpers,
@ -91,6 +93,7 @@ function createChildContext(
): SSRTransformContext { ): SSRTransformContext {
// ensure child inherits parent helpers // ensure child inherits parent helpers
return createSSRTransformContext( return createSSRTransformContext(
parent.root,
parent.options, parent.options,
parent.helpers, parent.helpers,
withSlotScopeId withSlotScopeId

View File

@ -8,7 +8,6 @@ import {
ComponentNode, ComponentNode,
SlotFnBuilder, SlotFnBuilder,
createFunctionExpression, createFunctionExpression,
createBlockStatement,
buildSlots, buildSlots,
FunctionExpression, FunctionExpression,
TemplateChildNode, TemplateChildNode,
@ -17,7 +16,14 @@ import {
TRANSITION_GROUP, TRANSITION_GROUP,
createIfStatement, createIfStatement,
createSimpleExpression, createSimpleExpression,
isText getDOMTransformPreset,
transform,
createReturnStatement,
ReturnStatement,
Namespaces,
locStub,
RootNode,
TransformContext
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import { import {
@ -25,7 +31,7 @@ import {
processChildren, processChildren,
processChildrenAsStatement processChildrenAsStatement
} from '../ssrCodegenTransform' } from '../ssrCodegenTransform'
import { isSymbol } from '@vue/shared' import { isSymbol, isObject, isArray } from '@vue/shared'
// We need to construct the slot functions in the 1st pass to ensure proper // We need to construct the slot functions in the 1st pass to ensure proper
// scope tracking, but the children of each slot cannot be processed until // scope tracking, but the children of each slot cannot be processed until
@ -36,6 +42,7 @@ const wipMap = new WeakMap<ComponentNode, WIPSlotEntry[]>()
interface WIPSlotEntry { interface WIPSlotEntry {
fn: FunctionExpression fn: FunctionExpression
children: TemplateChildNode[] children: TemplateChildNode[]
vnodeBranch: ReturnStatement
} }
const componentTypeMap = new WeakMap<ComponentNode, symbol>() const componentTypeMap = new WeakMap<ComponentNode, symbol>()
@ -55,26 +62,32 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
return // built-in component: fallthrough return // built-in component: fallthrough
} }
// note we are not passing ssr: true here because for components, v-on
// handlers should still be passed
const props = const props =
node.props.length > 0 ? buildProps(node, context).props || `null` : `null` node.props.length > 0
? // note we are not passing ssr: true here because for components, v-on
// handlers should still be passed
buildProps(node, context).props || `null`
: `null`
const wipEntries: WIPSlotEntry[] = [] const wipEntries: WIPSlotEntry[] = []
wipMap.set(node, wipEntries) wipMap.set(node, wipEntries)
const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => { const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
// An SSR slot function has the signature of
// (props, _push, _parent, _scopeId) => void
// See server-renderer/src/helpers/renderSlot.ts
const fn = createFunctionExpression( const fn = createFunctionExpression(
[props || `_`, `_push`, `_parent`, `_scopeId`], [props || `_`, `_push`, `_parent`, `_scopeId`],
undefined, // no return, assign body later undefined, // no return, assign body later
true, // newline true, // newline
false, // isSlot: pass false since we don't need client scopeId codegen true, // isSlot
loc loc
) )
wipEntries.push({ fn, children }) wipEntries.push({
fn,
children,
// build the children using normal vnode-based transforms
// TODO fixme: `children` here has already been mutated at this point
// so the sub-transform runs into errors :/
vnodeBranch: createVNodeSlotBranch(clone(children), context)
})
return fn return fn
} }
@ -82,9 +95,6 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
? buildSlots(node, context, buildSSRSlotFn).slots ? buildSlots(node, context, buildSSRSlotFn).slots
: `null` : `null`
// TODO option for slots bail out
// TODO scopeId
node.ssrCodegenNode = createCallExpression( node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_COMPONENT), context.helper(SSR_RENDER_COMPONENT),
[component, props, slots, `_parent`] [component, props, slots, `_parent`]
@ -113,26 +123,79 @@ export function ssrProcessComponent(
// finish up slot function expressions from the 1st pass. // finish up slot function expressions from the 1st pass.
const wipEntries = wipMap.get(node) || [] const wipEntries = wipMap.get(node) || []
for (let i = 0; i < wipEntries.length; i++) { for (let i = 0; i < wipEntries.length; i++) {
const { fn, children } = wipEntries[i] const { fn, children, vnodeBranch } = wipEntries[i]
const hasNonTextChild = children.some(c => !isText(c)) // For each slot, we generate two branches: one SSR-optimized branch and
if (hasNonTextChild) { // one normal vnode-based branch. The branches are taken based on the
// SSR slots need to handled potential presence of scopeId of the child // presence of the 2nd `_push` argument (which is only present if the slot
// component. To avoid the cost of concatenation when it's unnecessary, // is called by `_ssrRenderSlot`.
// we split the code into two paths, one with slot scopeId and one without. fn.body = createIfStatement(
fn.body = createBlockStatement([ createSimpleExpression(`_push`, false),
createIfStatement( processChildrenAsStatement(
createSimpleExpression(`_scopeId`, false), children,
// branch with scopeId concatenation context,
processChildrenAsStatement(children, context, false, true), false,
// branch without scopeId concatenation true /* withSlotScopeId */
processChildrenAsStatement(children, context, false, false) ),
) vnodeBranch
]) )
} else {
// only text, no need for scopeId branching.
fn.body = processChildrenAsStatement(children, context)
}
} }
context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode])) context.pushStatement(createCallExpression(`_push`, [node.ssrCodegenNode]))
} }
} }
function createVNodeSlotBranch(
children: TemplateChildNode[],
context: TransformContext
): ReturnStatement {
// we need to process the slot children using client-side transforms.
// in order to do that we need to construct a fresh root.
// in addition, wrap the children with a wrapper template for proper child
// treatment.
const { root } = context
const childRoot: RootNode = {
...root,
children: [
{
type: NodeTypes.ELEMENT,
ns: Namespaces.HTML,
tag: 'template',
tagType: ElementTypes.TEMPLATE,
isSelfClosing: false,
props: [],
children,
loc: locStub,
codegenNode: undefined
}
]
}
const [nodeTransforms, directiveTransforms] = getDOMTransformPreset(true)
transform(childRoot, {
...context, // copy transform options on context
nodeTransforms,
directiveTransforms
})
// merge helpers/components/directives/imports from the childRoot
// back to current root
;(['helpers', 'components', 'directives', 'imports'] as const).forEach(
key => {
root[key] = [...new Set([...root[key], ...childRoot[key]])] as any
}
)
return createReturnStatement(children)
}
function clone(v: any): any {
if (isArray(v)) {
return v.map(clone)
} else if (isObject(v)) {
const res: any = {}
for (const key in v) {
res[key] = v[key]
}
return res
} else {
return v
}
}

View File

@ -309,7 +309,6 @@ export function ssrProcessElement(
// Handle slot scopeId // Handle slot scopeId
if (context.withSlotScopeId) { if (context.withSlotScopeId) {
context.pushStringPart(` `)
context.pushStringPart(createSimpleExpression(`_scopeId`, false)) context.pushStringPart(createSimpleExpression(`_scopeId`, false))
} }

View File

@ -8,6 +8,7 @@ import {
VNode VNode
} from '../vnode' } from '../vnode'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
import { warn } from '../warning'
export function renderSlot( export function renderSlot(
slots: Record<string, Slot>, slots: Record<string, Slot>,
@ -17,7 +18,17 @@ export function renderSlot(
// the compiler and guaranteed to be an array // the compiler and guaranteed to be an array
fallback?: VNodeArrayChildren fallback?: VNodeArrayChildren
): VNode { ): VNode {
const slot = slots[name] let slot = slots[name]
if (__DEV__ && slot.length > 1) {
warn(
`SSR-optimized slot function detected in a non-SSR-optimized render ` +
`function. You need to mark this component with $dynamic-slots in the ` +
`parent template.`
)
slot = () => []
}
return ( return (
openBlock(), openBlock(),
createBlock( createBlock(

View File

@ -25,7 +25,7 @@ export function ssrRenderSlot(
if (slotFn.length > 1) { if (slotFn.length > 1) {
// only ssr-optimized slot fns accept more than 1 arguments // only ssr-optimized slot fns accept more than 1 arguments
const scopeId = parentComponent && parentComponent.type.__scopeId const scopeId = parentComponent && parentComponent.type.__scopeId
slotFn(slotProps, push, parentComponent, scopeId ? scopeId + `-s` : null) slotFn(slotProps, push, parentComponent, scopeId ? ` ${scopeId}-s` : ``)
} else { } else {
// normal slot // normal slot
renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent) renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent)

View File

@ -30,7 +30,7 @@ window.init = () => {
ssrMode.value = persistedState.ssr ssrMode.value = persistedState.ssr
Object.assign(compilerOptions, persistedState.options) Object.assign(compilerOptions, persistedState.options)
let lastSuccessfulCode: string = `/* See console for error */` let lastSuccessfulCode: string
let lastSuccessfulMap: SourceMapConsumer | undefined = undefined let lastSuccessfulMap: SourceMapConsumer | undefined = undefined
function compileCode(source: string): string { function compileCode(source: string): string {
console.clear() console.clear()
@ -57,6 +57,9 @@ window.init = () => {
lastSuccessfulMap = new window._deps['source-map'].SourceMapConsumer(map) lastSuccessfulMap = new window._deps['source-map'].SourceMapConsumer(map)
lastSuccessfulMap!.computeColumnSpans() lastSuccessfulMap!.computeColumnSpans()
} catch (e) { } catch (e) {
lastSuccessfulCode = `/* ERROR: ${
e.message
} (see console for more info) */`
console.error(e) console.error(e)
} }
return lastSuccessfulCode return lastSuccessfulCode