From da255173778860f92ed7985f8cf472147bc15c20 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 23 Jan 2020 21:01:56 -0500 Subject: [PATCH] wip(ssr): initial work on server-renderer --- packages/runtime-core/src/apiCreateApp.ts | 31 ++++---- packages/runtime-core/src/apiOptions.ts | 2 + packages/runtime-core/src/component.ts | 2 +- packages/runtime-core/src/componentProps.ts | 2 +- packages/runtime-core/src/renderer.ts | 4 +- packages/runtime-dom/src/index.ts | 2 +- packages/server-renderer/package.json | 5 +- packages/server-renderer/src/index.ts | 85 ++++++++++++++++++++- 8 files changed, 110 insertions(+), 23 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index a01b5ecb..234e4f7e 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -19,8 +19,11 @@ export interface App { mount(rootContainer: HostElement | string): ComponentPublicInstance unmount(rootContainer: HostElement | string): void provide(key: InjectionKey | 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 { @@ -85,18 +88,21 @@ export type CreateAppFunction = ( export function createAppAPI( render: RootRenderFunction ): CreateAppFunction { - return function createApp( - rootComponent: Component, - rootProps?: Data | null - ): App { + return function createApp(rootComponent: Component, rootProps = null) { + if (rootProps != null && !isObject(rootProps)) { + __DEV__ && warn(`root props passed to app.mount() must be an object.`) + rootProps = null + } + const context = createAppContext() const installedPlugins = new Set() let isMounted = false const app: App = { - rootComponent, - rootContainer: null, + _component: rootComponent, + _props: rootProps, + _container: null, get config() { return context.config @@ -176,11 +182,6 @@ export function createAppAPI( mount(rootContainer: HostElement): any { 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) // store app context on the root VNode. // this will be set on the root instance on initial mount. @@ -195,7 +196,7 @@ export function createAppAPI( render(vnode, rootContainer) isMounted = true - app.rootContainer = rootContainer + app._container = rootContainer return vnode.component!.proxy } else if (__DEV__) { warn( @@ -206,7 +207,7 @@ export function createAppAPI( unmount() { if (isMounted) { - render(null, app.rootContainer!) + render(null, app._container!) } else if (__DEV__) { warn(`Cannot unmount an app that is not mounted.`) } diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index 2de548cf..2cccb32c 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -63,6 +63,8 @@ export interface ComponentOptionsBase< // Luckily `render()` doesn't need any arguments nor does it care about return // type. render?: Function + // SSR only. This is produced by compiler-ssr and attached in compiler-sfc + ssrRender?: Function components?: Record< string, Component | { new (): ComponentPublicInstance } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index b46d10ab..fb0f3d68 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -154,7 +154,7 @@ export interface ComponentInternalInstance { const emptyAppContext = createAppContext() -export function defineComponentInstance( +export function createComponentInstance( vnode: VNode, parent: ComponentInternalInstance | null ) { diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index a0acd982..baad88fd 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -85,7 +85,7 @@ type NormalizedProp = type NormalizedPropsOptions = [Record, string[]] // 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 // component root) // - for the rest: diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 7af49a21..2fb27c41 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -12,7 +12,7 @@ import { } from './vnode' import { ComponentInternalInstance, - defineComponentInstance, + createComponentInstance, setupStatefulComponent, Component, Data @@ -927,7 +927,7 @@ export function createRenderer< parentSuspense: HostSuspenseBoundary | null, isSVG: boolean ) { - const instance: ComponentInternalInstance = (initialVNode.component = defineComponentInstance( + const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode, parentComponent )) diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 51a8e0aa..9c9c1b59 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -39,7 +39,7 @@ export const createApp: CreateAppFunction = (...args) => { return } } - const component = app.rootComponent + const component = app._component if ( __RUNTIME_COMPILE__ && !isFunction(component) && diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index d0e695df..0e8743b4 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -25,5 +25,8 @@ "bugs": { "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" + } } diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 861ee73f..17c5f9be 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -1,3 +1,84 @@ -export function renderToString() { - // TODO +import { + App, + Component, + ComponentInternalInstance, + SuspenseBoundary +} from '@vue/runtime-dom' +import { isString } from '@vue/shared' + +type SSRBuffer = SSRBufferItem[] +type SSRBufferItem = string | Promise +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 { + 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 | null, + parentComponent: ComponentInternalInstance | null, + parentSuspense: SuspenseBoundary | null +): Promise { + // 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) }