refactor(ssr): make hydration logic tree-shakeable
This commit is contained in:
parent
112d8f7d86
commit
167f8241bd
@ -91,7 +91,7 @@ export type CreateAppFunction<HostElement> = (
|
||||
|
||||
export function createAppAPI<HostNode, HostElement>(
|
||||
render: RootRenderFunction<HostNode, HostElement>,
|
||||
hydrate: (vnode: VNode, container: Element) => void
|
||||
hydrate?: (vnode: VNode, container: Element) => void
|
||||
): CreateAppFunction<HostElement> {
|
||||
return function createApp(rootComponent: Component, rootProps = null) {
|
||||
if (rootProps != null && !isObject(rootProps)) {
|
||||
@ -200,7 +200,7 @@ export function createAppAPI<HostNode, HostElement>(
|
||||
}
|
||||
}
|
||||
|
||||
if (isHydrate) {
|
||||
if (isHydrate && hydrate) {
|
||||
hydrate(vnode, rootContainer as any)
|
||||
} else {
|
||||
render(vnode, rootContainer)
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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,
|
||||
|
@ -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<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 HostVNodeChildren = VNodeArrayChildren<HostNode, HostElement>
|
||||
type HostSuspenseBoundary = SuspenseBoundary<HostNode, HostElement>
|
||||
@ -215,6 +241,12 @@ export function createRenderer<
|
||||
options
|
||||
}
|
||||
|
||||
let hydrate: ReturnType<typeof createHydrationFunctions>[0] | undefined
|
||||
let hydrateNode: ReturnType<typeof createHydrationFunctions>[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,
|
||||
|
@ -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<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
|
||||
export const render = baseRender as RootRenderFunction<Node, Element>
|
||||
export const hydrate = baseHydrate as RootRenderFunction<Node, Element>
|
||||
export const render = ((...args) => {
|
||||
ensureRenderer().render(...args)
|
||||
}) as RootRenderFunction<Node, Element>
|
||||
|
||||
export const createApp: CreateAppFunction<Element> = (...args) => {
|
||||
const app = baseCreateApp(...args)
|
||||
export const hydrate = ((...args) => {
|
||||
ensureHydrationRenderer().hydrate(...args)
|
||||
}) as (vnode: VNode, container: Element) => void
|
||||
|
||||
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<Element> = (...args) => {
|
||||
) {
|
||||
component.template = container.innerHTML
|
||||
}
|
||||
const isHydrate = container.hasAttribute('data-server-rendered')
|
||||
if (!isHydrate) {
|
||||
// clear content before mounting
|
||||
container.innerHTML = ''
|
||||
}
|
||||
return mount(container, isHydrate)
|
||||
// clear content before mounting
|
||||
container.innerHTML = ''
|
||||
return mount(container)
|
||||
}
|
||||
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user