refactor(ssr): make hydration logic tree-shakeable

This commit is contained in:
Evan You 2020-02-14 01:30:08 -05:00
parent 112d8f7d86
commit 167f8241bd
5 changed files with 121 additions and 41 deletions

View File

@ -91,7 +91,7 @@ export type CreateAppFunction<HostElement> = (
export function createAppAPI<HostNode, HostElement>( export function createAppAPI<HostNode, HostElement>(
render: RootRenderFunction<HostNode, HostElement>, render: RootRenderFunction<HostNode, HostElement>,
hydrate: (vnode: VNode, container: Element) => void hydrate?: (vnode: VNode, container: Element) => void
): CreateAppFunction<HostElement> { ): CreateAppFunction<HostElement> {
return function createApp(rootComponent: Component, rootProps = null) { return function createApp(rootComponent: Component, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) { if (rootProps != null && !isObject(rootProps)) {
@ -200,7 +200,7 @@ export function createAppAPI<HostNode, HostElement>(
} }
} }
if (isHydrate) { if (isHydrate && hydrate) {
hydrate(vnode, rootContainer as any) hydrate(vnode, rootContainer as any)
} else { } else {
render(vnode, rootContainer) render(vnode, rootContainer)

View File

@ -15,9 +15,11 @@ import { warn } from './warning'
import { PatchFlags, isReservedProp, isOn } from '@vue/shared' import { PatchFlags, isReservedProp, isOn } from '@vue/shared'
// Note: hydration is DOM-specific // Note: hydration is DOM-specific
// but we have to place it in core due to tight coupling with core - splitting // But we have to place it in core due to tight coupling with core - splitting
// it out creates a ton of unnecessary complexity. // it out creates a ton of unnecessary complexity.
export function createHydrateFn( // Hydration also depends on some renderer internal logic which needs to be
// passed in via arguments.
export function createHydrationFunctions(
mountComponent: any, // TODO mountComponent: any, // TODO
patchProp: any // TODO patchProp: any // TODO
) { ) {

View File

@ -65,7 +65,7 @@ export { useCSSModule } from './helpers/useCssModule'
// Internal API ---------------------------------------------------------------- // Internal API ----------------------------------------------------------------
// For custom renderers // For custom renderers
export { createRenderer } from './renderer' export { createRenderer, createHydrationRenderer } from './renderer'
export { warn } from './warning' export { warn } from './warning'
export { export {
handleError, handleError,

View File

@ -62,7 +62,7 @@ import {
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
import { registerHMR, unregisterHMR } from './hmr' import { registerHMR, unregisterHMR } from './hmr'
import { createHydrateFn } from './hydration' import { createHydrationFunctions } from './hydration'
const __HMR__ = __BUNDLER__ && __DEV__ const __HMR__ = __BUNDLER__ && __DEV__
@ -186,6 +186,32 @@ export function createRenderer<
HostNode extends object = any, HostNode extends object = any,
HostElement extends HostNode = any HostElement extends HostNode = any
>(options: RendererOptions<HostNode, HostElement>) { >(options: RendererOptions<HostNode, HostElement>) {
const res = baseCreateRenderer(options)
return res as typeof res & {
hydrate: undefined
}
}
// Separate API for creating hydration-enabled renderer.
// Hydration logic is only used when calling this function, making it
// tree-shakable.
export function createHydrationRenderer<
HostNode extends object = any,
HostElement extends HostNode = any
>(options: RendererOptions<HostNode, HostElement>) {
const res = baseCreateRenderer(options, createHydrationFunctions)
return res as typeof res & {
hydrate: ReturnType<typeof createHydrationFunctions>[0]
}
}
function baseCreateRenderer<
HostNode extends object = any,
HostElement extends HostNode = any
>(
options: RendererOptions<HostNode, HostElement>,
createHydrationFns?: typeof createHydrationFunctions
) {
type HostVNode = VNode<HostNode, HostElement> type HostVNode = VNode<HostNode, HostElement>
type HostVNodeChildren = VNodeArrayChildren<HostNode, HostElement> type HostVNodeChildren = VNodeArrayChildren<HostNode, HostElement>
type HostSuspenseBoundary = SuspenseBoundary<HostNode, HostElement> type HostSuspenseBoundary = SuspenseBoundary<HostNode, HostElement>
@ -215,6 +241,12 @@ export function createRenderer<
options options
} }
let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
let hydrateNode: ReturnType<typeof createHydrationFunctions>[1] | undefined
if (createHydrationFns) {
;[hydrate, hydrateNode] = createHydrationFns(mountComponent, hostPatchProp)
}
function patch( function patch(
n1: HostVNode | null, // null means this is a mount n1: HostVNode | null, // null means this is a mount
n2: HostVNode, n2: HostVNode,
@ -1054,7 +1086,7 @@ export function createRenderer<
if (instance.bm !== null) { if (instance.bm !== null) {
invokeHooks(instance.bm) invokeHooks(instance.bm)
} }
if (initialVNode.el) { if (initialVNode.el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount. // vnode has adopted host node - perform hydration instead of mount.
hydrateNode(initialVNode.el as Node, subTree, instance) hydrateNode(initialVNode.el as Node, subTree, instance)
} else { } else {
@ -1823,8 +1855,6 @@ export function createRenderer<
container._vnode = vnode container._vnode = vnode
} }
const [hydrate, hydrateNode] = createHydrateFn(mountComponent, hostPatchProp)
return { return {
render, render,
hydrate, hydrate,

View File

@ -1,49 +1,62 @@
import { import {
createRenderer, createRenderer,
createHydrationRenderer,
warn, warn,
RootRenderFunction, RootRenderFunction,
CreateAppFunction CreateAppFunction,
VNode,
App
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { nodeOps } from './nodeOps' import { nodeOps } from './nodeOps'
import { patchProp } from './patchProp' import { patchProp } from './patchProp'
// Importing from the compiler, will be tree-shaken in prod // Importing from the compiler, will be tree-shaken in prod
import { isFunction, isString, isHTMLTag, isSVGTag } from '@vue/shared' import { isFunction, isString, isHTMLTag, isSVGTag } from '@vue/shared'
const { const rendererOptions = {
render: baseRender,
hydrate: baseHydrate,
createApp: baseCreateApp
} = createRenderer({
patchProp, patchProp,
...nodeOps ...nodeOps
}) }
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer:
| ReturnType<typeof createRenderer>
| ReturnType<typeof createHydrationRenderer>
let enabledHydration = false
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
function ensureHydrationRenderer() {
renderer = enabledHydration
? renderer
: createHydrationRenderer(rendererOptions)
enabledHydration = true
return renderer as ReturnType<typeof createHydrationRenderer>
}
// use explicit type casts here to avoid import() calls in rolled-up d.ts // use explicit type casts here to avoid import() calls in rolled-up d.ts
export const render = baseRender as RootRenderFunction<Node, Element> export const render = ((...args) => {
export const hydrate = baseHydrate as RootRenderFunction<Node, Element> ensureRenderer().render(...args)
}) as RootRenderFunction<Node, Element>
export const createApp: CreateAppFunction<Element> = (...args) => { export const hydrate = ((...args) => {
const app = baseCreateApp(...args) ensureHydrationRenderer().hydrate(...args)
}) as (vnode: VNode, container: Element) => void
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
if (__DEV__) { if (__DEV__) {
// Inject `isNativeTag` injectNativeTagCheck(app)
// this is used for component name validation (dev only)
Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
writable: false
})
} }
const { mount } = app const { mount } = app
app.mount = (container: Element | string): any => { app.mount = (containerOrSelector: Element | string): any => {
if (isString(container)) { const container = normalizeContainer(containerOrSelector)
container = document.querySelector(container)! if (!container) return
if (!container) {
__DEV__ &&
warn(`Failed to mount app: mount target selector returned null.`)
return
}
}
const component = app._component const component = app._component
if ( if (
__RUNTIME_COMPILE__ && __RUNTIME_COMPILE__ &&
@ -53,15 +66,50 @@ export const createApp: CreateAppFunction<Element> = (...args) => {
) { ) {
component.template = container.innerHTML component.template = container.innerHTML
} }
const isHydrate = container.hasAttribute('data-server-rendered') // clear content before mounting
if (!isHydrate) { container.innerHTML = ''
// clear content before mounting return mount(container)
container.innerHTML = ''
}
return mount(container, isHydrate)
} }
return app return app
}) as CreateAppFunction<Element>
export const createSSRApp = ((...args) => {
const app = ensureHydrationRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
}
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (container) {
return mount(container, true)
}
}
return app
}) as CreateAppFunction<Element>
function injectNativeTagCheck(app: App) {
// Inject `isNativeTag`
// this is used for component name validation (dev only)
Object.defineProperty(app.config, 'isNativeTag', {
value: (tag: string) => isHTMLTag(tag) || isSVGTag(tag),
writable: false
})
}
function normalizeContainer(container: Element | string): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
if (__DEV__ && !res) {
warn(`Failed to mount app: mount target selector returned null.`)
}
return res
}
return container
} }
// DOM-only runtime directive helpers // DOM-only runtime directive helpers