wip(ssr): vdom serialization
This commit is contained in:
@@ -1,29 +1,19 @@
|
||||
describe('ssr: render props', () => {
|
||||
test('class', () => {})
|
||||
|
||||
test('styles', () => {
|
||||
test('style', () => {
|
||||
// only render numbers for properties that allow no unit numbers
|
||||
})
|
||||
|
||||
describe('attrs', () => {
|
||||
test('basic', () => {})
|
||||
test('normal attrs', () => {})
|
||||
|
||||
test('boolean attrs', () => {})
|
||||
test('boolean attrs', () => {})
|
||||
|
||||
test('enumerated attrs', () => {})
|
||||
test('enumerated attrs', () => {})
|
||||
|
||||
test('skip falsy values', () => {})
|
||||
})
|
||||
test('ignore falsy values', () => {})
|
||||
|
||||
describe('domProps', () => {
|
||||
test('innerHTML', () => {})
|
||||
test('props to attrs', () => {})
|
||||
|
||||
test('textContent', () => {})
|
||||
|
||||
test('textarea', () => {})
|
||||
|
||||
test('other renderable domProps', () => {
|
||||
// also test camel to kebab case conversion for some props
|
||||
})
|
||||
})
|
||||
test('ignore non-renderable props', () => {})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
// import { renderToString, renderComponent } from '../src'
|
||||
|
||||
describe('ssr: renderToString', () => {
|
||||
test('basic', () => {})
|
||||
describe('elements', () => {
|
||||
test('text children', () => {})
|
||||
|
||||
test('nested components', () => {})
|
||||
test('array children', () => {})
|
||||
|
||||
test('nested components with optimized slots', () => {})
|
||||
test('void elements', () => {})
|
||||
|
||||
test('mixing optimized / vnode components', () => {})
|
||||
test('innerHTML', () => {})
|
||||
|
||||
test('nested components with vnode slots', () => {})
|
||||
test('textContent', () => {})
|
||||
|
||||
test('async components', () => {})
|
||||
test('textarea value', () => {})
|
||||
})
|
||||
|
||||
test('parallel async components', () => {})
|
||||
describe('components', () => {
|
||||
test('nested components', () => {})
|
||||
|
||||
test('nested components with optimized slots', () => {})
|
||||
|
||||
test('mixing optimized / vnode components', () => {})
|
||||
|
||||
test('nested components with vnode slots', () => {})
|
||||
|
||||
test('async components', () => {})
|
||||
|
||||
test('parallel async components', () => {})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,64 @@
|
||||
export function renderProps() {}
|
||||
import { escape } from './escape'
|
||||
import {
|
||||
normalizeClass,
|
||||
normalizeStyle,
|
||||
propsToAttrMap,
|
||||
hyphenate,
|
||||
isString,
|
||||
isNoUnitNumericStyleProp,
|
||||
isOn,
|
||||
isSSRSafeAttrName,
|
||||
isBooleanAttr
|
||||
} from '@vue/shared/src'
|
||||
|
||||
export function renderClass() {}
|
||||
export function renderProps(
|
||||
props: Record<string, unknown>,
|
||||
isCustomElement: boolean = false
|
||||
): string {
|
||||
let ret = ''
|
||||
for (const key in props) {
|
||||
if (key === 'key' || key === 'ref' || isOn(key)) {
|
||||
continue
|
||||
}
|
||||
const value = props[key]
|
||||
if (key === 'class') {
|
||||
ret += ` class="${renderClass(value)}"`
|
||||
} else if (key === 'style') {
|
||||
ret += ` style="${renderStyle(value)}"`
|
||||
} else if (value != null) {
|
||||
const attrKey = isCustomElement
|
||||
? key
|
||||
: propsToAttrMap[key] || key.toLowerCase()
|
||||
if (isBooleanAttr(attrKey)) {
|
||||
ret += ` ${attrKey}=""`
|
||||
} else if (isSSRSafeAttrName(attrKey)) {
|
||||
ret += ` ${attrKey}="${escape(value)}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
export function renderStyle() {}
|
||||
export function renderClass(raw: unknown): string {
|
||||
return escape(normalizeClass(raw))
|
||||
}
|
||||
|
||||
export function renderStyle(raw: unknown): string {
|
||||
if (!raw) {
|
||||
return ''
|
||||
}
|
||||
const styles = normalizeStyle(raw)
|
||||
let ret = ''
|
||||
for (const key in styles) {
|
||||
const value = styles[key]
|
||||
const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key)
|
||||
if (
|
||||
isString(value) ||
|
||||
(typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey))
|
||||
) {
|
||||
// only render valid values
|
||||
ret += `${normalizedKey}:${value};`
|
||||
}
|
||||
}
|
||||
return escape(ret)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,27 @@ import {
|
||||
Component,
|
||||
ComponentInternalInstance,
|
||||
VNode,
|
||||
VNodeChildren,
|
||||
createComponentInstance,
|
||||
setupComponent,
|
||||
createVNode,
|
||||
renderComponentRoot
|
||||
renderComponentRoot,
|
||||
Text,
|
||||
Comment,
|
||||
Fragment,
|
||||
Portal,
|
||||
ShapeFlags,
|
||||
normalizeVNode
|
||||
} from 'vue'
|
||||
import { isString, isPromise, isArray, isFunction } from '@vue/shared'
|
||||
import {
|
||||
isString,
|
||||
isPromise,
|
||||
isArray,
|
||||
isFunction,
|
||||
isVoidTag
|
||||
} from '@vue/shared'
|
||||
import { renderProps } from './renderProps'
|
||||
import { escape } from './escape'
|
||||
|
||||
// Each component has a buffer array.
|
||||
// A buffer array can contain one of the following:
|
||||
@@ -19,6 +34,7 @@ import { isString, isPromise, isArray, isFunction } from '@vue/shared'
|
||||
type SSRBuffer = SSRBufferItem[]
|
||||
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<ResolvedSSRBuffer>
|
||||
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
||||
type PushFn = (item: SSRBufferItem) => void
|
||||
|
||||
function createBuffer() {
|
||||
let appendable = false
|
||||
@@ -59,39 +75,38 @@ function unrollBuffer(buffer: ResolvedSSRBuffer): string {
|
||||
}
|
||||
|
||||
export async function renderToString(app: App): Promise<string> {
|
||||
const resolvedBuffer = await renderComponent(app._component, app._props)
|
||||
const resolvedBuffer = await renderComponent(
|
||||
createVNode(app._component, app._props)
|
||||
)
|
||||
return unrollBuffer(resolvedBuffer)
|
||||
}
|
||||
|
||||
export function renderComponent(
|
||||
comp: Component,
|
||||
props: Record<string, any> | null = null,
|
||||
children: VNode['children'] = null,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
const vnode = createVNode(comp, props, children)
|
||||
const instance = createComponentInstance(vnode, parentComponent)
|
||||
const res = setupComponent(instance, null)
|
||||
if (isPromise(res)) {
|
||||
return res.then(() => innerRenderComponent(comp, instance))
|
||||
return res.then(() => innerRenderComponent(instance))
|
||||
} else {
|
||||
return innerRenderComponent(comp, instance)
|
||||
return innerRenderComponent(instance)
|
||||
}
|
||||
}
|
||||
|
||||
function innerRenderComponent(
|
||||
comp: Component,
|
||||
instance: ComponentInternalInstance
|
||||
): ResolvedSSRBuffer | Promise<ResolvedSSRBuffer> {
|
||||
const comp = instance.type as Component
|
||||
const { buffer, push, hasAsync } = createBuffer()
|
||||
if (isFunction(comp)) {
|
||||
renderVNode(push, renderComponentRoot(instance))
|
||||
renderVNode(push, renderComponentRoot(instance), instance)
|
||||
} else {
|
||||
if (comp.ssrRender) {
|
||||
// optimized
|
||||
comp.ssrRender(push, instance.proxy)
|
||||
} else if (comp.render) {
|
||||
renderVNode(push, renderComponentRoot(instance))
|
||||
renderVNode(push, renderComponentRoot(instance), instance)
|
||||
} else {
|
||||
// TODO on the fly template compilation support
|
||||
throw new Error(
|
||||
@@ -107,8 +122,103 @@ function innerRenderComponent(
|
||||
return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer)
|
||||
}
|
||||
|
||||
export function renderVNode(push: (item: SSRBufferItem) => void, vnode: VNode) {
|
||||
// TODO
|
||||
export function renderVNode(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
) {
|
||||
const { type, shapeFlag, children } = vnode
|
||||
switch (type) {
|
||||
case Text:
|
||||
push(children as string)
|
||||
break
|
||||
case Comment:
|
||||
push(children ? `<!--${children}-->` : `<!---->`)
|
||||
break
|
||||
case Fragment:
|
||||
push(`<!---->`)
|
||||
renderVNodeChildren(push, children as VNodeChildren, parentComponent)
|
||||
push(`<!---->`)
|
||||
break
|
||||
case Portal:
|
||||
// TODO
|
||||
break
|
||||
default:
|
||||
if (shapeFlag & ShapeFlags.ELEMENT) {
|
||||
renderElement(push, vnode, parentComponent)
|
||||
} else if (shapeFlag & ShapeFlags.COMPONENT) {
|
||||
push(renderComponent(vnode, parentComponent))
|
||||
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
|
||||
// TODO
|
||||
} else {
|
||||
console.warn(
|
||||
'[@vue/server-renderer] Invalid VNode type:',
|
||||
type,
|
||||
`(${typeof type})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderVNodeChildren(
|
||||
push: PushFn,
|
||||
children: VNodeChildren,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
renderVNode(push, normalizeVNode(children[i]), parentComponent)
|
||||
}
|
||||
}
|
||||
|
||||
function renderElement(
|
||||
push: PushFn,
|
||||
vnode: VNode,
|
||||
parentComponent: ComponentInternalInstance | null = null
|
||||
) {
|
||||
const tag = vnode.type as string
|
||||
const { props, children, shapeFlag, scopeId } = vnode
|
||||
let openTag = `<${tag}`
|
||||
|
||||
// TODO directives
|
||||
|
||||
if (props !== null) {
|
||||
openTag += renderProps(props, tag.indexOf(`-`) > 0)
|
||||
}
|
||||
|
||||
if (scopeId !== null) {
|
||||
openTag += ` ${scopeId}`
|
||||
const treeOwnerId = parentComponent && parentComponent.type.__scopeId
|
||||
// vnode's own scopeId and the current rendering component's scopeId is
|
||||
// different - this is a slot content node.
|
||||
if (treeOwnerId != null && treeOwnerId !== scopeId) {
|
||||
openTag += ` ${scopeId}-s`
|
||||
}
|
||||
}
|
||||
|
||||
push(openTag + `>`)
|
||||
if (!isVoidTag(tag)) {
|
||||
let hasChildrenOverride = false
|
||||
if (props !== null) {
|
||||
if (props.innerHTML) {
|
||||
hasChildrenOverride = true
|
||||
push(props.innerHTML)
|
||||
} else if (props.textContent) {
|
||||
hasChildrenOverride = true
|
||||
push(escape(props.textContent))
|
||||
} else if (tag === 'textarea' && props.value) {
|
||||
hasChildrenOverride = true
|
||||
push(escape(props.value))
|
||||
}
|
||||
}
|
||||
if (!hasChildrenOverride) {
|
||||
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||
push(escape(children as string))
|
||||
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||
renderVNodeChildren(push, children as VNodeChildren, parentComponent)
|
||||
}
|
||||
}
|
||||
push(`</${tag}>`)
|
||||
}
|
||||
}
|
||||
|
||||
export function renderSlot() {
|
||||
|
||||
Reference in New Issue
Block a user