vue3-yuanma/packages/runtime-core/src/apiCreateApp.ts

259 lines
7.2 KiB
TypeScript

import {
Component,
Data,
validateComponentName,
PublicAPIComponent
} from './component'
import { ComponentOptions } from './componentOptions'
import { ComponentPublicInstance } from './componentProxy'
import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer'
import { InjectionKey } from './apiInject'
import { isFunction, NO, isObject } from '@vue/shared'
import { warn } from './warning'
import { createVNode, cloneVNode, VNode } from './vnode'
import { RootHydrateFunction } from './hydration'
export interface App<HostElement = any> {
config: AppConfig
use(plugin: Plugin, ...options: any[]): this
mixin(mixin: ComponentOptions): this
component(name: string): PublicAPIComponent | undefined
component(name: string, component: PublicAPIComponent): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
mount(
rootContainer: HostElement | string,
isHydrate?: boolean
): ComponentPublicInstance
unmount(rootContainer: HostElement | string): void
provide<T>(key: InjectionKey<T> | string, value: T): this
// internal. We need to expose these for the server-renderer
_component: Component
_props: Data | null
_container: HostElement | null
_context: AppContext
}
export type OptionMergeFunction = (
to: unknown,
from: unknown,
instance: any,
key: string
) => any
export interface AppConfig {
// @private
readonly isNativeTag?: (tag: string) => boolean
devtools: boolean
performance: boolean
optionMergeStrategies: Record<string, OptionMergeFunction>
globalProperties: Record<string, any>
isCustomElement: (tag: string) => boolean
errorHandler?: (
err: unknown,
instance: ComponentPublicInstance | null,
info: string
) => void
warnHandler?: (
msg: string,
instance: ComponentPublicInstance | null,
trace: string
) => void
}
export interface AppContext {
config: AppConfig
mixins: ComponentOptions[]
components: Record<string, PublicAPIComponent>
directives: Record<string, Directive>
provides: Record<string | symbol, any>
reload?: () => void // HMR only
}
type PluginInstallFunction = (app: App, ...options: any[]) => any
export type Plugin =
| PluginInstallFunction & { install?: PluginInstallFunction }
| {
install: PluginInstallFunction
}
export function createAppContext(): AppContext {
return {
config: {
isNativeTag: NO,
devtools: true,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
}
}
export type CreateAppFunction<HostElement> = (
rootComponent: PublicAPIComponent,
rootProps?: Data | null
) => App<HostElement>
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = {
_component: rootComponent as Component,
_props: rootProps,
_container: null,
_context: context,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
} else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : '')
)
}
} else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')
}
return app
},
component(name: string, component?: PublicAPIComponent): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
},
directive(name: string, directive?: Directive) {
if (__DEV__) {
validateDirectiveName(name)
}
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(rootComponent as Component, rootProps)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
return vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted. Create a new app instance instead.`
)
}
},
unmount() {
if (isMounted) {
render(null, app._container)
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
provide(key, value) {
if (__DEV__ && key in context.provides) {
warn(
`App already provides property with key "${key}". ` +
`It will be overwritten with the new value.`
)
}
// TypeScript doesn't allow symbols as index type
// https://github.com/Microsoft/TypeScript/issues/24587
context.provides[key as string] = value
return app
}
}
return app
}
}