apiCreateApp.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import {
  2. ConcreteComponent,
  3. Data,
  4. validateComponentName,
  5. Component
  6. } from './component'
  7. import { ComponentOptions } from './componentOptions'
  8. import { ComponentPublicInstance } from './componentPublicInstance'
  9. import { Directive, validateDirectiveName } from './directives'
  10. import { RootRenderFunction } from './renderer'
  11. import { InjectionKey } from './apiInject'
  12. import { isFunction, NO, isObject } from '@vue/shared'
  13. import { warn } from './warning'
  14. import { createVNode, cloneVNode, VNode } from './vnode'
  15. import { RootHydrateFunction } from './hydration'
  16. import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
  17. import { version } from '.'
  18. export interface App<HostElement = any> {
  19. version: string
  20. config: AppConfig
  21. use(plugin: Plugin, ...options: any[]): this
  22. mixin(mixin: ComponentOptions): this
  23. component(name: string): Component | undefined
  24. component(name: string, component: Component): this
  25. directive(name: string): Directive | undefined
  26. directive(name: string, directive: Directive): this
  27. mount(
  28. rootContainer: HostElement | string,
  29. isHydrate?: boolean
  30. ): ComponentPublicInstance
  31. unmount(rootContainer: HostElement | string): void
  32. provide<T>(key: InjectionKey<T> | string, value: T): this
  33. // internal, but we need to expose these for the server-renderer and devtools
  34. _component: ConcreteComponent
  35. _props: Data | null
  36. _container: HostElement | null
  37. _context: AppContext
  38. }
  39. export type OptionMergeFunction = (
  40. to: unknown,
  41. from: unknown,
  42. instance: any,
  43. key: string
  44. ) => any
  45. export interface AppConfig {
  46. // @private
  47. readonly isNativeTag?: (tag: string) => boolean
  48. performance: boolean
  49. optionMergeStrategies: Record<string, OptionMergeFunction>
  50. globalProperties: Record<string, any>
  51. isCustomElement: (tag: string) => boolean
  52. errorHandler?: (
  53. err: unknown,
  54. instance: ComponentPublicInstance | null,
  55. info: string
  56. ) => void
  57. warnHandler?: (
  58. msg: string,
  59. instance: ComponentPublicInstance | null,
  60. trace: string
  61. ) => void
  62. }
  63. export interface AppContext {
  64. app: App // for devtools
  65. config: AppConfig
  66. mixins: ComponentOptions[]
  67. components: Record<string, Component>
  68. directives: Record<string, Directive>
  69. provides: Record<string | symbol, any>
  70. reload?: () => void // HMR only
  71. }
  72. type PluginInstallFunction = (app: App, ...options: any[]) => any
  73. export type Plugin =
  74. | PluginInstallFunction & { install?: PluginInstallFunction }
  75. | {
  76. install: PluginInstallFunction
  77. }
  78. export function createAppContext(): AppContext {
  79. return {
  80. app: null as any,
  81. config: {
  82. isNativeTag: NO,
  83. performance: false,
  84. globalProperties: {},
  85. optionMergeStrategies: {},
  86. isCustomElement: NO,
  87. errorHandler: undefined,
  88. warnHandler: undefined
  89. },
  90. mixins: [],
  91. components: {},
  92. directives: {},
  93. provides: Object.create(null)
  94. }
  95. }
  96. export type CreateAppFunction<HostElement> = (
  97. rootComponent: Component,
  98. rootProps?: Data | null
  99. ) => App<HostElement>
  100. export function createAppAPI<HostElement>(
  101. render: RootRenderFunction,
  102. hydrate?: RootHydrateFunction
  103. ): CreateAppFunction<HostElement> {
  104. return function createApp(rootComponent, rootProps = null) {
  105. if (rootProps != null && !isObject(rootProps)) {
  106. __DEV__ && warn(`root props passed to app.mount() must be an object.`)
  107. rootProps = null
  108. }
  109. const context = createAppContext()
  110. const installedPlugins = new Set()
  111. let isMounted = false
  112. const app: App = (context.app = {
  113. _component: rootComponent as ConcreteComponent,
  114. _props: rootProps,
  115. _container: null,
  116. _context: context,
  117. version,
  118. get config() {
  119. return context.config
  120. },
  121. set config(v) {
  122. if (__DEV__) {
  123. warn(
  124. `app.config cannot be replaced. Modify individual options instead.`
  125. )
  126. }
  127. },
  128. use(plugin: Plugin, ...options: any[]) {
  129. if (installedPlugins.has(plugin)) {
  130. __DEV__ && warn(`Plugin has already been applied to target app.`)
  131. } else if (plugin && isFunction(plugin.install)) {
  132. installedPlugins.add(plugin)
  133. plugin.install(app, ...options)
  134. } else if (isFunction(plugin)) {
  135. installedPlugins.add(plugin)
  136. plugin(app, ...options)
  137. } else if (__DEV__) {
  138. warn(
  139. `A plugin must either be a function or an object with an "install" ` +
  140. `function.`
  141. )
  142. }
  143. return app
  144. },
  145. mixin(mixin: ComponentOptions) {
  146. if (__FEATURE_OPTIONS_API__) {
  147. if (!context.mixins.includes(mixin)) {
  148. context.mixins.push(mixin)
  149. } else if (__DEV__) {
  150. warn(
  151. 'Mixin has already been applied to target app' +
  152. (mixin.name ? `: ${mixin.name}` : '')
  153. )
  154. }
  155. } else if (__DEV__) {
  156. warn('Mixins are only available in builds supporting Options API')
  157. }
  158. return app
  159. },
  160. component(name: string, component?: Component): any {
  161. if (__DEV__) {
  162. validateComponentName(name, context.config)
  163. }
  164. if (!component) {
  165. return context.components[name]
  166. }
  167. if (__DEV__ && context.components[name]) {
  168. warn(`Component "${name}" has already been registered in target app.`)
  169. }
  170. context.components[name] = component
  171. return app
  172. },
  173. directive(name: string, directive?: Directive) {
  174. if (__DEV__) {
  175. validateDirectiveName(name)
  176. }
  177. if (!directive) {
  178. return context.directives[name] as any
  179. }
  180. if (__DEV__ && context.directives[name]) {
  181. warn(`Directive "${name}" has already been registered in target app.`)
  182. }
  183. context.directives[name] = directive
  184. return app
  185. },
  186. mount(rootContainer: HostElement, isHydrate?: boolean): any {
  187. if (!isMounted) {
  188. const vnode = createVNode(
  189. rootComponent as ConcreteComponent,
  190. rootProps
  191. )
  192. // store app context on the root VNode.
  193. // this will be set on the root instance on initial mount.
  194. vnode.appContext = context
  195. // HMR root reload
  196. if (__DEV__) {
  197. context.reload = () => {
  198. render(cloneVNode(vnode), rootContainer)
  199. }
  200. }
  201. if (isHydrate && hydrate) {
  202. hydrate(vnode as VNode<Node, Element>, rootContainer as any)
  203. } else {
  204. render(vnode, rootContainer)
  205. }
  206. isMounted = true
  207. app._container = rootContainer
  208. // for devtools and telemetry
  209. ;(rootContainer as any).__vue_app__ = app
  210. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  211. devtoolsInitApp(app, version)
  212. }
  213. return vnode.component!.proxy
  214. } else if (__DEV__) {
  215. warn(
  216. `App has already been mounted.\n` +
  217. `If you want to remount the same app, move your app creation logic ` +
  218. `into a factory function and create fresh app instances for each ` +
  219. `mount - e.g. \`const createMyApp = () => createApp(App)\``
  220. )
  221. }
  222. },
  223. unmount() {
  224. if (isMounted) {
  225. render(null, app._container)
  226. devtoolsUnmountApp(app)
  227. } else if (__DEV__) {
  228. warn(`Cannot unmount an app that is not mounted.`)
  229. }
  230. },
  231. provide(key, value) {
  232. if (__DEV__ && key in context.provides) {
  233. warn(
  234. `App already provides property with key "${String(key)}". ` +
  235. `It will be overwritten with the new value.`
  236. )
  237. }
  238. // TypeScript doesn't allow symbols as index type
  239. // https://github.com/Microsoft/TypeScript/issues/24587
  240. context.provides[key as string] = value
  241. return app
  242. }
  243. })
  244. return app
  245. }
  246. }