wip(ssr): restructure

This commit is contained in:
Evan You 2020-01-27 17:23:42 -05:00
parent d293876c34
commit 012bc5df9d
11 changed files with 224 additions and 144 deletions

View File

@ -103,6 +103,7 @@ export { registerRuntimeCompiler } from './component'
// For server-renderer
export { createComponentInstance, setupComponent } from './component'
export { renderComponentRoot } from './componentRenderUtils'
// Types -----------------------------------------------------------------------

View File

@ -4,7 +4,9 @@ import {
isString,
isObject,
EMPTY_ARR,
extend
extend,
normalizeClass,
normalizeStyle
} from '@vue/shared'
import {
ComponentInternalInstance,
@ -378,43 +380,6 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
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/
export function mergeProps(...args: (Data & VNodeProps)[]) {

View File

@ -0,0 +1 @@
test('ssr: escape HTML', () => {})

View 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', () => {})
})

View 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
})
})
})

View File

@ -1,5 +1,3 @@
import { toDisplayString } from '@vue/shared'
const escapeRE = /["'&<>]/
export function escape(string: unknown) {
@ -45,7 +43,3 @@ export function escape(string: unknown) {
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
}
export function interpolate(value: unknown) {
return escape(toDisplayString(value))
}

View File

@ -1,104 +1,16 @@
import {
App,
Component,
ComponentInternalInstance,
createComponentInstance,
setupComponent,
VNode,
createVNode
} from 'vue'
import { isString, isPromise, isArray } from '@vue/shared'
import { toDisplayString } from 'vue'
export * from './helpers'
export { renderToString, renderComponent } from './renderToString'
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | ResolvedSSRBuffer | Promise<SSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
export {
renderVNode,
renderClass,
renderStyle,
renderProps
} from './renderVnode'
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 (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)
export { escape } from './escape'
export function interpolate(value: unknown) {
return escape(toDisplayString(value))
}

View 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)
}

View 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() {}

View File

@ -6,6 +6,7 @@ export * from './globalsWhitelist'
export * from './codeframe'
export * from './domTagConfig'
export * from './mockWarn'
export * from './normalizeProp'
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})

View 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()
}