Просмотр исходного кода

wip(ssr): initial work on server-renderer

Evan You 6 лет назад
Родитель
Сommit
da25517377

+ 16 - 15
packages/runtime-core/src/apiCreateApp.ts

@@ -19,8 +19,11 @@ export interface App<HostElement = any> {
   mount(rootContainer: HostElement | string): ComponentPublicInstance
   unmount(rootContainer: HostElement | string): void
   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 {
@@ -85,18 +88,21 @@ export type CreateAppFunction<HostElement> = (
 export function createAppAPI<HostNode, HostElement>(
   render: RootRenderFunction<HostNode, HostElement>
 ): CreateAppFunction<HostElement> {
-  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<HostNode, HostElement>(
 
       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<HostNode, HostElement>(
 
           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<HostNode, HostElement>(
 
       unmount() {
         if (isMounted) {
-          render(null, app.rootContainer!)
+          render(null, app._container!)
         } else if (__DEV__) {
           warn(`Cannot unmount an app that is not mounted.`)
         }

+ 2 - 0
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<any, any, any, any, any> }

+ 1 - 1
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
 ) {

+ 1 - 1
packages/runtime-core/src/componentProps.ts

@@ -85,7 +85,7 @@ type NormalizedProp =
 type NormalizedPropsOptions = [Record<string, NormalizedProp>, 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:

+ 2 - 2
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
     ))

+ 1 - 1
packages/runtime-dom/src/index.ts

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

+ 4 - 1
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"
+  }
 }

+ 83 - 2
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<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)
 }