wip(ssr): initial work on server-renderer

This commit is contained in:
Evan You 2020-01-23 21:01:56 -05:00
parent c07751fd36
commit da25517377
8 changed files with 110 additions and 23 deletions

View File

@ -19,8 +19,11 @@ export interface App<HostElement = any> {
mount(rootContainer: HostElement | string): ComponentPublicInstance mount(rootContainer: HostElement | string): ComponentPublicInstance
unmount(rootContainer: HostElement | string): void unmount(rootContainer: HostElement | string): void
provide<T>(key: InjectionKey<T> | string, value: T): this provide<T>(key: InjectionKey<T> | string, value: T): this
rootComponent: Component
rootContainer: HostElement | null // internal. We need to expose these for the server-renderer
_component: Component
_props: Data | null
_container: HostElement | null
} }
export interface AppConfig { export interface AppConfig {
@ -85,18 +88,21 @@ export type CreateAppFunction<HostElement> = (
export function createAppAPI<HostNode, HostElement>( export function createAppAPI<HostNode, HostElement>(
render: RootRenderFunction<HostNode, HostElement> render: RootRenderFunction<HostNode, HostElement>
): CreateAppFunction<HostElement> { ): CreateAppFunction<HostElement> {
return function createApp( return function createApp(rootComponent: Component, rootProps = null) {
rootComponent: Component, if (rootProps != null && !isObject(rootProps)) {
rootProps?: Data | null __DEV__ && warn(`root props passed to app.mount() must be an object.`)
): App { rootProps = null
}
const context = createAppContext() const context = createAppContext()
const installedPlugins = new Set() const installedPlugins = new Set()
let isMounted = false let isMounted = false
const app: App = { const app: App = {
rootComponent, _component: rootComponent,
rootContainer: null, _props: rootProps,
_container: null,
get config() { get config() {
return context.config return context.config
@ -176,11 +182,6 @@ export function createAppAPI<HostNode, HostElement>(
mount(rootContainer: HostElement): any { mount(rootContainer: HostElement): any {
if (!isMounted) { if (!isMounted) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ &&
warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const vnode = createVNode(rootComponent, rootProps) const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode. // store app context on the root VNode.
// this will be set on the root instance on initial mount. // this will be set on the root instance on initial mount.
@ -195,7 +196,7 @@ export function createAppAPI<HostNode, HostElement>(
render(vnode, rootContainer) render(vnode, rootContainer)
isMounted = true isMounted = true
app.rootContainer = rootContainer app._container = rootContainer
return vnode.component!.proxy return vnode.component!.proxy
} else if (__DEV__) { } else if (__DEV__) {
warn( warn(
@ -206,7 +207,7 @@ export function createAppAPI<HostNode, HostElement>(
unmount() { unmount() {
if (isMounted) { if (isMounted) {
render(null, app.rootContainer!) render(null, app._container!)
} else if (__DEV__) { } else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`) warn(`Cannot unmount an app that is not mounted.`)
} }

View File

@ -63,6 +63,8 @@ export interface ComponentOptionsBase<
// Luckily `render()` doesn't need any arguments nor does it care about return // Luckily `render()` doesn't need any arguments nor does it care about return
// type. // type.
render?: Function render?: Function
// SSR only. This is produced by compiler-ssr and attached in compiler-sfc
ssrRender?: Function
components?: Record< components?: Record<
string, string,
Component | { new (): ComponentPublicInstance<any, any, any, any, any> } Component | { new (): ComponentPublicInstance<any, any, any, any, any> }

View File

@ -154,7 +154,7 @@ export interface ComponentInternalInstance {
const emptyAppContext = createAppContext() const emptyAppContext = createAppContext()
export function defineComponentInstance( export function createComponentInstance(
vnode: VNode, vnode: VNode,
parent: ComponentInternalInstance | null parent: ComponentInternalInstance | null
) { ) {

View File

@ -85,7 +85,7 @@ type NormalizedProp =
type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]] type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
// resolve raw VNode data. // resolve raw VNode data.
// - filter out reserved keys (key, ref, slots) // - filter out reserved keys (key, ref)
// - extract class and style into $attrs (to be merged onto child // - extract class and style into $attrs (to be merged onto child
// component root) // component root)
// - for the rest: // - for the rest:

View File

@ -12,7 +12,7 @@ import {
} from './vnode' } from './vnode'
import { import {
ComponentInternalInstance, ComponentInternalInstance,
defineComponentInstance, createComponentInstance,
setupStatefulComponent, setupStatefulComponent,
Component, Component,
Data Data
@ -927,7 +927,7 @@ export function createRenderer<
parentSuspense: HostSuspenseBoundary | null, parentSuspense: HostSuspenseBoundary | null,
isSVG: boolean isSVG: boolean
) { ) {
const instance: ComponentInternalInstance = (initialVNode.component = defineComponentInstance( const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode, initialVNode,
parentComponent parentComponent
)) ))

View File

@ -39,7 +39,7 @@ export const createApp: CreateAppFunction<Element> = (...args) => {
return return
} }
} }
const component = app.rootComponent const component = app._component
if ( if (
__RUNTIME_COMPILE__ && __RUNTIME_COMPILE__ &&
!isFunction(component) && !isFunction(component) &&

View File

@ -25,5 +25,8 @@
"bugs": { "bugs": {
"url": "https://github.com/vuejs/vue/issues" "url": "https://github.com/vuejs/vue/issues"
}, },
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme" "homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme",
"peerDependencies": {
"@vue/runtime-dom": "3.0.0-alpha.3"
}
} }

View File

@ -1,3 +1,84 @@
export function renderToString() { import {
// TODO App,
Component,
ComponentInternalInstance,
SuspenseBoundary
} from '@vue/runtime-dom'
import { isString } from '@vue/shared'
type SSRBuffer = SSRBufferItem[]
type SSRBufferItem = string | Promise<SSRBuffer>
type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[]
function createSSRBuffer() {
let appendable = false
const buffer: SSRBuffer = []
return {
buffer,
push(item: SSRBufferItem) {
const isStringItem = isString(item)
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item as string
} else {
buffer.push(item)
}
appendable = isStringItem
}
}
}
export async function renderToString(app: App): Promise<string> {
const resolvedBuffer = (await renderComponent(
app._component,
app._props,
null,
null
)) as ResolvedSSRBuffer
return unrollBuffer(resolvedBuffer)
}
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 renderComponent(
comp: Component,
props: Record<string, any> | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null
): Promise<SSRBuffer> {
// 1. create component buffer
const { buffer, push } = createSSRBuffer()
// 2. TODO create actual instance
const instance = {
proxy: {
msg: 'hello'
}
}
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
}
}
// TS can't figure this out due to recursive occurance of Promise in type
// @ts-ignore
return Promise.all(buffer)
} }