From: Evan You Date: Fri, 14 Feb 2020 06:30:08 +0000 (-0500) Subject: refactor(ssr): make hydration logic tree-shakeable X-Git-Tag: v3.0.0-alpha.5~36 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=167f8241bdcd33d84494b37042dc91ab416e0a50;p=thirdparty%2Fvuejs%2Fcore.git refactor(ssr): make hydration logic tree-shakeable --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index e8888487f7..ed5ce0950c 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -91,7 +91,7 @@ export type CreateAppFunction = ( export function createAppAPI( render: RootRenderFunction, - hydrate: (vnode: VNode, container: Element) => void + hydrate?: (vnode: VNode, container: Element) => void ): CreateAppFunction { return function createApp(rootComponent: Component, rootProps = null) { if (rootProps != null && !isObject(rootProps)) { @@ -200,7 +200,7 @@ export function createAppAPI( } } - if (isHydrate) { + if (isHydrate && hydrate) { hydrate(vnode, rootContainer as any) } else { render(vnode, rootContainer) diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index ee6a810cae..3318001f19 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -15,9 +15,11 @@ 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 - splitting +// But we have to place it in core due to tight coupling with core - splitting // 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 patchProp: any // TODO ) { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 6a74f56939..09a3ac2db0 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -65,7 +65,7 @@ export { useCSSModule } from './helpers/useCssModule' // Internal API ---------------------------------------------------------------- // For custom renderers -export { createRenderer } from './renderer' +export { createRenderer, createHydrationRenderer } from './renderer' export { warn } from './warning' export { handleError, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index afbb289870..ab8a29d455 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -62,7 +62,7 @@ import { import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { KeepAliveSink, isKeepAlive } from './components/KeepAlive' import { registerHMR, unregisterHMR } from './hmr' -import { createHydrateFn } from './hydration' +import { createHydrationFunctions } from './hydration' const __HMR__ = __BUNDLER__ && __DEV__ @@ -186,6 +186,32 @@ export function createRenderer< HostNode extends object = any, HostElement extends HostNode = any >(options: RendererOptions) { + 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) { + const res = baseCreateRenderer(options, createHydrationFunctions) + return res as typeof res & { + hydrate: ReturnType[0] + } +} + +function baseCreateRenderer< + HostNode extends object = any, + HostElement extends HostNode = any +>( + options: RendererOptions, + createHydrationFns?: typeof createHydrationFunctions +) { type HostVNode = VNode type HostVNodeChildren = VNodeArrayChildren type HostSuspenseBoundary = SuspenseBoundary @@ -215,6 +241,12 @@ export function createRenderer< options } + let hydrate: ReturnType[0] | undefined + let hydrateNode: ReturnType[1] | undefined + if (createHydrationFns) { + ;[hydrate, hydrateNode] = createHydrationFns(mountComponent, hostPatchProp) + } + function patch( n1: HostVNode | null, // null means this is a mount n2: HostVNode, @@ -1054,7 +1086,7 @@ export function createRenderer< if (instance.bm !== null) { invokeHooks(instance.bm) } - if (initialVNode.el) { + if (initialVNode.el && hydrateNode) { // vnode has adopted host node - perform hydration instead of mount. hydrateNode(initialVNode.el as Node, subTree, instance) } else { @@ -1823,8 +1855,6 @@ export function createRenderer< container._vnode = vnode } - const [hydrate, hydrateNode] = createHydrateFn(mountComponent, hostPatchProp) - return { render, hydrate, diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 102a8a7384..cda9c50231 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -1,49 +1,62 @@ import { createRenderer, + createHydrationRenderer, warn, RootRenderFunction, - CreateAppFunction + CreateAppFunction, + VNode, + App } from '@vue/runtime-core' import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' // Importing from the compiler, will be tree-shaken in prod import { isFunction, isString, isHTMLTag, isSVGTag } from '@vue/shared' -const { - render: baseRender, - hydrate: baseHydrate, - createApp: baseCreateApp -} = createRenderer({ +const rendererOptions = { patchProp, ...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 + | ReturnType + +let enabledHydration = false + +function ensureRenderer() { + return renderer || (renderer = createRenderer(rendererOptions)) +} + +function ensureHydrationRenderer() { + renderer = enabledHydration + ? renderer + : createHydrationRenderer(rendererOptions) + enabledHydration = true + return renderer as ReturnType +} // use explicit type casts here to avoid import() calls in rolled-up d.ts -export const render = baseRender as RootRenderFunction -export const hydrate = baseHydrate as RootRenderFunction +export const render = ((...args) => { + ensureRenderer().render(...args) +}) as RootRenderFunction + +export const hydrate = ((...args) => { + ensureHydrationRenderer().hydrate(...args) +}) as (vnode: VNode, container: Element) => void -export const createApp: CreateAppFunction = (...args) => { - const app = baseCreateApp(...args) +export const createApp = ((...args) => { + const app = ensureRenderer().createApp(...args) if (__DEV__) { - // 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 - }) + injectNativeTagCheck(app) } const { mount } = app - app.mount = (container: Element | string): any => { - if (isString(container)) { - container = document.querySelector(container)! - if (!container) { - __DEV__ && - warn(`Failed to mount app: mount target selector returned null.`) - return - } - } + app.mount = (containerOrSelector: Element | string): any => { + const container = normalizeContainer(containerOrSelector) + if (!container) return const component = app._component if ( __RUNTIME_COMPILE__ && @@ -53,15 +66,50 @@ export const createApp: CreateAppFunction = (...args) => { ) { component.template = container.innerHTML } - const isHydrate = container.hasAttribute('data-server-rendered') - if (!isHydrate) { - // clear content before mounting - container.innerHTML = '' + // clear content before mounting + container.innerHTML = '' + return mount(container) + } + + return app +}) as CreateAppFunction + +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 mount(container, isHydrate) } return app +}) as CreateAppFunction + +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