apiCreateApp.ts 13 KB

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