wip(vapor): reuse createApp from core

This commit is contained in:
Evan You 2024-12-04 11:54:26 +08:00
parent cc2439c9e6
commit 4fe05bdd74
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
7 changed files with 158 additions and 82 deletions

View File

@ -1,7 +1,8 @@
import { import {
type Component, type Component,
type ComponentInternalInstance,
type ConcreteComponent, type ConcreteComponent,
type GenericComponent,
type GenericComponentInstance,
getComponentPublicInstance, getComponentPublicInstance,
validateComponentName, validateComponentName,
} from './component' } from './component'
@ -18,8 +19,7 @@ import { type Directive, validateDirectiveName } from './directives'
import type { ElementNamespace, RootRenderFunction } from './renderer' import type { ElementNamespace, RootRenderFunction } from './renderer'
import type { InjectionKey } from './apiInject' import type { InjectionKey } from './apiInject'
import { warn } from './warning' import { warn } from './warning'
import { type VNode, cloneVNode, createVNode } from './vnode' import type { VNode } from './vnode'
import type { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { NO, extend, isFunction, isObject } from '@vue/shared' import { NO, extend, isFunction, isObject } from '@vue/shared'
import type { Data } from '@vue/runtime-shared' import type { Data } from '@vue/runtime-shared'
@ -95,11 +95,11 @@ export interface App<HostElement = any> {
// internal, but we need to expose these for the server-renderer and devtools // internal, but we need to expose these for the server-renderer and devtools
_uid: number _uid: number
_component: ConcreteComponent _component: GenericComponent
_props: Data | null _props: Data | null
_container: HostElement | null _container: HostElement | null
_context: AppContext _context: AppContext
_instance: ComponentInternalInstance | null _instance: GenericComponentInstance | null
/** /**
* @internal custom element vnode * @internal custom element vnode
@ -257,15 +257,30 @@ export function createAppContext(): AppContext {
} }
export type CreateAppFunction<HostElement> = ( export type CreateAppFunction<HostElement> = (
rootComponent: Component, rootComponent: GenericComponent,
rootProps?: Data | null, rootProps?: Data | null,
) => App<HostElement> ) => App<HostElement>
let uid = 0 let uid = 0
export type AppMountFn<HostElement> = (
app: App,
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
) => GenericComponentInstance
export type AppUnmountFn = (app: App) => void
/**
* @internal
*/
export function createAppAPI<HostElement>( export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>, // render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction, // hydrate?: RootHydrateFunction,
mount: AppMountFn<HostElement>,
unmount: AppUnmountFn,
render?: RootRenderFunction,
): CreateAppFunction<HostElement> { ): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) { return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) { if (!isFunction(rootComponent)) {
@ -369,59 +384,32 @@ export function createAppAPI<HostElement>(
}, },
mount( mount(
rootContainer: HostElement, rootContainer: HostElement & { __vue_app__?: App },
isHydrate?: boolean, isHydrate?: boolean,
namespace?: boolean | ElementNamespace, namespace?: boolean | ElementNamespace,
): any { ): any {
if (!isMounted) { if (!isMounted) {
// #5571 // #5571
if (__DEV__ && (rootContainer as any).__vue_app__) { if (__DEV__ && rootContainer.__vue_app__) {
warn( warn(
`There is already an app instance mounted on the host container.\n` + `There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` + ` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`, ` you need to unmount the previous app by calling \`app.unmount()\` first.`,
) )
} }
const vnode = app._ceVNode || createVNode(rootComponent, rootProps) const instance = mount(app, rootContainer, isHydrate, namespace)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload
if (__DEV__) {
context.reload = () => {
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(
cloneVNode(vnode),
rootContainer,
namespace as ElementNamespace,
)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, namespace)
}
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component app._instance = instance
devtoolsInitApp(app, version) devtoolsInitApp(app, version)
} }
return getComponentPublicInstance(vnode.component!) isMounted = true
app._container = rootContainer
// for devtools and telemetry
rootContainer.__vue_app__ = app
return getComponentPublicInstance(instance)
} else if (__DEV__) { } else if (__DEV__) {
warn( warn(
`App has already been mounted.\n` + `App has already been mounted.\n` +
@ -449,7 +437,7 @@ export function createAppAPI<HostElement>(
app._instance, app._instance,
ErrorCodes.APP_UNMOUNT_CLEANUP, ErrorCodes.APP_UNMOUNT_CLEANUP,
) )
render(null, app._container) unmount(app)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null app._instance = null
devtoolsUnmountApp(app) devtoolsUnmountApp(app)
@ -485,7 +473,12 @@ export function createAppAPI<HostElement>(
}) })
if (__COMPAT__) { if (__COMPAT__) {
installAppCompatProperties(app, context, render) installAppCompatProperties(
app,
context,
// vapor doesn't have compat mode so this is always passed
render!,
)
} }
return app return app

View File

@ -367,6 +367,9 @@ export interface GenericComponentInstance {
*/ */
propsDefaults: Data | null propsDefaults: Data | null
// exposed properties via expose()
exposed: Record<string, any> | null
// lifecycle // lifecycle
isMounted: boolean isMounted: boolean
isUnmounted: boolean isUnmounted: boolean
@ -519,8 +522,7 @@ export interface ComponentInternalInstance extends GenericComponentInstance {
data: Data // options API only data: Data // options API only
emit: EmitFn emit: EmitFn
slots: InternalSlots slots: InternalSlots
// exposed properties via expose()
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null exposeProxy: Record<string, any> | null
/** /**
@ -1228,24 +1230,33 @@ export function createSetupContext(
} }
export function getComponentPublicInstance( export function getComponentPublicInstance(
instance: ComponentInternalInstance, instance: GenericComponentInstance,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null { ): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null {
if (instance.exposed) { if (instance.exposed) {
return ( if ('exposeProxy' in instance) {
instance.exposeProxy || return (
(instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), { instance.exposeProxy ||
get(target, key: string) { (instance.exposeProxy = new Proxy(
if (key in target) { proxyRefs(markRaw(instance.exposed)),
return target[key] {
} else if (key in publicPropertiesMap) { get(target, key: string) {
return publicPropertiesMap[key](instance) if (key in target) {
} return target[key]
}, } else if (key in publicPropertiesMap) {
has(target, key: string) { return publicPropertiesMap[key](
return key in target || key in publicPropertiesMap instance as ComponentInternalInstance,
}, )
})) }
) },
has(target, key: string) {
return key in target || key in publicPropertiesMap
},
},
))
)
} else {
return instance.exposed
}
} else { } else {
return instance.proxy return instance.proxy
} }

View File

@ -501,3 +501,8 @@ export {
nextUid, nextUid,
} from './component' } from './component'
export { pushWarningContext, popWarningContext } from './warning' export { pushWarningContext, popWarningContext } from './warning'
export {
createAppAPI,
type AppMountFn,
type AppUnmountFn,
} from './apiCreateApp'

View File

@ -8,6 +8,7 @@ import {
type VNodeHook, type VNodeHook,
type VNodeProps, type VNodeProps,
cloneIfMounted, cloneIfMounted,
cloneVNode,
createVNode, createVNode,
invokeVNodeHook, invokeVNodeHook,
isSameVNodeType, isSameVNodeType,
@ -57,7 +58,12 @@ import {
import { updateProps } from './componentProps' import { updateProps } from './componentProps'
import { updateSlots } from './componentSlots' import { updateSlots } from './componentSlots'
import { popWarningContext, pushWarningContext, warn } from './warning' import { popWarningContext, pushWarningContext, warn } from './warning'
import { type CreateAppFunction, createAppAPI } from './apiCreateApp' import {
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
createAppAPI,
} from './apiCreateApp'
import { setRef } from './rendererTemplateRef' import { setRef } from './rendererTemplateRef'
import { import {
type SuspenseBoundary, type SuspenseBoundary,
@ -2397,10 +2403,49 @@ function baseCreateRenderer(
) )
} }
const mountApp: AppMountFn<Element> = (
app,
container,
isHydrate,
namespace,
) => {
const vnode = app._ceVNode || createVNode(app._component, app._props)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = app._context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload
if (__DEV__) {
app._context.reload = () => {
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(cloneVNode(vnode), container, namespace as ElementNamespace)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, container as any)
} else {
render(vnode, container, namespace)
}
return vnode.component!
}
const unmountApp: AppUnmountFn = app => {
render(null, app._container)
}
return { return {
render, render,
hydrate, hydrate,
createApp: createAppAPI(render, hydrate), createApp: createAppAPI(mountApp, unmountApp, render),
} }
} }

View File

@ -1,5 +1,6 @@
import { import {
type App, type App,
type ConcreteComponent,
type CreateAppFunction, type CreateAppFunction,
type DefineComponent, type DefineComponent,
DeprecationTypes, DeprecationTypes,
@ -108,7 +109,7 @@ export const createApp = ((...args) => {
const container = normalizeContainer(containerOrSelector) const container = normalizeContainer(containerOrSelector)
if (!container) return if (!container) return
const component = app._component const component = app._component as ConcreteComponent
if (!isFunction(component) && !component.render && !component.template) { if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__ // __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template. // Reason: potential execution of JS expressions in in-DOM template.
@ -225,7 +226,10 @@ function injectCompilerOptionsCheck(app: App) {
} }
} }
function normalizeContainer( /**
* @internal
*/
export function normalizeContainer(
container: Element | ShadowRoot | string, container: Element | ShadowRoot | string,
): Element | ShadowRoot | null { ): Element | ShadowRoot | null {
if (isString(container)) { if (isString(container)) {

View File

@ -1,18 +1,36 @@
import { normalizeContainer } from '../apiRender' import { normalizeContainer } from '../apiRender'
import { insert } from '../dom/element' import { insert } from '../dom/element'
import { type VaporComponent, createComponent } from './component' import { type VaporComponent, createComponent } from './component'
import {
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
createAppAPI,
} from '@vue/runtime-core'
let _createApp: CreateAppFunction<ParentNode>
const mountApp: AppMountFn<ParentNode> = (app, container) => {
// clear content before mounting
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
container.textContent = ''
}
const instance = createComponent(app._component)
insert(instance.block, container)
return instance
}
const unmountApp: AppUnmountFn = app => {
// TODO
}
export function createVaporApp(comp: VaporComponent): any { export function createVaporApp(comp: VaporComponent): any {
return { if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp)
mount(container: string | ParentNode) { const app = _createApp(comp)
container = normalizeContainer(container) const mount = app.mount
// clear content before mounting app.mount = (container, ...args: any[]) => {
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) { container = normalizeContainer(container) // TODO reuse from runtime-dom
container.textContent = '' return mount(container, ...args)
}
const instance = createComponent(comp)
insert(instance.block, container)
return instance
},
} }
return app
} }

View File

@ -143,7 +143,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
rawProps: RawProps | undefined rawProps: RawProps | undefined
props: Record<string, any> props: Record<string, any>
attrs: Record<string, any> attrs: Record<string, any>
exposed?: Record<string, any> exposed: Record<string, any> | null
emitted: Record<string, boolean> | null emitted: Record<string, boolean> | null
propsDefaults: Record<string, any> | null propsDefaults: Record<string, any> | null
@ -178,7 +178,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.rawProps = rawProps this.rawProps = rawProps
this.provides = this.refs = EMPTY_OBJ this.provides = this.refs = EMPTY_OBJ
this.emitted = this.ec = null this.emitted = this.ec = this.exposed = null
this.isMounted = this.isUnmounted = this.isDeactivated = false this.isMounted = this.isUnmounted = this.isDeactivated = false
// init props // init props