wip(compiler-ssr): built-in component fallthrough

This commit is contained in:
Evan You 2020-02-06 15:29:02 -05:00
parent 9cfbab0686
commit 3c27bf6133
10 changed files with 129 additions and 61 deletions

View File

@ -79,6 +79,8 @@ function createCodegenContext(
sourceMap = false, sourceMap = false,
filename = `template.vue.html`, filename = `template.vue.html`,
scopeId = null, scopeId = null,
runtimeGlobalName = `Vue`,
runtimeModuleName = `vue`,
ssr = false ssr = false
}: CodegenOptions }: CodegenOptions
): CodegenContext { ): CodegenContext {
@ -88,6 +90,8 @@ function createCodegenContext(
sourceMap, sourceMap,
filename, filename,
scopeId, scopeId,
runtimeGlobalName,
runtimeModuleName,
ssr, ssr,
source: ast.loc.source, source: ast.loc.source,
code: ``, code: ``,
@ -275,8 +279,18 @@ export function generate(
} }
function genFunctionPreamble(ast: RootNode, context: CodegenContext) { function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const { ssr, helper, prefixIdentifiers, push, newline } = context const {
const VueBinding = ssr ? `require("vue")` : `Vue` ssr,
helper,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
const VueBinding = ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
// Generate const declaration for helpers // Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done // In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the // only once; But if we not prefixing, we place the declaration inside the
@ -319,7 +333,7 @@ function genModulePreamble(
context: CodegenContext, context: CodegenContext,
genScopeId: boolean genScopeId: boolean
) { ) {
const { push, helper, newline, scopeId } = context const { push, helper, newline, scopeId, runtimeModuleName } = context
// generate import statements for helpers // generate import statements for helpers
if (genScopeId) { if (genScopeId) {
ast.helpers.push(WITH_SCOPE_ID) ast.helpers.push(WITH_SCOPE_ID)
@ -328,7 +342,11 @@ function genModulePreamble(
} }
} }
if (ast.helpers.length) { if (ast.helpers.length) {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`) push(
`import { ${ast.helpers.map(helper).join(', ')} } from ${JSON.stringify(
runtimeModuleName
)}\n`
)
} }
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) { if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
push( push(

View File

@ -71,6 +71,9 @@ export interface CodegenOptions {
scopeId?: string | null scopeId?: string | null
// we need to know about this to generate proper preambles // we need to know about this to generate proper preambles
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
// for specifying where to import helpers
runtimeModuleName?: string
runtimeGlobalName?: string
// generate ssr-specific code? // generate ssr-specific code?
ssr?: boolean ssr?: boolean
} }

View File

@ -182,7 +182,8 @@ export const transformElement: NodeTransform = (node, context) => {
export function resolveComponentType( export function resolveComponentType(
node: ComponentNode, node: ComponentNode,
context: TransformContext context: TransformContext,
ssr = false
) { ) {
const { tag } = node const { tag } = node
@ -211,7 +212,9 @@ export function resolveComponentType(
// 2. built-in components (Portal, Transition, KeepAlive, Suspense...) // 2. built-in components (Portal, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) { if (builtIn) {
context.helper(builtIn) // built-ins are simply fallthroughs / have special handling during ssr
// no we don't need to import their runtime equivalents
if (!ssr) context.helper(builtIn)
return builtIn return builtIn
} }

View File

@ -22,6 +22,14 @@ export const parserOptions = __BROWSER__
? parserOptionsMinimal ? parserOptionsMinimal
: parserOptionsStandard : parserOptionsStandard
export const isBuiltInDOMComponent = (tag: string): symbol | undefined => {
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
}
export function compile( export function compile(
template: string, template: string,
options: CompilerOptions = {} options: CompilerOptions = {}
@ -39,13 +47,7 @@ export function compile(
show: transformShow, show: transformShow,
...(options.directiveTransforms || {}) ...(options.directiveTransforms || {})
}, },
isBuiltInComponent: tag => { isBuiltInComponent: isBuiltInDOMComponent
if (isBuiltInType(tag, `Transition`)) {
return TRANSITION
} else if (isBuiltInType(tag, `TransitionGroup`)) {
return TRANSITION_GROUP
}
}
}) })
} }
@ -56,6 +58,7 @@ export function parse(template: string, options: ParserOptions = {}): RootNode {
}) })
} }
export * from './runtimeHelpers'
export { transformStyle } from './transforms/transformStyle' export { transformStyle } from './transforms/transformStyle'
export { createDOMCompilerError, DOMErrorCodes } from './errors' export { createDOMCompilerError, DOMErrorCodes } from './errors'
export * from '@vue/compiler-core' export * from '@vue/compiler-core'

View File

@ -161,5 +161,47 @@ describe('ssr: components', () => {
}" }"
`) `)
}) })
test('built-in fallthroughs', () => {
// no fragment
expect(compile(`<transition><div/></transition>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
_push(\`<div></div>\`)
}"
`)
// wrap with fragment
expect(compile(`<transition-group><div/></transition-group>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
_push(\`<!----><div></div><!---->\`)
}"
`)
// no fragment
expect(compile(`<keep-alive><foo/></keep-alive>`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\")
const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = resolveComponent(\\"foo\\")
_ssrRenderComponent(_component_foo, null, null, _parent)
}"
`)
// wrap with fragment
expect(compile(`<suspense><div/></suspense>`).code)
.toMatchInlineSnapshot(`
"
return function ssrRender(_ctx, _push, _parent) {
_push(\`<!----><div></div><!---->\`)
}"
`)
})
}) })
}) })

