wip(ssr): scopeId in slots

This commit is contained in:
Evan You
2020-02-06 17:45:34 -05:00
parent 797cc18967
commit 7984a135ca
9 changed files with 239 additions and 69 deletions

View File

@@ -13,12 +13,13 @@ import {
IfStatement,
CallExpression
} from '@vue/compiler-dom'
import { isString, escapeHtml, NO } from '@vue/shared'
import { isString, escapeHtml } from '@vue/shared'
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
import { ssrProcessIf } from './transforms/ssrVIf'
import { ssrProcessFor } from './transforms/ssrVFor'
import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrProcessComponent } from './transforms/ssrTransformComponent'
import { ssrProcessElement } from './transforms/ssrTransformElement'
// Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal
@@ -29,7 +30,7 @@ import { ssrProcessComponent } from './transforms/ssrTransformComponent'
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext(options)
const isFragment =
ast.children.length > 1 && !ast.children.every(c => isText(c))
ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment)
ast.codegenNode = createBlockStatement(context.body)
@@ -46,7 +47,8 @@ export type SSRTransformContext = ReturnType<typeof createSSRTransformContext>
function createSSRTransformContext(
options: CompilerOptions,
helpers: Set<symbol> = new Set()
helpers: Set<symbol> = new Set(),
withSlotScopeId = false
) {
const body: BlockStatement['body'] = []
let currentString: TemplateLiteral | null = null
@@ -55,6 +57,7 @@ function createSSRTransformContext(
options,
body,
helpers,
withSlotScopeId,
helper<T extends symbol>(name: T): T {
helpers.add(name)
return name
@@ -82,11 +85,16 @@ function createSSRTransformContext(
}
}
export function createChildContext(
parent: SSRTransformContext
function createChildContext(
parent: SSRTransformContext,
withSlotScopeId = parent.withSlotScopeId
): SSRTransformContext {
// ensure child inherits parent helpers
return createSSRTransformContext(parent.options, parent.helpers)
return createSSRTransformContext(
parent.options,
parent.helpers,
withSlotScopeId
)
}
export function processChildren(
@@ -97,23 +105,11 @@ export function processChildren(
if (asFragment) {
context.pushStringPart(`<!---->`)
}
const isVoidTag = context.options.isVoidTag || NO
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.tagType === ElementTypes.ELEMENT) {
const elementsToAdd = child.ssrCodegenNode!.elements
for (let j = 0; j < elementsToAdd.length; j++) {
context.pushStringPart(elementsToAdd[j])
}
if (child.children.length) {
processChildren(child.children, context)
}
if (!isVoidTag(child.tag)) {
// push closing tag
context.pushStringPart(`</${child.tag}>`)
}
ssrProcessElement(child, context)
} else if (child.tagType === ElementTypes.COMPONENT) {
ssrProcessComponent(child, context)
} else if (child.tagType === ElementTypes.SLOT) {
@@ -135,3 +131,14 @@ export function processChildren(
context.pushStringPart(`<!---->`)
}
}
export function processChildrenAsStatement(
children: TemplateChildNode[],
parentContext: SSRTransformContext,
asFragment = false,
withSlotScopeId = parentContext.withSlotScopeId
): BlockStatement {
const childContext = createChildContext(parentContext, withSlotScopeId)
processChildren(children, childContext, asFragment)
return createBlockStatement(childContext.body)
}

View File

@@ -14,13 +14,16 @@ import {
TemplateChildNode,
PORTAL,
SUSPENSE,
TRANSITION_GROUP
TRANSITION_GROUP,
createIfStatement,
createSimpleExpression,
isText
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import {
SSRTransformContext,
createChildContext,
processChildren
processChildren,
processChildrenAsStatement
} from '../ssrCodegenTransform'
import { isSymbol } from '@vue/shared'
@@ -62,10 +65,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
// An SSR slot function has the signature of
// (props, _push, _parent) => void
// (props, _push, _parent, _scopeId) => void
// See server-renderer/src/helpers/renderSlot.ts
const fn = createFunctionExpression(
[props || `_`, `_push`, `_parent`],
[props || `_`, `_push`, `_parent`, `_scopeId`],
undefined, // no return, assign body later
true, // newline
false, // isSlot: pass false since we don't need client scopeId codegen
@@ -111,9 +114,24 @@ export function ssrProcessComponent(
const wipEntries = wipMap.get(node) || []
for (let i = 0; i < wipEntries.length; i++) {
const { fn, children } = wipEntries[i]
const childContext = createChildContext(context)
processChildren(children, childContext)
fn.body = createBlockStatement(childContext.body)
const hasNonTextChild = children.some(c => !isText(c))
if (hasNonTextChild) {
// SSR slots need to handled potential presence of scopeId of the child
// component. To avoid the cost of concatenation when it's unnecessary,
// we split the code into two paths, one with slot scopeId and one without.
fn.body = createBlockStatement([
createIfStatement(
createSimpleExpression(`_scopeId`, false),
// branch with scopeId concatenation
processChildrenAsStatement(children, context, false, true),
// branch without scopeId concatenation
processChildrenAsStatement(children, context, false, false)
)
])
} else {
// only text, no need for scopeId branching.
fn.body = processChildrenAsStatement(children, context)
}
}
context.pushStatement(node.ssrCodegenNode)
}

View File

@@ -24,7 +24,7 @@ import {
MERGE_PROPS,
isBindKey
} from '@vue/compiler-dom'
import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
import { escapeHtml, isBooleanAttr, isSSRSafeAttrName, NO } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
import {
SSR_RENDER_ATTR,
@@ -35,6 +35,14 @@ import {
SSR_INTERPOLATE,
SSR_GET_DYNAMIC_MODEL_PROPS
} from '../runtimeHelpers'
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
// for directives with children overwrite (e.g. v-html & v-text), we need to
// store the raw children so that they can be added in the 2nd pass.
const rawChildrenMap = new WeakMap<
PlainElementNode,
TemplateLiteral['elements'][0]
>()
export const ssrTransformElement: NodeTransform = (node, context) => {
if (
@@ -45,7 +53,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// element
// generate the template literal representing the open tag.
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
let rawChildren
// v-bind="obj" or v-bind:[key] can potentially overwrite other static
// attrs and can affect final rendering result, so when they are present
@@ -70,10 +77,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
props
)
const existingText = node.children[0] as TextNode | undefined
node.children = []
rawChildren = createCallExpression(
context.helper(SSR_INTERPOLATE),
[
rawChildrenMap.set(
node,
createCallExpression(context.helper(SSR_INTERPOLATE), [
createConditionalExpression(
createSimpleExpression(`"value" in ${tempId}`, false),
createSimpleExpression(`${tempId}.value`, false),
@@ -83,7 +89,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
),
false
)
]
])
)
} else if (node.tag === 'input') {
// <input v-bind="obj" v-model>
@@ -126,8 +132,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// special cases with children override
if (prop.type === NodeTypes.DIRECTIVE) {
if (prop.name === 'html' && prop.exp) {
node.children = []
rawChildren = prop.exp
rawChildrenMap.set(node, prop.exp)
} else if (prop.name === 'text' && prop.exp) {
node.children = [createInterpolation(prop.exp, prop.loc)]
} else if (prop.name === 'slot') {
@@ -225,8 +230,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
} else {
// special case: value on <textarea>
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
node.children = []
rawChildren = escapeHtml(prop.value.content)
rawChildrenMap.set(node, escapeHtml(prop.value.content))
} else if (!hasDynamicVBind) {
// static prop
if (prop.name === 'class' && prop.value) {
@@ -250,10 +254,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
openTag.push(` ${context.scopeId}`)
}
openTag.push(`>`)
if (rawChildren) {
openTag.push(rawChildren)
}
node.ssrCodegenNode = createTemplateLiteral(openTag)
}
}
@@ -296,3 +296,35 @@ function findVModel(node: PlainElementNode): DirectiveNode | undefined {
p => p.type === NodeTypes.DIRECTIVE && p.name === 'model' && p.exp
) as DirectiveNode | undefined
}
export function ssrProcessElement(
node: PlainElementNode,
context: SSRTransformContext
) {
const isVoidTag = context.options.isVoidTag || NO
const elementsToAdd = node.ssrCodegenNode!.elements
for (let j = 0; j < elementsToAdd.length; j++) {
context.pushStringPart(elementsToAdd[j])
}
// Handle slot scopeId
if (context.withSlotScopeId) {
context.pushStringPart(` `)
context.pushStringPart(createSimpleExpression(`_scopeId`, false))
}
// close open tag
context.pushStringPart(`>`)
const rawChildren = rawChildrenMap.get(node)
if (rawChildren) {
context.pushStringPart(rawChildren)
} else if (node.children.length) {
processChildren(node.children, context)
}
if (!isVoidTag(node.tag)) {
// push closing tag
context.pushStringPart(`</${node.tag}>`)
}
}

View File

@@ -4,14 +4,12 @@ import {
processSlotOutlet,
createCallExpression,
SlotOutletNode,
createFunctionExpression,
createBlockStatement
createFunctionExpression
} from '@vue/compiler-dom'
import { SSR_RENDER_SLOT } from '../runtimeHelpers'
import {
SSRTransformContext,
createChildContext,
processChildren
processChildrenAsStatement
} from '../ssrCodegenTransform'
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
@@ -38,10 +36,8 @@ export function ssrProcessSlotOutlet(
const renderCall = node.ssrCodegenNode!
// has fallback content
if (node.children.length) {
const childContext = createChildContext(context)
processChildren(node.children, childContext)
const fallbackRenderFn = createFunctionExpression([])
fallbackRenderFn.body = createBlockStatement(childContext.body)
fallbackRenderFn.body = processChildrenAsStatement(node.children, context)
// _renderSlot(slots, name, props, fallback, ...)
renderCall.arguments[3] = fallbackRenderFn
}

View File

@@ -5,13 +5,11 @@ import {
createCallExpression,
createFunctionExpression,
createForLoopParams,
createBlockStatement,
NodeTypes
} from '@vue/compiler-dom'
import {
SSRTransformContext,
createChildContext,
processChildren
processChildrenAsStatement
} from '../ssrCodegenTransform'
import { SSR_RENDER_LIST } from '../runtimeHelpers'
@@ -24,14 +22,16 @@ export const ssrTransformFor = createStructuralDirectiveTransform(
// This is called during the 2nd transform pass to construct the SSR-sepcific
// codegen nodes.
export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
const childContext = createChildContext(context)
const needFragmentWrapper =
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
processChildren(node.children, childContext, needFragmentWrapper)
const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult)
)
renderLoop.body = createBlockStatement(childContext.body)
renderLoop.body = processChildrenAsStatement(
node.children,
context,
needFragmentWrapper
)
// v-for always renders a fragment
context.pushStringPart(`<!---->`)

View File

@@ -11,8 +11,7 @@ import {
} from '@vue/compiler-dom'
import {
SSRTransformContext,
createChildContext,
processChildren
processChildrenAsStatement
} from '../ssrCodegenTransform'
// Plugin for the first transform pass, which simply constructs the AST node
@@ -63,7 +62,5 @@ function processIfBranch(
(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)
const childContext = createChildContext(context)
processChildren(children, childContext, needFragmentWrapper)
return createBlockStatement(childContext.body)
return processChildrenAsStatement(children, context, needFragmentWrapper)
}