wip(ssr): restructure
This commit is contained in:
parent
d293876c34
commit
012bc5df9d
@ -103,6 +103,7 @@ export { registerRuntimeCompiler } from './component'
|
|||||||
|
|
||||||
// For server-renderer
|
// For server-renderer
|
||||||
export { createComponentInstance, setupComponent } from './component'
|
export { createComponentInstance, setupComponent } from './component'
|
||||||
|
export { renderComponentRoot } from './componentRenderUtils'
|
||||||
|
|
||||||
// Types -----------------------------------------------------------------------
|
// Types -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
isString,
|
isString,
|
||||||
isObject,
|
isObject,
|
||||||
EMPTY_ARR,
|
EMPTY_ARR,
|
||||||
extend
|
extend,
|
||||||
|
normalizeClass,
|
||||||
|
normalizeStyle
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
@ -378,43 +380,6 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
|
|||||||
vnode.shapeFlag |= type
|
vnode.shapeFlag |= type
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStyle(
|
|
||||||
value: unknown
|
|
||||||
): Record<string, string | number> | void {
|
|
||||||
if (isArray(value)) {
|
|
||||||
const res: Record<string, string | number> = {}
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
const normalized = normalizeStyle(value[i])
|
|
||||||
if (normalized) {
|
|
||||||
for (const key in normalized) {
|
|
||||||
res[key] = normalized[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
} else if (isObject(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeClass(value: unknown): string {
|
|
||||||
let res = ''
|
|
||||||
if (isString(value)) {
|
|
||||||
res = value
|
|
||||||
} else if (isArray(value)) {
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
res += normalizeClass(value[i]) + ' '
|
|
||||||
}
|
|
||||||
} else if (isObject(value)) {
|
|
||||||
for (const name in value) {
|
|
||||||
if (value[name]) {
|
|
||||||
res += name + ' '
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlersRE = /^on|^vnode/
|
const handlersRE = /^on|^vnode/
|
||||||
|
|
||||||
export function mergeProps(...args: (Data & VNodeProps)[]) {
|
export function mergeProps(...args: (Data & VNodeProps)[]) {
|
||||||
|
1
packages/server-renderer/__tests__/escape.spec.ts
Normal file
1
packages/server-renderer/__tests__/escape.spec.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
test('ssr: escape HTML', () => {})
|
17
packages/server-renderer/__tests__/renderToString.spec.ts
Normal file
17
packages/server-renderer/__tests__/renderToString.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// import { renderToString, renderComponent } from '../src'
|
||||||
|
|
||||||
|
describe('ssr: renderToString', () => {
|
||||||
|
test('basic', () => {})
|
||||||
|
|
||||||
|
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', () => {})
|
||||||
|
})
|
29
packages/server-renderer/__tests__/renderVnode.spec.ts
Normal file
29
packages/server-renderer/__tests__/renderVnode.spec.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
describe('ssr: render raw vnodes', () => {
|
||||||
|
test('class', () => {})
|
||||||
|
|
||||||
|
test('styles', () => {
|
||||||
|
// only render numbers for properties that allow no unit numbers
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('attrs', () => {
|
||||||
|
test('basic', () => {})
|
||||||
|
|
||||||
|
test('boolean attrs', () => {})
|
||||||
|
|
||||||
|
test('enumerated attrs', () => {})
|
||||||
|
|
||||||
|
test('skip falsy values', () => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('domProps', () => {
|
||||||
|
test('innerHTML', () => {})
|
||||||
|
|
||||||
|
test('textContent', () => {})
|
||||||
|
|
||||||
|
test('textarea', () => {})
|
||||||
|
|
||||||
|
test('other renderable domProps', () => {
|
||||||
|
// also test camel to kebab case conversion for some props
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,5 +1,3 @@
|
|||||||
import { toDisplayString } from '@vue/shared'
|
|
||||||
|
|
||||||
const escapeRE = /["'&<>]/
|
const escapeRE = /["'&<>]/
|
||||||
|
|
||||||
export function escape(string: unknown) {
|
export function escape(string: unknown) {
|
||||||
@ -45,7 +43,3 @@ export function escape(string: unknown) {
|
|||||||
|
|
||||||
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
|
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
|
||||||
}
|
}
|
||||||
|
|
||||||
export function interpolate(value: unknown) {
|
|
||||||
return escape(toDisplayString(value))
|
|
||||||
}
|
|
@ -1,104 +1,16 @@
|
|||||||
import {
|
import { toDisplayString } from 'vue'
|
||||||
App,
|
|
||||||
Component,
|
|
||||||
ComponentInternalInstance,
|
|
||||||
createComponentInstance,
|
|
||||||
setupComponent,
|
|
||||||
VNode,
|
|
||||||
createVNode
|
|
||||||
} from 'vue'
|
|
||||||
import { isString, isPromise, isArray } from '@vue/shared'
|
|
||||||
|
|
||||||
export * from './helpers'
|
export { renderToString, renderComponent } from './renderToString'
|
||||||
|
|
||||||
type SSRBuffer = SSRBufferItem[]
|
export {
|
||||||
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
|
renderVNode,
|
||||||
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
renderClass,
|
||||||
|
renderStyle,
|
||||||
|
renderProps
|
||||||
|
} from './renderVnode'
|
||||||
|
|
||||||
function createBuffer() {
|
export { escape } from './escape'
|
||||||
let appendable = false
|
|
||||||
let hasAsync = false
|
|
||||||
const buffer: SSRBuffer = []
|
|
||||||
return {
|
|
||||||
buffer,
|
|
||||||
hasAsync() {
|
|
||||||
return hasAsync
|
|
||||||
},
|
|
||||||
push(item: SSRBufferItem) {
|
|
||||||
const isStringItem = isString(item)
|
|
||||||
if (appendable && isStringItem) {
|
|
||||||
buffer[buffer.length - 1] += item as string
|
|
||||||
} else {
|
|
||||||
buffer.push(item)
|
|
||||||
}
|
|
||||||
appendable = isStringItem
|
|
||||||
if (!isStringItem && !isArray(item)) {
|
|
||||||
// promise
|
|
||||||
hasAsync = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
|
export function interpolate(value: unknown) {
|
||||||
let ret = ''
|
return escape(toDisplayString(value))
|
||||||
for (let i = 0; i < buffer.length; i++) {
|
|
||||||
const item = buffer[i]
|
|
||||||
if (isString(item)) {
|
|
||||||
ret += item
|
|
||||||
} else {
|
|
||||||
ret += unrollBuffer(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderToString(app: App): Promise<string> {
|
|
||||||
const resolvedBuffer = (await renderComponent(
|
|
||||||
app._component,
|
|
||||||
app._props
|
|
||||||
)) as ResolvedSSRBuffer
|
|
||||||
return unrollBuffer(resolvedBuffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderComponent(
|
|
||||||
comp: Component,
|
|
||||||
props: Record<string, any> | null = null,
|
|
||||||
children: VNode['children'] = null,
|
|
||||||
parentComponent: ComponentInternalInstance | null = null
|
|
||||||
): ResolvedSSRBuffer | Promise<SSRBuffer> {
|
|
||||||
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))
|
|
||||||
} else {
|
|
||||||
return innerRenderComponent(comp, instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function innerRenderComponent(
|
|
||||||
comp: Component,
|
|
||||||
instance: ComponentInternalInstance
|
|
||||||
): ResolvedSSRBuffer | Promise<SSRBuffer> {
|
|
||||||
const { buffer, push, hasAsync } = createBuffer()
|
|
||||||
if (typeof comp === 'function') {
|
|
||||||
// TODO FunctionalComponent
|
|
||||||
} else {
|
|
||||||
if (comp.ssrRender) {
|
|
||||||
// optimized
|
|
||||||
comp.ssrRender(push, instance.proxy)
|
|
||||||
} else if (comp.render) {
|
|
||||||
// TODO fallback to vdom serialization
|
|
||||||
} else {
|
|
||||||
// TODO warn component missing render function
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the current component's buffer contains any Promise from async children,
|
|
||||||
// then it must return a Promise too. Otherwise this is a component that
|
|
||||||
// contains only sync children so we can avoid the async book-keeping overhead.
|
|
||||||
return hasAsync()
|
|
||||||
? // TS can't figure out the typing due to recursive appearance of Promise
|
|
||||||
Promise.all(buffer as any)
|
|
||||||
: (buffer as ResolvedSSRBuffer)
|
|
||||||
}
|
}
|
||||||
|
109
packages/server-renderer/src/renderToString.ts
Normal file
109
packages/server-renderer/src/renderToString.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import {
|
||||||
|
App,
|
||||||
|
Component,
|
||||||
|
ComponentInternalInstance,
|
||||||
|
VNode,
|
||||||
|
createComponentInstance,
|
||||||
|
setupComponent,
|
||||||
|
createVNode,
|
||||||
|
renderComponentRoot
|
||||||
|
} from 'vue'
|
||||||
|
import { isString, isPromise, isArray, isFunction } from '@vue/shared'
|
||||||
|
import { renderVNode } from './renderVnode'
|
||||||
|
|
||||||
|
export type SSRBuffer = SSRBufferItem[]
|
||||||
|
export type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
|
||||||
|
export type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
|
||||||
|
|
||||||
|
function createBuffer() {
|
||||||
|
let appendable = false
|
||||||
|
let hasAsync = false
|
||||||
|
const buffer: SSRBuffer = []
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
hasAsync() {
|
||||||
|
return hasAsync
|
||||||
|
},
|
||||||
|
push(item: SSRBufferItem) {
|
||||||
|
const isStringItem = isString(item)
|
||||||
|
if (appendable && isStringItem) {
|
||||||
|
buffer[buffer.length - 1] += item as string
|
||||||
|
} else {
|
||||||
|
buffer.push(item)
|
||||||
|
}
|
||||||
|
appendable = isStringItem
|
||||||
|
if (!isStringItem && !isArray(item)) {
|
||||||
|
// promise
|
||||||
|
hasAsync = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unrollBuffer(buffer: ResolvedSSRBuffer): string {
|
||||||
|
let ret = ''
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
const item = buffer[i]
|
||||||
|
if (isString(item)) {
|
||||||
|
ret += item
|
||||||
|
} else {
|
||||||
|
ret += unrollBuffer(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderToString(app: App): Promise<string> {
|
||||||
|
const resolvedBuffer = (await renderComponent(
|
||||||
|
app._component,
|
||||||
|
app._props
|
||||||
|
)) as ResolvedSSRBuffer
|
||||||
|
return unrollBuffer(resolvedBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderComponent(
|
||||||
|
comp: Component,
|
||||||
|
props: Record<string, any> | null = null,
|
||||||
|
children: VNode['children'] = null,
|
||||||
|
parentComponent: ComponentInternalInstance | null = null
|
||||||
|
): ResolvedSSRBuffer | Promise<SSRBuffer> {
|
||||||
|
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))
|
||||||
|
} else {
|
||||||
|
return innerRenderComponent(comp, instance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function innerRenderComponent(
|
||||||
|
comp: Component,
|
||||||
|
instance: ComponentInternalInstance
|
||||||
|
): ResolvedSSRBuffer | Promise<SSRBuffer> {
|
||||||
|
const { buffer, push, hasAsync } = createBuffer()
|
||||||
|
if (isFunction(comp)) {
|
||||||
|
renderVNode(push, renderComponentRoot(instance))
|
||||||
|
} else {
|
||||||
|
if (comp.ssrRender) {
|
||||||
|
// optimized
|
||||||
|
comp.ssrRender(push, instance.proxy)
|
||||||
|
} else if (comp.render) {
|
||||||
|
renderVNode(push, renderComponentRoot(instance))
|
||||||
|
} else {
|
||||||
|
// TODO on the fly template compilation support
|
||||||
|
throw new Error(
|
||||||
|
`Component ${
|
||||||
|
comp.name ? `${comp.name} ` : ``
|
||||||
|
} is missing render function.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the current component's buffer contains any Promise from async children,
|
||||||
|
// then it must return a Promise too. Otherwise this is a component that
|
||||||
|
// contains only sync children so we can avoid the async book-keeping overhead.
|
||||||
|
return hasAsync()
|
||||||
|
? // TS can't figure out the typing due to recursive appearance of Promise
|
||||||
|
Promise.all(buffer as any)
|
||||||
|
: (buffer as ResolvedSSRBuffer)
|
||||||
|
}
|
13
packages/server-renderer/src/renderVnode.ts
Normal file
13
packages/server-renderer/src/renderVnode.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { VNode } from 'vue'
|
||||||
|
import { SSRBufferItem } from './renderToString'
|
||||||
|
|
||||||
|
export function renderVNode(
|
||||||
|
push: (item: SSRBufferItem) => void,
|
||||||
|
vnode: VNode
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export function renderProps() {}
|
||||||
|
|
||||||
|
export function renderClass() {}
|
||||||
|
|
||||||
|
export function renderStyle() {}
|
@ -6,6 +6,7 @@ export * from './globalsWhitelist'
|
|||||||
export * from './codeframe'
|
export * from './codeframe'
|
||||||
export * from './domTagConfig'
|
export * from './domTagConfig'
|
||||||
export * from './mockWarn'
|
export * from './mockWarn'
|
||||||
|
export * from './normalizeProp'
|
||||||
|
|
||||||
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
|
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
|
||||||
? Object.freeze({})
|
? Object.freeze({})
|
||||||
|
38
packages/shared/src/normalizeProp.ts
Normal file
38
packages/shared/src/normalizeProp.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { isArray, isString, isObject } from './'
|
||||||
|
|
||||||
|
export function normalizeStyle(
|
||||||
|
value: unknown
|
||||||
|
): Record<string, string | number> | void {
|
||||||
|
if (isArray(value)) {
|
||||||
|
const res: Record<string, string | number> = {}
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const normalized = normalizeStyle(value[i])
|
||||||
|
if (normalized) {
|
||||||
|
for (const key in normalized) {
|
||||||
|
res[key] = normalized[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
} else if (isObject(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeClass(value: unknown): string {
|
||||||
|
let res = ''
|
||||||
|
if (isString(value)) {
|
||||||
|
res = value
|
||||||
|
} else if (isArray(value)) {
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
res += normalizeClass(value[i]) + ' '
|
||||||
|
}
|
||||||
|
} else if (isObject(value)) {
|
||||||
|
for (const name in value) {
|
||||||
|
if (value[name]) {
|
||||||
|
res += name + ' '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.trim()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user