View File

@ -10,7 +10,8 @@ import {
trackSlotScopes, trackSlotScopes,
noopDirectiveTransform, noopDirectiveTransform,
transformBind, transformBind,
transformStyle transformStyle,
isBuiltInDOMComponent
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { ssrCodegenTransform } from './ssrCodegenTransform' import { ssrCodegenTransform } from './ssrCodegenTransform'
import { ssrTransformElement } from './transforms/ssrTransformElement' import { ssrTransformElement } from './transforms/ssrTransformElement'
@ -64,7 +65,8 @@ export function compile(
cloak: noopDirectiveTransform, cloak: noopDirectiveTransform,
once: noopDirectiveTransform, once: noopDirectiveTransform,
...(options.directiveTransforms || {}) // user transforms ...(options.directiveTransforms || {}) // user transforms
} },
isBuiltInComponent: isBuiltInDOMComponent
}) })
// traverse the template AST and convert into SSR codegen AST // traverse the template AST and convert into SSR codegen AST

View File

@ -28,17 +28,9 @@ import { ssrProcessComponent } from './transforms/ssrTransformComponent'
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) { export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext(options) const context = createSSRTransformContext(options)
const isFragment = const isFragment =
ast.children.length > 1 && !ast.children.every(c => isText(c)) ast.children.length > 1 && !ast.children.every(c => isText(c))
if (isFragment) { processChildren(ast.children, context, isFragment)
context.pushStringPart(`<!---->`)
}
processChildren(ast.children, context)
if (isFragment) {
context.pushStringPart(`<!---->`)
}
ast.codegenNode = createBlockStatement(context.body) ast.codegenNode = createBlockStatement(context.body)
// Finalize helpers. // Finalize helpers.
@ -99,8 +91,12 @@ export function createChildContext(
export function processChildren( export function processChildren(
children: TemplateChildNode[], children: TemplateChildNode[],
context: SSRTransformContext context: SSRTransformContext,
asFragment = false
) { ) {
if (asFragment) {
context.pushStringPart(`<!---->`)
}
const isVoidTag = context.options.isVoidTag || NO const isVoidTag = context.options.isVoidTag || NO
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
@ -135,4 +131,7 @@ export function processChildren(
ssrProcessFor(child, context) ssrProcessFor(child, context)
} }
} }
if (asFragment) {
context.pushStringPart(`<!---->`)
}
} }

