apiCreateApp.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import {
  2. type Component,
  3. type ComponentInternalInstance,
  4. type ConcreteComponent,
  5. type Data,
  6. getComponentPublicInstance,
  7. validateComponentName,
  8. } from './component'
  9. import type {
  10. ComponentOptions,
  11. MergedComponentOptions,
  12. RuntimeCompilerOptions,
  13. } from './componentOptions'
  14. import type {
  15. ComponentCustomProperties,
  16. ComponentPublicInstance,
  17. } from './componentPublicInstance'
  18. import { type Directive, validateDirectiveName } from './directives'
  19. import type { ElementNamespace, RootRenderFunction } from './renderer'
  20. import type { InjectionKey } from './apiInject'
  21. import { warn } from './warning'
  22. import { type VNode, cloneVNode, createVNode } from './vnode'
  23. import type { RootHydrateFunction } from './hydration'
  24. import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
  25. import { NO, extend, isFunction, isObject } from '@vue/shared'
  26. import { version } from '.'
  27. import { installAppCompatProperties } from './compat/global'
  28. import type { NormalizedPropsOptions } from './componentProps'
  29. import type { ObjectEmitsOptions } from './componentEmits'
  30. import type { DefineComponent } from './apiDefineComponent'
  31. export interface App<HostElement = any> {
  32. version: string
  33. config: AppConfig
  34. use<Options extends unknown[]>(
  35. plugin: Plugin<Options>,
  36. ...options: Options
  37. ): this
  38. use<Options>(plugin: Plugin<Options>, options: Options): this
  39. mixin(mixin: ComponentOptions): this
  40. component(name: string): Component | undefined
  41. component(name: string, component: Component | DefineComponent): this
  42. directive<T = any, V = any>(name: string): Directive<T, V> | undefined
  43. directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
  44. mount(
  45. rootContainer: HostElement | string,
  46. isHydrate?: boolean,
  47. namespace?: boolean | ElementNamespace,
  48. ): ComponentPublicInstance
  49. unmount(): void
  50. provide<T, K = InjectionKey<T> | string | number>(
  51. key: K,
  52. value: K extends InjectionKey<infer V> ? V : T,
  53. ): this
  54. /**
  55. * Runs a function with the app as active instance. This allows using of `inject()` within the function to get access
  56. * to variables provided via `app.provide()`.
  57. *
  58. * @param fn - function to run with the app as active instance
  59. */
  60. runWithContext<T>(fn: () => T): T
  61. // internal, but we need to expose these for the server-renderer and devtools
  62. _uid: number
  63. _component: ConcreteComponent
  64. _props: Data | null
  65. _container: HostElement | null
  66. _context: AppContext
  67. _instance: ComponentInternalInstance | null
  68. /**
  69. * v2 compat only
  70. */
  71. filter?(name: string): Function | undefined
  72. filter?(name: string, filter: Function): this
  73. /**
  74. * @internal v3 compat only
  75. */
  76. _createRoot?(options: ComponentOptions): ComponentPublicInstance
  77. }
  78. export type OptionMergeFunction = (to: unknown, from: unknown) => any
  79. export interface AppConfig {
  80. // @private
  81. readonly isNativeTag: (tag: string) => boolean
  82. performance: boolean
  83. optionMergeStrategies: Record<string, OptionMergeFunction>
  84. globalProperties: ComponentCustomProperties & Record<string, any>
  85. errorHandler?: (
  86. err: unknown,
  87. instance: ComponentPublicInstance | null,
  88. info: string,
  89. ) => void
  90. warnHandler?: (
  91. msg: string,
  92. instance: ComponentPublicInstance | null,
  93. trace: string,
  94. ) => void
  95. /**
  96. * Options to pass to `@vue/compiler-dom`.
  97. * Only supported in runtime compiler build.
  98. */
  99. compilerOptions: RuntimeCompilerOptions
  100. /**
  101. * @deprecated use config.compilerOptions.isCustomElement
  102. */
  103. isCustomElement?: (tag: string) => boolean
  104. /**
  105. * TODO document for 3.5
  106. * Enable warnings for computed getters that recursively trigger itself.
  107. */
  108. warnRecursiveComputed?: boolean
  109. }
  110. export interface AppContext {
  111. app: App // for devtools
  112. config: AppConfig
  113. mixins: ComponentOptions[]
  114. components: Record<string, Component>
  115. directives: Record<string, Directive>
  116. provides: Record<string | symbol, any>
  117. /**
  118. * Cache for merged/normalized component options
  119. * Each app instance has its own cache because app-level global mixins and
  120. * optionMergeStrategies can affect merge behavior.
  121. * @internal
  122. */
  123. optionsCache: WeakMap<ComponentOptions, MergedComponentOptions>
  124. /**
  125. * Cache for normalized props options
  126. * @internal
  127. */
  128. propsCache: WeakMap<ConcreteComponent, NormalizedPropsOptions>
  129. /**
  130. * Cache for normalized emits options
  131. * @internal
  132. */
  133. emitsCache: WeakMap<ConcreteComponent, ObjectEmitsOptions | null>
  134. /**
  135. * HMR only
  136. * @internal
  137. */
  138. reload?: () => void
  139. /**
  140. * v2 compat only
  141. * @internal
  142. */
  143. filters?: Record<string, Function>
  144. }
  145. type PluginInstallFunction<Options = any[]> = Options extends unknown[]
  146. ? (app: App, ...options: Options) => any
  147. : (app: App, options: Options) => any
  148. export type ObjectPlugin<Options = any[]> = {
  149. install: PluginInstallFunction<Options>
  150. }
  151. export type FunctionPlugin<Options = any[]> = PluginInstallFunction<Options> &
  152. Partial<ObjectPlugin<Options>>
  153. export type Plugin<Options = any[]> =
  154. | FunctionPlugin<Options>
  155. | ObjectPlugin<Options>
  156. export function createAppContext(): AppContext {
  157. return {
  158. app: null as any,
  159. config: {
  160. isNativeTag: NO,
  161. performance: false,
  162. globalProperties: {},
  163. optionMergeStrategies: {},
  164. errorHandler: undefined,
  165. warnHandler: undefined,
  166. compilerOptions: {},
  167. },
  168. mixins: [],
  169. components: {},
  170. directives: {},
  171. provides: Object.create(null),
  172. optionsCache: new WeakMap(),
  173. propsCache: new WeakMap(),
  174. emitsCache: new WeakMap(),
  175. }
  176. }
  177. export type CreateAppFunction<HostElement> = (
  178. rootComponent: Component,
  179. rootProps?: Data | null,
  180. ) => App<HostElement>
  181. let uid = 0
  182. export function createAppAPI<HostElement>(
  183. render: RootRenderFunction<HostElement>,
  184. hydrate?: RootHydrateFunction,
  185. ): CreateAppFunction<HostElement> {
  186. return function createApp(rootComponent, rootProps = null) {
  187. if (!isFunction(rootComponent)) {
  188. rootComponent = extend({}, rootComponent)
  189. }
  190. if (rootProps != null && !isObject(rootProps)) {
  191. __DEV__ && warn(`root props passed to app.mount() must be an object.`)
  192. rootProps = null
  193. }
  194. const context = createAppContext()
  195. const installedPlugins = new WeakSet()
  196. let isMounted = false
  197. const app: App = (context.app = {
  198. _uid: uid++,
  199. _component: rootComponent as ConcreteComponent,
  200. _props: rootProps,
  201. _container: null,
  202. _context: context,
  203. _instance: null,
  204. version,
  205. get config() {
  206. return context.config
  207. },
  208. set config(v) {
  209. if (__DEV__) {
  210. warn(
  211. `app.config cannot be replaced. Modify individual options instead.`,
  212. )
  213. }
  214. },
  215. use(plugin: Plugin, ...options: any[]) {
  216. if (installedPlugins.has(plugin)) {
  217. __DEV__ && warn(`Plugin has already been applied to target app.`)
  218. } else if (plugin && isFunction(plugin.install)) {
  219. installedPlugins.add(plugin)
  220. plugin.install(app, ...options)
  221. } else if (isFunction(plugin)) {
  222. installedPlugins.add(plugin)
  223. plugin(app, ...options)
  224. } else if (__DEV__) {
  225. warn(
  226. `A plugin must either be a function or an object with an "install" ` +
  227. `function.`,
  228. )
  229. }
  230. return app
  231. },
  232. mixin(mixin: ComponentOptions) {
  233. if (__FEATURE_OPTIONS_API__) {
  234. if (!context.mixins.includes(mixin)) {
  235. context.mixins.push(mixin)
  236. } else if (__DEV__) {
  237. warn(
  238. 'Mixin has already been applied to target app' +
  239. (mixin.name ? `: ${mixin.name}` : ''),
  240. )
  241. }
  242. } else if (__DEV__) {
  243. warn('Mixins are only available in builds supporting Options API')
  244. }
  245. return app
  246. },
  247. component(name: string, component?: Component): any {
  248. if (__DEV__) {
  249. validateComponentName(name, context.config)
  250. }
  251. if (!component) {
  252. return context.components[name]
  253. }
  254. if (__DEV__ && context.components[name]) {
  255. warn(`Component "${name}" has already been registered in target app.`)
  256. }
  257. context.components[name] = component
  258. return app
  259. },
  260. directive(name: string, directive?: Directive) {
  261. if (__DEV__) {
  262. validateDirectiveName(name)
  263. }
  264. if (!directive) {
  265. return context.directives[name] as any
  266. }
  267. if (__DEV__ && context.directives[name]) {
  268. warn(`Directive "${name}" has already been registered in target app.`)
  269. }
  270. context.directives[name] = directive
  271. return app
  272. },
  273. mount(
  274. rootContainer: HostElement,
  275. isHydrate?: boolean,
  276. namespace?: boolean | ElementNamespace,
  277. ): any {
  278. if (!isMounted) {
  279. // #5571
  280. if (__DEV__ && (rootContainer as any).__vue_app__) {
  281. warn(
  282. `There is already an app instance mounted on the host container.\n` +
  283. ` If you want to mount another app on the same host container,` +
  284. ` you need to unmount the previous app by calling \`app.unmount()\` first.`,
  285. )
  286. }
  287. const vnode = createVNode(rootComponent, rootProps)
  288. // store app context on the root VNode.
  289. // this will be set on the root instance on initial mount.
  290. vnode.appContext = context
  291. if (namespace === true) {
  292. namespace = 'svg'
  293. } else if (namespace === false) {
  294. namespace = undefined
  295. }
  296. // HMR root reload
  297. if (__DEV__) {
  298. context.reload = () => {
  299. // casting to ElementNamespace because TS doesn't guarantee type narrowing
  300. // over function boundaries
  301. render(
  302. cloneVNode(vnode),
  303. rootContainer,
  304. namespace as ElementNamespace,
  305. )
  306. }
  307. }
  308. if (isHydrate && hydrate) {
  309. hydrate(vnode as VNode<Node, Element>, rootContainer as any)
  310. } else {
  311. render(vnode, rootContainer, namespace)
  312. }
  313. isMounted = true
  314. app._container = rootContainer
  315. // for devtools and telemetry
  316. ;(rootContainer as any).__vue_app__ = app
  317. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  318. app._instance = vnode.component
  319. devtoolsInitApp(app, version)
  320. }
  321. return getComponentPublicInstance(vnode.component!)
  322. } else if (__DEV__) {
  323. warn(
  324. `App has already been mounted.\n` +
  325. `If you want to remount the same app, move your app creation logic ` +
  326. `into a factory function and create fresh app instances for each ` +
  327. `mount - e.g. \`const createMyApp = () => createApp(App)\``,
  328. )
  329. }
  330. },
  331. unmount() {
  332. if (isMounted) {
  333. render(null, app._container)
  334. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  335. app._instance = null
  336. devtoolsUnmountApp(app)
  337. }
  338. delete app._container.__vue_app__
  339. } else if (__DEV__) {
  340. warn(`Cannot unmount an app that is not mounted.`)
  341. }
  342. },
  343. provide(key, value) {
  344. if (__DEV__ && (key as string | symbol) in context.provides) {
  345. warn(
  346. `App already provides property with key "${String(key)}". ` +
  347. `It will be overwritten with the new value.`,
  348. )
  349. }
  350. context.provides[key as string | symbol] = value
  351. return app
  352. },
  353. runWithContext(fn) {
  354. const lastApp = currentApp
  355. currentApp = app
  356. try {
  357. return fn()
  358. } finally {
  359. currentApp = lastApp
  360. }
  361. },
  362. })
  363. if (__COMPAT__) {
  364. installAppCompatProperties(app, context, render)
  365. }
  366. return app
  367. }
  368. }
  369. /**
  370. * @internal Used to identify the current app when using `inject()` within
  371. * `app.runWithContext()`.
  372. */
  373. export let currentApp: App<unknown> | null = null