Skip to content

Commit 012bc5d

Browse files
committed
wip(ssr): restructure
1 parent d293876 commit 012bc5d

File tree

11 files changed

+223
-143
lines changed

11 files changed

+223
-143
lines changed

packages/runtime-core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export { registerRuntimeCompiler } from './component'
103103

104104
// For server-renderer
105105
export { createComponentInstance, setupComponent } from './component'
106+
export { renderComponentRoot } from './componentRenderUtils'
106107

107108
// Types -----------------------------------------------------------------------
108109

packages/runtime-core/src/vnode.ts

+3-38
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
isString,
55
isObject,
66
EMPTY_ARR,
7-
extend
7+
extend,
8+
normalizeClass,
9+
normalizeStyle
810
} from '@vue/shared'
911
import {
1012
ComponentInternalInstance,
@@ -378,43 +380,6 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
378380
vnode.shapeFlag |= type
379381
}
380382

381-
function normalizeStyle(
382-
value: unknown
383-
): Record<string, string | number> | void {
384-
if (isArray(value)) {
385-
const res: Record<string, string | number> = {}
386-
for (let i = 0; i < value.length; i++) {
387-
const normalized = normalizeStyle(value[i])
388-
if (normalized) {
389-
for (const key in normalized) {
390-
res[key] = normalized[key]
391-
}
392-
}
393-
}
394-
return res
395-
} else if (isObject(value)) {
396-
return value
397-
}
398-
}
399-
400-
export function normalizeClass(value: unknown): string {
401-
let res = ''
402-
if (isString(value)) {
403-
res = value
404-
} else if (isArray(value)) {
405-
for (let i = 0; i < value.length; i++) {
406-
res += normalizeClass(value[i]) + ' '
407-
}
408-
} else if (isObject(value)) {
409-
for (const name in value) {
410-
if (value[name]) {
411-
res += name + ' '
412-
}
413-
}
414-
}
415-
return res.trim()
416-
}
417-
418383
const handlersRE = /^on|^vnode/
419384

