wip(ssr): basic components

This commit is contained in:
Evan You 2020-02-05 23:07:23 -05:00
parent 27e2e482e9
commit ee5ed73361
20 changed files with 254 additions and 132 deletions

View File

@ -353,17 +353,21 @@ function genModulePreamble(
function genAssets( function genAssets(
assets: string[], assets: string[],
type: 'component' | 'directive', type: 'component' | 'directive',
context: CodegenContext { helper, push, newline }: CodegenContext
) { ) {
const resolver = context.helper( const resolver = helper(
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE
) )
for (let i = 0; i < assets.length; i++) { for (let i = 0; i < assets.length; i++) {
const id = assets[i] const id = assets[i]
context.push( push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})` `const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`
) )
context.newline() if (i < assets.length - 1) {
newline()
} else {
push(`\n`)
}
} }
} }

View File

@ -34,7 +34,7 @@ export { transformOn } from './transforms/vOn'
export { transformBind } from './transforms/vBind' export { transformBind } from './transforms/vBind'
// exported for compiler-ssr // exported for compiler-ssr
export { MERGE_PROPS } from './runtimeHelpers' 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 {
@ -42,7 +42,7 @@ export {
processExpression processExpression
} from './transforms/transformExpression' } from './transforms/transformExpression'
export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot' export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
export { buildProps } from './transforms/transformElement' export { resolveComponentType, buildProps } from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet' export { processSlotOutlet } from './transforms/transformSlotOutlet'
// utility, but need to rewrite typing to avoid dts relying on @vue/shared // utility, but need to rewrite typing to avoid dts relying on @vue/shared

View File

@ -465,7 +465,8 @@ function parseTag(
} else if ( } else if (
isCoreComponent(tag) || isCoreComponent(tag) ||
(options.isBuiltInComponent && options.isBuiltInComponent(tag)) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
/^[A-Z]/.test(tag) /^[A-Z]/.test(tag) ||
tag === 'component'
) { ) {
tagType = ElementTypes.COMPONENT tagType = ElementTypes.COMPONENT
} }

View File

@ -14,7 +14,8 @@ import {
createSimpleExpression, createSimpleExpression,
createObjectExpression, createObjectExpression,
Property, Property,
createSequenceExpression createSequenceExpression,
ComponentNode
} from '../ast' } from '../ast'
import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared' import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
@ -35,7 +36,8 @@ import {
getInnerRange, getInnerRange,
toValidAssetId, toValidAssetId,
findProp, findProp,
isCoreComponent isCoreComponent,
isBindKey
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { isStaticNode } from './hoistStatic' import { isStaticNode } from './hoistStatic'
@ -58,69 +60,30 @@ export const transformElement: NodeTransform = (node, context) => {
// perform the work on exit, after all child expressions have been // perform the work on exit, after all child expressions have been
// processed and merged. // processed and merged.
return function postTransformElement() { return function postTransformElement() {
const { tag, tagType, props } = node const { tag, props } = node
const builtInComponentSymbol = const isComponent = node.tagType === ElementTypes.COMPONENT
isCoreComponent(tag) || context.isBuiltInComponent(tag)
const isComponent = tagType === ElementTypes.COMPONENT // <svg> and <foreignObject> must be forced into blocks so that block
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
const shouldUseBlock =
!isComponent && (tag === 'svg' || tag === 'foreignObject')
const nodeType = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`
const args: CallExpression['arguments'] = [nodeType]
let hasProps = props.length > 0 let hasProps = props.length > 0
let patchFlag: number = 0 let patchFlag: number = 0
let runtimeDirectives: DirectiveNode[] | undefined let runtimeDirectives: DirectiveNode[] | undefined
let dynamicPropNames: string[] | undefined let dynamicPropNames: string[] | undefined
let dynamicComponent: string | CallExpression | undefined
let shouldUseBlock = false
// handle dynamic component
const isProp = tag === 'component' && findProp(node, 'is')
if (isProp) {
// static <component is="foo" />
if (isProp.type === NodeTypes.ATTRIBUTE) {
const tag = isProp.value && isProp.value.content
if (tag) {
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
dynamicComponent = toValidAssetId(tag, `component`)
}
}
// dynamic <component :is="asdf" />
else if (isProp.exp) {
dynamicComponent = createCallExpression(
context.helper(RESOLVE_DYNAMIC_COMPONENT),
// _ctx.$ exposes the owner instance of current render function
[isProp.exp, context.prefixIdentifiers ? `_ctx.$` : `$`]
)
}
}
let nodeType
if (dynamicComponent) {
nodeType = dynamicComponent
} else if (builtInComponentSymbol) {
nodeType = context.helper(builtInComponentSymbol)
} else if (isComponent) {
// user component w/ resolve
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
nodeType = toValidAssetId(tag, `component`)
} else {
// plain element
nodeType = `"${tag}"`
// <svg> and <foreignObject> must be forced into blocks so that block
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
shouldUseBlock = tag === 'svg' || tag === 'foreignObject'
}
const args: CallExpression['arguments'] = [nodeType]
// props // props
if (hasProps) { if (hasProps) {
const propsBuildResult = buildProps( const propsBuildResult = buildProps(node, context)
node,
context,
// skip reserved "is" prop <component is>
isProp ? node.props.filter(p => p !== isProp) : node.props
)
patchFlag = propsBuildResult.patchFlag patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames dynamicPropNames = propsBuildResult.dynamicPropNames
runtimeDirectives = propsBuildResult.directives runtimeDirectives = propsBuildResult.directives
@ -130,6 +93,7 @@ export const transformElement: NodeTransform = (node, context) => {
args.push(propsBuildResult.props) args.push(propsBuildResult.props)
} }
} }
// children // children
const hasChildren = node.children.length > 0 const hasChildren = node.children.length > 0
if (hasChildren) { if (hasChildren) {
@ -140,11 +104,7 @@ export const transformElement: NodeTransform = (node, context) => {
// Portal is not a real component has dedicated handling in the renderer // Portal is not a real component has dedicated handling in the renderer
// KeepAlive should not track its own deps so that it can be used inside // KeepAlive should not track its own deps so that it can be used inside
// Transition // Transition
if ( if (isComponent && nodeType !== PORTAL && nodeType !== KEEP_ALIVE) {
isComponent &&
builtInComponentSymbol !== PORTAL &&
builtInComponentSymbol !== KEEP_ALIVE
) {
const { slots, hasDynamicSlots } = buildSlots(node, context) const { slots, hasDynamicSlots } = buildSlots(node, context)
args.push(slots) args.push(slots)
if (hasDynamicSlots) { if (hasDynamicSlots) {
@ -171,6 +131,7 @@ export const transformElement: NodeTransform = (node, context) => {
args.push(node.children) args.push(node.children)
} }
} }
// patchFlag & dynamicPropNames // patchFlag & dynamicPropNames
if (patchFlag !== 0) { if (patchFlag !== 0) {
if (!hasChildren) { if (!hasChildren) {
@ -219,13 +180,45 @@ export const transformElement: NodeTransform = (node, context) => {
} }
} }
function stringifyDynamicPropNames(props: string[]): string { export function resolveComponentType(
let propsNamesString = `[` node: ComponentNode,
for (let i = 0, l = props.length; i < l; i++) { context: TransformContext
propsNamesString += JSON.stringify(props[i]) ) {
if (i < l - 1) propsNamesString += ', ' const { tag } = node
// 1. dynamic component
const isProp = node.tag === 'component' && findProp(node, 'is')
if (isProp) {
// static <component is="foo" />
if (isProp.type === NodeTypes.ATTRIBUTE) {
const isType = isProp.value && isProp.value.content
if (isType) {
context.helper(RESOLVE_COMPONENT)
context.components.add(isType)
return toValidAssetId(isType, `component`)
} }
return propsNamesString + `]` }
// dynamic <component :is="asdf" />
else if (isProp.exp) {
return createCallExpression(
context.helper(RESOLVE_DYNAMIC_COMPONENT),
// _ctx.$ exposes the owner instance of current render function
[isProp.exp, context.prefixIdentifiers ? `_ctx.$` : `$`]
)
}
}
// 2. built-in components (Portal, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
if (builtIn) {
context.helper(builtIn)
return builtIn
}
// 3. user component (resolve)
context.helper(RESOLVE_COMPONENT)
context.components.add(tag)
return toValidAssetId(tag, `component`)
} }
export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode export type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
@ -241,7 +234,7 @@ export function buildProps(
patchFlag: number patchFlag: number
dynamicPropNames: string[] dynamicPropNames: string[]
} { } {
const elementLoc = node.loc const { tag, loc: elementLoc } = node
const isComponent = node.tagType === ElementTypes.COMPONENT const isComponent = node.tagType === ElementTypes.COMPONENT
let properties: ObjectExpression['properties'] = [] let properties: ObjectExpression['properties'] = []
const mergeArgs: PropsExpression[] = [] const mergeArgs: PropsExpression[] = []
@ -288,6 +281,10 @@ export function buildProps(
if (name === 'ref') { if (name === 'ref') {
hasRef = true hasRef = true
} }
// skip :is on <component>
if (name === 'is' && tag === 'component') {
continue
}
properties.push( properties.push(
createObjectProperty( createObjectProperty(
createSimpleExpression( createSimpleExpression(
@ -305,6 +302,8 @@ export function buildProps(
} else { } else {
// directives // directives
const { name, arg, exp, loc } = prop const { name, arg, exp, loc } = prop
const isBind = name === 'bind'
const isOn = name === 'on'
// skip v-slot - it is handled by its dedicated transform. // skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') { if (name === 'slot') {
@ -315,17 +314,16 @@ export function buildProps(
} }
continue continue
} }
// skip v-once - it is handled by its dedicated transform. // skip v-once - it is handled by its dedicated transform.
if (name === 'once') { if (name === 'once') {
continue continue
} }
// skip :is on <component>
const isBind = name === 'bind' if (isBind && tag === 'component' && isBindKey(arg, 'is')) {
const isOn = name === 'on' continue
}
// skip v-on in SSR compilation // skip v-on in SSR compilation
if (ssr && isOn) { if (isOn && ssr) {
continue continue
} }
@ -518,3 +516,12 @@ function buildDirectiveArgs(
} }
return createArrayExpression(dirArgs, dir.loc) return createArrayExpression(dirArgs, dir.loc)
} }
function stringifyDynamicPropNames(props: string[]): string {
let propsNamesString = `[`
for (let i = 0, l = props.length; i < l; i++) {
propsNamesString += JSON.stringify(props[i])
if (i < l - 1) propsNamesString += ', '
}
return propsNamesString + `]`
}

View File

@ -44,10 +44,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
const eventName = arg const eventName = arg
? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic ? arg.type === NodeTypes.SIMPLE_EXPRESSION && arg.isStatic
? `onUpdate:${arg.content}` ? `onUpdate:${arg.content}`
: createCompoundExpression([ : createCompoundExpression(['"onUpdate:" + ', arg])
'"onUpdate:" + ',
...(arg.type === NodeTypes.SIMPLE_EXPRESSION ? [arg] : arg.children)
])
: `onUpdate:modelValue` : `onUpdate:modelValue`
const props = [ const props = [

View File

@ -192,19 +192,21 @@ export function findProp(
if (p.name === name && p.value) { if (p.name === name && p.value) {
return p return p
} }
} else if ( } else if (p.name === 'bind' && p.exp && isBindKey(p.arg, name)) {
p.name === 'bind' &&
p.arg &&
p.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
p.arg.isStatic &&
p.arg.content === name &&
p.exp
) {
return p return p
} }
} }
} }
export function isBindKey(arg: DirectiveNode['arg'], name: string): boolean {
return !!(
arg &&
arg.type === NodeTypes.SIMPLE_EXPRESSION &&
arg.isStatic &&
arg.content === name
)
}
export function hasDynamicKeyVBind(node: ElementNode): boolean { export function hasDynamicKeyVBind(node: ElementNode): boolean {
return node.props.some( return node.props.some(
p => p =>

View File

@ -0,0 +1,48 @@
import { compile } from '../src'
describe('ssr: components', () => {
test('basic', () => {
expect(compile(`<foo id="a" :prop="b" />`).code).toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\")
const { _renderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = resolveComponent(\\"foo\\")
_renderComponent(_component_foo, {
id: \\"a\\",
prop: _ctx.b
}, null, _parent)
}"
`)
})
test('dynamic component', () => {
expect(compile(`<component is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\")
const { _renderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_foo = resolveComponent(\\"foo\\")
_renderComponent(_component_foo, { prop: \\"b\\" }, null, _parent)
}"
`)
expect(compile(`<compoonent :is="foo" prop="b" />`).code)
.toMatchInlineSnapshot(`
"const { resolveComponent } = require(\\"vue\\")
const { _renderComponent } = require(\\"@vue/server-renderer\\")
return function ssrRender(_ctx, _push, _parent) {
const _component_compoonent = resolveComponent(\\"compoonent\\")
_renderComponent(_component_compoonent, {
is: _ctx.foo,
prop: \\"b\\"
}, null, _parent)
}"
`)
})
})

View File

@ -18,6 +18,7 @@ import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
import { ssrProcessIf } from './transforms/ssrVIf' import { ssrProcessIf } from './transforms/ssrVIf'
import { ssrProcessFor } from './transforms/ssrVFor' import { ssrProcessFor } from './transforms/ssrVFor'
import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet' import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrProcessComponent } from './transforms/ssrTransformComponent'
// Because SSR codegen output is completely different from client-side output // Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal // (e.g. multiple elements can be concatenated into a single template literal
@ -118,7 +119,7 @@ export function processChildren(
context.pushStringPart(`</${child.tag}>`) context.pushStringPart(`</${child.tag}>`)
} }
} else if (child.tagType === ElementTypes.COMPONENT) { } else if (child.tagType === ElementTypes.COMPONENT) {
// TODO ssrProcessComponent(child, context)
} else if (child.tagType === ElementTypes.SLOT) { } else if (child.tagType === ElementTypes.SLOT) {
ssrProcessSlotOutlet(child, context) ssrProcessSlotOutlet(child, context)
} }

View File

@ -1,15 +1,64 @@
import { NodeTransform, NodeTypes, ElementTypes } from '@vue/compiler-dom' import {
NodeTransform,
NodeTypes,
ElementTypes,
createCallExpression,
resolveComponentType,
buildProps,
ComponentNode,
PORTAL,
SUSPENSE
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import { SSRTransformContext } from '../ssrCodegenTransform'
import { isSymbol } from '@vue/shared'
export const ssrTransformComponent: NodeTransform = (node, context) => { export const ssrTransformComponent: NodeTransform = (node, context) => {
if ( if (
node.type === NodeTypes.ELEMENT && node.type !== NodeTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT node.tagType !== ElementTypes.COMPONENT
) { ) {
return
}
return function ssrPostTransformComponent() { return function ssrPostTransformComponent() {
// generate a _push(_renderComponent) call const component = resolveComponentType(node, context)
// dynamic component as well
// !check if we need to bail out for slots if (isSymbol(component)) {
// TODO also handle scopeID here // built-in compoonent
if (component === PORTAL) {
// 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
// handlers should still be passed
const { props } = buildProps(node, context)
// TODO slots
// TODO option for slots bail out
// TODO scopeId
node.ssrCodegenNode = createCallExpression(
context.helper(SSR_RENDER_COMPONENT),
[
component,
props || `null`,
`null`, // TODO slots
`_parent`
]
)
}
}
export function ssrProcessComponent(
node: ComponentNode,
context: SSRTransformContext
) {
context.pushStatement(node.ssrCodegenNode!)
} }

View File

@ -21,7 +21,8 @@ import {
createAssignmentExpression, createAssignmentExpression,
TextNode, TextNode,
hasDynamicKeyVBind, hasDynamicKeyVBind,
MERGE_PROPS MERGE_PROPS,
isBindKey
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared' import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors' import { createSSRCompilerError, SSRErrorCodes } from '../errors'
@ -261,10 +262,7 @@ function isTextareaWithValue(
return !!( return !!(
node.tag === 'textarea' && node.tag === 'textarea' &&
prop.name === 'bind' && prop.name === 'bind' &&
prop.arg && isBindKey(prop.arg, 'value')
prop.arg.type === NodeTypes.SIMPLE_EXPRESSION &&
prop.arg.isStatic &&
prop.arg.content === 'value'
) )
} }

View File

@ -19,6 +19,13 @@ import { warn } from './warning'
// resolveComponent, resolveDirective) during render // resolveComponent, resolveDirective) during render
export let currentRenderingInstance: ComponentInternalInstance | null = null export let currentRenderingInstance: ComponentInternalInstance | null = null
// exposed for server-renderer only
export function setCurrentRenderingInstance(
instance: ComponentInternalInstance | null
) {
currentRenderingInstance = instance
}
// dev only flag to track whether $attrs was used during render. // dev only flag to track whether $attrs was used during render.
// If $attrs was used during render then the warning for failed attrs // If $attrs was used during render then the warning for failed attrs
// fallthrough can be suppressed. // fallthrough can be suppressed.

View File

@ -101,7 +101,10 @@ export { registerRuntimeCompiler } from './component'
// SSR ------------------------------------------------------------------------- // SSR -------------------------------------------------------------------------
import { createComponentInstance, setupComponent } from './component' import { createComponentInstance, setupComponent } from './component'
import { renderComponentRoot } from './componentRenderUtils' import {
renderComponentRoot,
setCurrentRenderingInstance
} from './componentRenderUtils'
import { isVNode, normalizeVNode } from './vnode' import { isVNode, normalizeVNode } from './vnode'
// SSR utils are only exposed in cjs builds. // SSR utils are only exposed in cjs builds.
@ -109,6 +112,7 @@ const _ssrUtils = {
createComponentInstance, createComponentInstance,
setupComponent, setupComponent,
renderComponentRoot, renderComponentRoot,
setCurrentRenderingInstance,
isVNode, isVNode,
normalizeVNode normalizeVNode
} }

View File

@ -27,6 +27,7 @@ import { SSRSlots } from './helpers/renderSlot'
const { const {
isVNode, isVNode,
createComponentInstance, createComponentInstance,
setCurrentRenderingInstance,
setupComponent, setupComponent,
renderComponentRoot, renderComponentRoot,
normalizeVNode normalizeVNode
@ -135,7 +136,10 @@ function renderComponentSubTree(
} else { } else {
if (comp.ssrRender) { if (comp.ssrRender) {
// optimized // optimized
// set current rendering instance for asset resoolution
setCurrentRenderingInstance(instance)
comp.ssrRender(instance.proxy, push, instance) comp.ssrRender(instance.proxy, push, instance)
setCurrentRenderingInstance(null)
} else if (comp.render) { } else if (comp.render) {
renderVNode(push, renderComponentRoot(instance), instance) renderVNode(push, renderComponentRoot(instance), instance)
} else { } else {