View File

@ -6,14 +6,15 @@ import {
resolveComponentType, resolveComponentType,
buildProps, buildProps,
ComponentNode, ComponentNode,
PORTAL,
SUSPENSE,
SlotFnBuilder, SlotFnBuilder,
createFunctionExpression, createFunctionExpression,
createBlockStatement, createBlockStatement,
buildSlots, buildSlots,
FunctionExpression, FunctionExpression,
TemplateChildNode TemplateChildNode,
PORTAL,
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 {
@ -34,6 +35,8 @@ interface WIPSlotEntry {
children: TemplateChildNode[] children: TemplateChildNode[]
} }
const componentTypeMap = new WeakMap<ComponentNode, symbol>()
export const ssrTransformComponent: NodeTransform = (node, context) => { export const ssrTransformComponent: NodeTransform = (node, context) => {
if ( if (
node.type !== NodeTypes.ELEMENT || node.type !== NodeTypes.ELEMENT ||
@ -43,18 +46,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
} }
return function ssrPostTransformComponent() { return function ssrPostTransformComponent() {
const component = resolveComponentType(node, context) const component = resolveComponentType(node, context, true /* ssr */)
if (isSymbol(component)) { if (isSymbol(component)) {
// built-in compoonent componentTypeMap.set(node, component)
if (component === PORTAL) { return // built-in component: fallthrough
// TODO
} else if (component === SUSPENSE) {
// TODO fallthrough
// TODO option to use fallback content and resolve on client
} else {
// TODO fallthrough for KeepAlive & Transition
}
} }
// note we are not passing ssr: true here because for components, v-on // note we are not passing ssr: true here because for components, v-on
@ -98,6 +93,20 @@ export function ssrProcessComponent(
node: ComponentNode, node: ComponentNode,
context: SSRTransformContext context: SSRTransformContext
) { ) {
if (!node.ssrCodegenNode) {
// this is a built-in component that fell-through.
// just render its children.
const component = componentTypeMap.get(node)!
if (component === PORTAL) {
// TODO
return
}
const needFragmentWrapper =
component === SUSPENSE || component === TRANSITION_GROUP
processChildren(node.children, context, needFragmentWrapper)
} else {
// 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++) {
@ -106,5 +115,6 @@ export function ssrProcessComponent(
processChildren(children, childContext) processChildren(children, childContext)
fn.body = createBlockStatement(childContext.body) fn.body = createBlockStatement(childContext.body)
} }
context.pushStatement(node.ssrCodegenNode!) context.pushStatement(node.ssrCodegenNode)
}
} }

View File

@ -27,13 +27,7 @@ export function ssrProcessFor(node: ForNode, context: SSRTransformContext) {
const childContext = createChildContext(context) const childContext = createChildContext(context)
const needFragmentWrapper = const needFragmentWrapper =
node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT
if (needFragmentWrapper) { processChildren(node.children, childContext, needFragmentWrapper)
childContext.pushStringPart(`<!---->`)
}
processChildren(node.children, childContext)
if (needFragmentWrapper) {
childContext.pushStringPart(`<!---->`)
}
const renderLoop = createFunctionExpression( const renderLoop = createFunctionExpression(
createForLoopParams(node.parseResult) createForLoopParams(node.parseResult)
) )

View File

@ -64,12 +64,6 @@ function processIfBranch(
// optimize away nested fragments when the only child is a ForNode // optimize away nested fragments when the only child is a ForNode
!(children.length === 1 && children[0].type === NodeTypes.FOR) !(children.length === 1 && children[0].type === NodeTypes.FOR)
const childContext = createChildContext(context) const childContext = createChildContext(context)
if (needFragmentWrapper) { processChildren(children, childContext, needFragmentWrapper)
childContext.pushStringPart(`<!---->`)
}
processChildren(children, childContext)
if (needFragmentWrapper) {
childContext.pushStringPart(`<!---->`)
}
return createBlockStatement(childContext.body) return createBlockStatement(childContext.body)
} }