420385
export function mergeProps(...args: (Data & VNodeProps)[]) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test('ssr: escape HTML', () => {})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// import { renderToString, renderComponent } from '../src'
2+
3+
describe('ssr: renderToString', () => {
4+
test('basic', () => {})
5+
6+
test('nested components', () => {})
7+
8+
test('nested components with optimized slots', () => {})
9+
10+
test('mixing optimized / vnode components', () => {})
11+
12+
test('nested components with vnode slots', () => {})
13+
14+
test('async components', () => {})
15+
16+
test('parallel async components', () => {})
17+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
describe('ssr: render raw vnodes', () => {
2+
test('class', () => {})
3+
4+
test('styles', () => {
5+
// only render numbers for properties that allow no unit numbers
6+
})
7+
8+
describe('attrs', () => {
9+
test('basic', () => {})
10+
11+
test('boolean attrs', () => {})
12+
13+
test('enumerated attrs', () => {})
14+
15+
test('skip falsy values', () => {})
16+
})
17+
18+
describe('domProps', () => {
19+
test('innerHTML', () => {})
20+
21+
test('textContent', () => {})
22+
23+
test('textarea', () => {})
24+
25+
test('other renderable domProps', () => {
26+
// also test camel to kebab case conversion for some props
27+
})
28+
})
29+
})

packages/server-renderer/src/helpers.ts renamed to packages/server-renderer/src/escape.ts

-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { toDisplayString } from '@vue/shared'
2-
31
const escapeRE = /["'&<>]/
42

53
export function escape(string: unknown) {
@@ -45,7 +43,3 @@ export function escape(string: unknown) {
4543

4644
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
4745
}
48-
49-
export function interpolate(value: unknown) {
50-
return escape(toDisplayString(value))
51-
}

packages/server-renderer/src/index.ts

+11-99
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,16 @@
1-
import {
2-
App,
3-
Component,
4-
ComponentInternalInstance,
5-
createComponentInstance,
6-
setupComponent,
7-
VNode,
8-
createVNode
9-
} from 'vue'
10-
import { isString, isPromise, isArray } from '@vue/shared'
1+
import { toDisplayString } from 'vue'
112

12-
export * from './helpers'
3+
export { renderToString, renderComponent } from './renderToString'
134

14-
type SSRBuffer = SSRBufferItem[]
15-
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
16-
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
5+
export {
6+
renderVNode,
7+
renderClass,
8+
renderStyle,
9+
renderProps
10+
} from './renderVnode'
1711

18-
function createBuffer() {
19-
let appendable = false
20-
let hasAsync = false
21-
const buffer: SSRBuffer = []
22-
return {
23-
buffer,
24-
hasAsync() {
25-
return hasAsync
26-
},
27-
push(item: SSRBufferItem) {
28-
const isStringItem = isString(item)
29-
if (appendable && isStringItem) {
30-
buffer[buffer.length - 1] += item as string
31-
} else {
32-
buffer.push(item)
33-
}
34-
appendable = isStringItem
35-
if (!isStringItem && !isArray(item)) {
36-
// promise
37-
hasAsync = true
38-
}
39-
}
40-
}
41-
}
42-
43-
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
44-
let ret = ''
45-
for (let i = 0; i < buffer.length; i++) {
46-
const item = buffer[i]
47-
if (isString(item)) {
48-
ret += item
49-
} else {
50-
ret += unrollBuffer(item)
51-
}
52-
}
53-
return ret
54-
}
55-
56-
export async function renderToString(app: App): Promise<string> {
57-
const resolvedBuffer = (await renderComponent(
58-
app._component,
59-
app._props
60-
)) as ResolvedSSRBuffer
61-
return unrollBuffer(resolvedBuffer)
62-
}
63-
64-
export function renderComponent(
65-
comp: Component,
66-
props: Record<string, any> | null = null,
67-
children: VNode['children'] = null,
68-
parentComponent: ComponentInternalInstance | null = null
69-
): ResolvedSSRBuffer | Promise<SSRBuffer> {
70-
const vnode = createVNode(comp, props, children)
71-
const instance = createComponentInstance(vnode, parentComponent)
72-
const res = setupComponent(instance, null)
73-
if (isPromise(res)) {
74-
return res.then(() => innerRenderComponent(comp, instance))
75-
} else {
76-
return innerRenderComponent(comp, instance)
77-
}
78-
}
12+
export { escape } from './escape'
7913

80-
function innerRenderComponent(
81-
comp: Component,
82-
instance: ComponentInternalInstance
83-
): ResolvedSSRBuffer | Promise<SSRBuffer> {
84-
const { buffer, push, hasAsync } = createBuffer()
85-
if (typeof comp === 'function') {
86-
// TODO FunctionalComponent
87-
} else {
88-
if (comp.ssrRender) {
89-
// optimized
90-
comp.ssrRender(push, instance.proxy)
91-
} else if (comp.render) {
92-
// TODO fallback to vdom serialization
93-
} else {
94-
// TODO warn component missing render function
95-
}
96-
}
97-
// If the current component's buffer contains any Promise from async children,
98-
// then it must return a Promise too. Otherwise this is a component that
99-
// contains only sync children so we can avoid the async book-keeping overhead.
100-
return hasAsync()
101-
? // TS can't figure out the typing due to recursive appearance of Promise
102-
Promise.all(buffer as any)
103-
: (buffer as ResolvedSSRBuffer)
14+
export function interpolate(value: unknown) {
15+
return escape(toDisplayString(value))
10416
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
App,
3+
Component,
4+
ComponentInternalInstance,
5+
VNode,
6+
createComponentInstance,
7+
setupComponent,
8+
createVNode,
9+
renderComponentRoot
10+
} from 'vue'
11+
import { isString, isPromise, isArray, isFunction } from '@vue/shared'
12+
import { renderVNode } from './renderVnode'
13+
14+
export type SSRBuffer = SSRBufferItem[]
15+
export type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
16+
export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
17+
18+
function createBuffer() {
19+
let appendable = false
20+
let hasAsync = false
21+
const buffer: SSRBuffer = []
22+
return {
23+
buffer,
24+
hasAsync() {
25+
return hasAsync
26+
},
27+
push(item: SSRBufferItem) {
28+
const isStringItem = isString(item)
29+
if (appendable && isStringItem) {
30+
buffer[buffer.length - 1] += item as string
31+
} else {
32+
buffer.push(item)
33+
}
34+
appendable = isStringItem
35+
if (!isStringItem && !isArray(item)) {
36+
// promise
37+
hasAsync = true
38+
}
39+
}
40+
}
41+
}
42+
43+
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
44+
let ret = ''
45+
for (let i = 0; i < buffer.length; i++) {
46+
const item = buffer[i]
47+
if (isString(item)) {
48+
ret += item
49+
} else {
50+
ret += unrollBuffer(item)
51+
}
52+
}
53+
return ret
54+
}
55+
56+
export async function renderToString(app: App): Promise<string> {
57+
const resolvedBuffer = (await renderComponent(
58+
app._component,
59+
app._props
60+
)) as ResolvedSSRBuffer
61+
return unrollBuffer(resolvedBuffer)
62+
}
63+
64+
export function renderComponent(
65+
comp: Component,
66+
props: Record<string, any> | null = null,
67+
children: VNode['children'] = null,
68+
parentComponent: ComponentInternalInstance | null = null
69+
): ResolvedSSRBuffer | Promise<SSRBuffer> {
70+
const vnode = createVNode(comp, props, children)
71+
const instance = createComponentInstance(vnode, parentComponent)
72+
const res = setupComponent(instance, null)
73+
if (isPromise(res)) {
74+
return res.then(() => innerRenderComponent(comp, instance))
75+
} else {
76+
return innerRenderComponent(comp, instance)
77+
}
78+
}
79+
80+
function innerRenderComponent(
81+
comp: Component,
82+
instance: ComponentInternalInstance
83+
): ResolvedSSRBuffer | Promise<SSRBuffer> {
84+
const { buffer, push, hasAsync } = createBuffer()
85+
if (isFunction(comp)) {
86+
renderVNode(push, renderComponentRoot(instance))
87+
} else {
88+
if (comp.ssrRender) {
89+
// optimized
90+
comp.ssrRender(push, instance.proxy)
91+
} else if (comp.render) {
92+
renderVNode(push, renderComponentRoot(instance))
93+
} else {
94+
// TODO on the fly template compilation support
95+
throw new Error(
96+
`Component ${
97+
comp.name ? `${comp.name} ` : ``
98+
} is missing render function.`
99+
)
100+
}
101+
}
102+
// If the current component's buffer contains any Promise from async children,
103+
// then it must return a Promise too. Otherwise this is a component that
104+
// contains only sync children so we can avoid the async book-keeping overhead.
105+
return hasAsync()
106+
? // TS can't figure out the typing due to recursive appearance of Promise
107+
Promise.all(buffer as any)
108+
: (buffer as ResolvedSSRBuffer)
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { VNode } from 'vue'
2+
import { SSRBufferItem } from './renderToString'
3+
4+
export function renderVNode(
5+
push: (item: SSRBufferItem) => void,
6+
vnode: VNode
7+
) {}
8+
9+
export function renderProps() {}
10+
11+
export function renderClass() {}
12+
13+
export function renderStyle() {}

packages/shared/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './globalsWhitelist'
66
export * from './codeframe'
77
export * from './domTagConfig'
88
export * from './mockWarn'
9+
export * from './normalizeProp'
910

1011
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
1112
? Object.freeze({})

0 commit comments

Comments
 (0)