]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): component hydration
authorEvan You <yyx990803@gmail.com>
Fri, 14 Feb 2020 04:31:03 +0000 (23:31 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 14 Feb 2020 04:31:03 +0000 (23:31 -0500)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/hydration.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/index.ts

index c810fcbc1cc651abd25a099142598c5fb3bce652..e8888487f75e2cf1e078efbf2f3ae5cffbf6724c 100644 (file)
@@ -6,7 +6,7 @@ import { RootRenderFunction } from './renderer'
 import { InjectionKey } from './apiInject'
 import { isFunction, NO, isObject } from '@vue/shared'
 import { warn } from './warning'
-import { createVNode, cloneVNode } from './vnode'
+import { createVNode, cloneVNode, VNode } from './vnode'
 
 export interface App<HostElement = any> {
   config: AppConfig
@@ -16,7 +16,10 @@ export interface App<HostElement = any> {
   component(name: string, component: Component): this
   directive(name: string): Directive | undefined
   directive(name: string, directive: Directive): this
-  mount(rootContainer: HostElement | string): ComponentPublicInstance
+  mount(
+    rootContainer: HostElement | string,
+    isHydrate?: boolean
+  ): ComponentPublicInstance
   unmount(rootContainer: HostElement | string): void
   provide<T>(key: InjectionKey<T> | string, value: T): this
 
@@ -87,7 +90,8 @@ export type CreateAppFunction<HostElement> = (
 ) => App<HostElement>
 
 export function createAppAPI<HostNode, HostElement>(
-  render: RootRenderFunction<HostNode, HostElement>
+  render: RootRenderFunction<HostNode, HostElement>,
+  hydrate: (vnode: VNode, container: Element) => void
 ): CreateAppFunction<HostElement> {
   return function createApp(rootComponent: Component, rootProps = null) {
     if (rootProps != null && !isObject(rootProps)) {
@@ -182,7 +186,7 @@ export function createAppAPI<HostNode, HostElement>(
         return app
       },
 
-      mount(rootContainer: HostElement): any {
+      mount(rootContainer: HostElement, isHydrate?: boolean): any {
         if (!isMounted) {
           const vnode = createVNode(rootComponent, rootProps)
           // store app context on the root VNode.
@@ -196,7 +200,11 @@ export function createAppAPI<HostNode, HostElement>(
             }
           }
 
-          render(vnode, rootContainer)
+          if (isHydrate) {
+            hydrate(vnode, rootContainer as any)
+          } else {
+            render(vnode, rootContainer)
+          }
           isMounted = true
           app._container = rootContainer
           return vnode.component!.proxy
index d075ce12d325432013bede017ba46abc782f7e2d..7fd782ffece7db410e168751a858d46e1f5a2338 100644 (file)
@@ -219,10 +219,10 @@ export interface SuspenseBoundary<
     instance: ComponentInternalInstance,
     setupRenderEffect: (
       instance: ComponentInternalInstance,
-      parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
       initialVNode: VNode<HostNode, HostElement>,
-      container: HostElement,
+      container: HostElement | null,
       anchor: HostNode | null,
+      parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
       isSVG: boolean
     ) => void
   ): void
@@ -419,11 +419,11 @@ function createSuspenseBoundary<HostNode, HostElement>(
           handleSetupResult(instance, asyncSetupResult, suspense)
           setupRenderEffect(
             instance,
-            suspense,
             vnode,
             // component may have been moved before resolve
             parentNode(instance.subTree.el)!,
             next(instance.subTree),
+            suspense,
             isSVG
           )
           updateHOCHostEl(instance, vnode.el)
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
new file mode 100644 (file)
index 0000000..3ac09b6
--- /dev/null
@@ -0,0 +1,141 @@
+import {
+  VNode,
+  normalizeVNode,
+  Text,
+  Comment,
+  Static,
+  Fragment,
+  Portal
+} from './vnode'
+import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
+import { ComponentInternalInstance } from './component'
+import { invokeDirectiveHook } from './directives'
+import { ShapeFlags } from './shapeFlags'
+import { warn } from './warning'
+import { PatchFlags, isReservedProp, isOn } from '@vue/shared'
+
+// Note: hydration is DOM-specific
+// but we have to place it in core due to tight coupling with core renderer
+// logic - splitting it out
+export function createHydrateFn(
+  mountComponent: any, // TODO
+  patchProp: any // TODO
+) {
+  function hydrate(vnode: VNode, container: Element) {
+    if (__DEV__ && !container.hasChildNodes()) {
+      warn(`Attempting to hydrate existing markup but container is empty.`)
+      return
+    }
+    hydrateNode(container.firstChild!, vnode)
+    flushPostFlushCbs()
+  }
+
+  // TODO handle mismatches
+  // TODO SVG
+  // TODO Suspense
+  function hydrateNode(
+    node: Node,
+    vnode: VNode,
+    parentComponent: ComponentInternalInstance | null = null
+  ): Node | null | undefined {
+    const { type, shapeFlag } = vnode
+    vnode.el = node
+    switch (type) {
+      case Text:
+      case Comment:
+      case Static:
+        return node.nextSibling
+      case Fragment:
+        const anchor = (vnode.anchor = hydrateChildren(
+          node.nextSibling,
+          vnode.children as VNode[],
+          parentComponent
+        )!)
+        // TODO handle potential hydration error if fragment didn't get
+        // back anchor as expected.
+        return anchor.nextSibling
+      case Portal:
+        // TODO
+        break
+      default:
+        if (shapeFlag & ShapeFlags.ELEMENT) {
+          return hydrateElement(node as Element, vnode, parentComponent)
+        } else if (shapeFlag & ShapeFlags.COMPONENT) {
+          mountComponent(vnode, null, null, parentComponent, null, false)
+          const subTree = vnode.component!.subTree
+          return (subTree.anchor || subTree.el).nextSibling
+        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
+          // TODO
+        } else if (__DEV__) {
+          warn('Invalid HostVNode type:', type, `(${typeof type})`)
+        }
+    }
+  }
+
+  function hydrateElement(
+    el: Element,
+    vnode: VNode,
+    parentComponent: ComponentInternalInstance | null
+  ) {
+    const { props, patchFlag } = vnode
+    // skip props & children if this is hoisted static nodes
+    if (patchFlag !== PatchFlags.HOISTED) {
+      // props
+      if (props !== null) {
+        if (
+          patchFlag & PatchFlags.FULL_PROPS ||
+          patchFlag & PatchFlags.HYDRATE_EVENTS
+        ) {
+          for (const key in props) {
+            if (!isReservedProp(key) && isOn(key)) {
+              patchProp(el, key, props[key], null)
+            }
+          }
+        } else if (props.onClick != null) {
+          // Fast path for click listeners (which is most often) to avoid
+          // iterating through props.
+          patchProp(el, 'onClick', props.onClick, null)
+        }
+        // vnode hooks
+        const { onVnodeBeforeMount, onVnodeMounted } = props
+        if (onVnodeBeforeMount != null) {
+          invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
+        }
+        if (onVnodeMounted != null) {
+          queuePostFlushCb(() => {
+            invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
+          })
+        }
+      }
+      // children
+      if (
+        vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
+        // skip if element has innerHTML / textContent
+        !(props !== null && (props.innerHTML || props.textContent))
+      ) {
+        hydrateChildren(
+          el.firstChild,
+          vnode.children as VNode[],
+          parentComponent
+        )
+      }
+    }
+    return el.nextSibling
+  }
+
+  function hydrateChildren(
+    node: Node | null | undefined,
+    vnodes: VNode[],
+    parentComponent: ComponentInternalInstance | null
+  ): Node | null | undefined {
+    for (let i = 0; node != null && i < vnodes.length; i++) {
+      // TODO can skip normalizeVNode in optimized mode
+      // (need hint on rendered markup?)
+      const vnode = (vnodes[i] = normalizeVNode(vnodes[i]))
+      node = hydrateNode(node, vnode, parentComponent)
+    }
+    return node
+  }
+
+  return [hydrate, hydrateNode] as const
+}
index 92919307863f36df1b82d1b4ff8279e43e2c39ff..34ef0eaadfb148d6750227d77cbac10a766702c7 100644 (file)
@@ -23,9 +23,8 @@ export {
   openBlock,
   createBlock
 } from './vnode'
-// VNode type symbols
-export { Text, Comment, Fragment, Portal } from './vnode'
 // Internal Components
+export { Fragment, Portal } from './vnode'
 export { Suspense, SuspenseProps } from './components/Suspense'
 export { KeepAlive, KeepAliveProps } from './components/KeepAlive'
 export {
index 3c0828df4a7d040f4a11119f31ba80acbdd53c10..afbb289870546ca9bc8d752f11ab8fff99e977ba 100644 (file)
@@ -30,8 +30,7 @@ import {
   isReservedProp,
   isFunction,
   PatchFlags,
-  NOOP,
-  isOn
+  NOOP
 } from '@vue/shared'
 import {
   queueJob,
@@ -54,7 +53,7 @@ import { ShapeFlags } from './shapeFlags'
 import { pushWarningContext, popWarningContext, warn } from './warning'
 import { invokeDirectiveHook } from './directives'
 import { ComponentPublicInstance } from './componentProxy'
-import { createAppAPI, CreateAppFunction } from './apiCreateApp'
+import { createAppAPI } from './apiCreateApp'
 import {
   SuspenseBoundary,
   queueEffectWithSuspense,
@@ -63,6 +62,7 @@ import {
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
 import { registerHMR, unregisterHMR } from './hmr'
+import { createHydrateFn } from './hydration'
 
 const __HMR__ = __BUNDLER__ && __DEV__
 
@@ -185,13 +185,7 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
 export function createRenderer<
   HostNode extends object = any,
   HostElement extends HostNode = any
->(
-  options: RendererOptions<HostNode, HostElement>
-): {
-  render: RootRenderFunction<HostNode, HostElement>
-  hydrate: RootRenderFunction<HostNode, HostElement>
-  createApp: CreateAppFunction<HostElement>
-} {
+>(options: RendererOptions<HostNode, HostElement>) {
   type HostVNode = VNode<HostNode, HostElement>
   type HostVNodeChildren = VNodeArrayChildren<HostNode, HostElement>
   type HostSuspenseBoundary = SuspenseBoundary<HostNode, HostElement>
@@ -984,7 +978,7 @@ export function createRenderer<
 
   function mountComponent(
     initialVNode: HostVNode,
-    container: HostElement,
+    container: HostElement | null, // only null during hydration
     anchor: HostNode | null,
     parentComponent: ComponentInternalInstance | null,
     parentSuspense: HostSuspenseBoundary | null,
@@ -1023,19 +1017,19 @@ export function createRenderer<
 
       parentSuspense.registerDep(instance, setupRenderEffect)
 
-      // give it a placeholder
+      // Give it a placeholder if this is not hydration
       const placeholder = (instance.subTree = createVNode(Comment))
-      processCommentNode(null, placeholder, container, anchor)
+      processCommentNode(null, placeholder, container!, anchor)
       initialVNode.el = placeholder.el
       return
     }
 
     setupRenderEffect(
       instance,
-      parentSuspense,
       initialVNode,
       container,
       anchor,
+      parentSuspense,
       isSVG
     )
 
@@ -1046,10 +1040,10 @@ export function createRenderer<
 
   function setupRenderEffect(
     instance: ComponentInternalInstance,
-    parentSuspense: HostSuspenseBoundary | null,
     initialVNode: HostVNode,
-    container: HostElement,
+    container: HostElement | null, // only null during hydration
     anchor: HostNode | null,
+    parentSuspense: HostSuspenseBoundary | null,
     isSVG: boolean
   ) {
     // create reactive effect for rendering
@@ -1060,8 +1054,21 @@ export function createRenderer<
         if (instance.bm !== null) {
           invokeHooks(instance.bm)
         }
-        patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
-        initialVNode.el = subTree.el
+        if (initialVNode.el) {
+          // vnode has adopted host node - perform hydration instead of mount.
+          hydrateNode(initialVNode.el as Node, subTree, instance)
+        } else {
+          patch(
+            null,
+            subTree,
+            container!, // container is only null during hydration
+            anchor,
+            instance,
+            parentSuspense,
+            isSVG
+          )
+          initialVNode.el = subTree.el
+        }
         // mounted hook
         if (instance.m !== null) {
           queuePostRenderEffect(instance.m, parentSuspense)
@@ -1816,119 +1823,12 @@ export function createRenderer<
     container._vnode = vnode
   }
 
-  function hydrate(vnode: HostVNode, container: any) {
-    hydrateNode(container.firstChild, vnode, container)
-    flushPostFlushCbs()
-  }
-
-  // TODO handle mismatches
-  function hydrateNode(
-    node: any,
-    vnode: HostVNode,
-    container: any,
-    parentComponent: ComponentInternalInstance | null = null
-  ): any {
-    const { type, shapeFlag } = vnode
-    switch (type) {
-      case Text:
-      case Comment:
-      case Static:
-        vnode.el = node
-        return node.nextSibling
-      case Fragment:
-        vnode.el = node
-        const anchor = (vnode.anchor = hydrateChildren(
-          node.nextSibling,
-          vnode.children as HostVNode[],
-          container,
-          parentComponent
-        ))
-        return anchor.nextSibling
-      case Portal:
-        // TODO
-        break
-      default:
-        if (shapeFlag & ShapeFlags.ELEMENT) {
-          return hydrateElement(node, vnode, parentComponent)
-        } else if (shapeFlag & ShapeFlags.COMPONENT) {
-          // TODO
-        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
-          // TODO
-        } else if (__DEV__) {
-          warn('Invalid HostVNode type:', type, `(${typeof type})`)
-        }
-    }
-  }
-
-  function hydrateElement(
-    el: any,
-    vnode: HostVNode,
-    parentComponent: ComponentInternalInstance | null
-  ) {
-    vnode.el = el
-    const { props, patchFlag } = vnode
-    // skip props & children if this is hoisted static nodes
-    if (patchFlag !== PatchFlags.HOISTED) {
-      // props
-      if (props !== null) {
-        if (
-          patchFlag & PatchFlags.FULL_PROPS ||
-          patchFlag & PatchFlags.HYDRATE_EVENTS
-        ) {
-          for (const key in props) {
-            if (!isReservedProp(key) && isOn(key)) {
-              hostPatchProp(el, key, props[key], null)
-            }
-          }
-        } else if (props.onClick != null) {
-          // Fast path for click listeners (which is most often) to avoid
-          // iterating through props.
-          hostPatchProp(el, 'onClick', props.onClick, null)
-        }
-        // vnode mounted hook
-        const { onVnodeMounted } = props
-        if (onVnodeMounted != null) {
-          queuePostFlushCb(() => {
-            invokeDirectiveHook(onVnodeMounted, parentComponent, vnode, null)
-          })
-        }
-      }
-      // children
-      if (
-        vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
-        // skip if element has innerHTML / textContent
-        !(props !== null && (props.innerHTML || props.textContent))
-      ) {
-        hydrateChildren(
-          el.firstChild,
-          vnode.children as HostVNode[],
-          el,
-          parentComponent
-        )
-      }
-    }
-    return el.nextSibling
-  }
-
-  function hydrateChildren(
-    node: any,
-    vnodes: HostVNode[],
-    container: any,
-    parentComponent: ComponentInternalInstance | null = null
-  ) {
-    for (let i = 0; i < vnodes.length; i++) {
-      // TODO can skip normalizeVNode in optimized mode
-      // (need hint on rendered markup?)
-      const vnode = (vnodes[i] = normalizeVNode(vnodes[i]))
-      node = hydrateNode(node, vnode, container, parentComponent)
-    }
-    return node
-  }
+  const [hydrate, hydrateNode] = createHydrateFn(mountComponent, hostPatchProp)
 
   return {
     render,
     hydrate,
-    createApp: createAppAPI(render)
+    createApp: createAppAPI<HostNode, HostElement>(render, hydrate)
   }
 }
 
index 8f4ec06f7925fd6866363eff8a00ad9aa45a3e01..102a8a7384fac28cf09921e5ee51b4b1146435b3 100644 (file)
@@ -35,7 +35,7 @@ export const createApp: CreateAppFunction<Element> = (...args) => {
   }
 
   const { mount } = app
-  app.mount = (container): any => {
+  app.mount = (container: Element | string): any => {
     if (isString(container)) {
       container = document.querySelector(container)!
       if (!container) {
@@ -53,9 +53,12 @@ export const createApp: CreateAppFunction<Element> = (...args) => {
     ) {
       component.template = container.innerHTML
     }
-    // clear content before mounting
-    container.innerHTML = ''
-    return mount(container)
+    const isHydrate = container.hasAttribute('data-server-rendered')
+    if (!isHydrate) {
+      // clear content before mounting
+      container.innerHTML = ''
+    }
+    return mount(container, isHydrate)
   }
 
   return app