refactor: adjust createApp related API signatures

BREAKING CHANGE: `createApp` API has been adjusted.

  - `createApp()` now accepts the root component, and optionally a props
  object to pass to the root component.
  - `app.mount()` now accepts a single argument (the root container)
  - `app.unmount()` no longer requires arguments.

  New behavior looks like the following:

  ``` js
  const app = createApp(RootComponent)
  app.mount('#app')
  app.unmount()
  ```
This commit is contained in:
Evan You
2020-01-23 15:05:38 -05:00
parent eacd390992
commit c07751fd36
25 changed files with 276 additions and 326 deletions

View File

@@ -30,18 +30,18 @@ describe('api: createApp', () => {
}
const root1 = nodeOps.createElement('div')
createApp().mount(Comp, root1)
createApp(Comp).mount(root1)
expect(serializeInner(root1)).toBe(`0`)
// mount with props
const root2 = nodeOps.createElement('div')
const app2 = createApp()
app2.mount(Comp, root2, { count: 1 })
const app2 = createApp(Comp, { count: 1 })
app2.mount(root2)
expect(serializeInner(root2)).toBe(`1`)
// remount warning
const root3 = nodeOps.createElement('div')
app2.mount(Comp, root3)
app2.mount(root3)
expect(serializeInner(root3)).toBe(``)
expect(`already been mounted`).toHaveBeenWarned()
})
@@ -59,18 +59,14 @@ describe('api: createApp', () => {
}
const root = nodeOps.createElement('div')
const app = createApp()
app.mount(Comp, root)
const app = createApp(Comp)
app.mount(root)
app.unmount(root)
expect(serializeInner(root)).toBe(``)
})
test('provide', () => {
const app = createApp()
app.provide('foo', 1)
app.provide('bar', 2)
const Root = {
setup() {
// test override
@@ -87,25 +83,16 @@ describe('api: createApp', () => {
}
}
const app = createApp(Root)
app.provide('foo', 1)
app.provide('bar', 2)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(serializeInner(root)).toBe(`3,2`)
})
test('component', () => {
const app = createApp()
const FooBar = () => 'foobar!'
app.component('FooBar', FooBar)
expect(app.component('FooBar')).toBe(FooBar)
app.component('BarBaz', () => 'barbaz!')
app.component('BarBaz', () => 'barbaz!')
expect(
'Component "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const Root = {
// local override
components: {
@@ -122,33 +109,29 @@ describe('api: createApp', () => {
}
}
const app = createApp(Root)
const FooBar = () => 'foobar!'
app.component('FooBar', FooBar)
expect(app.component('FooBar')).toBe(FooBar)
app.component('BarBaz', () => 'barbaz!')
app.component('BarBaz', () => 'barbaz!')
expect(
'Component "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
})
test('directive', () => {
const app = createApp()
const spy1 = jest.fn()
const spy2 = jest.fn()
const spy3 = jest.fn()
const FooBar = { mounted: spy1 }
app.directive('FooBar', FooBar)
expect(app.directive('FooBar')).toBe(FooBar)
app.directive('BarBaz', {
mounted: spy2
})
app.directive('BarBaz', {
mounted: spy2
})
expect(
'Directive "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const Root = {
// local override
directives: {
@@ -165,8 +148,25 @@ describe('api: createApp', () => {
}
}
const app = createApp(Root)
const FooBar = { mounted: spy1 }
app.directive('FooBar', FooBar)
expect(app.directive('FooBar')).toBe(FooBar)
app.directive('BarBaz', {
mounted: spy2
})
app.directive('BarBaz', {
mounted: spy2
})
expect(
'Directive "BarBaz" has already been registered in target app.'
).toHaveBeenWarnedTimes(1)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(spy1).toHaveBeenCalled()
expect(spy2).not.toHaveBeenCalled()
expect(spy3).toHaveBeenCalled()
@@ -232,7 +232,7 @@ describe('api: createApp', () => {
}
}
const app = createApp()
const app = createApp(Comp)
app.mixin(mixinA)
app.mixin(mixinB)
@@ -246,7 +246,7 @@ describe('api: createApp', () => {
).toHaveBeenWarnedTimes(1)
const root = nodeOps.createElement('div')
app.mount(Comp, root)
app.mount(root)
expect(serializeInner(root)).toBe(`123`)
expect(calls).toEqual([
@@ -272,11 +272,6 @@ describe('api: createApp', () => {
}
const PluginD: any = undefined
const app = createApp()
app.use(PluginA)
app.use(PluginB, 1, 1)
app.use(PluginC)
const Root = {
setup() {
const foo = inject('foo')
@@ -284,8 +279,14 @@ describe('api: createApp', () => {
return () => `${foo},${bar}`
}
}
const app = createApp(Root)
app.use(PluginA)
app.use(PluginB, 1, 1)
app.use(PluginC)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(serializeInner(root)).toBe(`1,2`)
app.use(PluginA)
@@ -301,18 +302,14 @@ describe('api: createApp', () => {
})
test('config.errorHandler', () => {
const app = createApp()
const error = new Error()
const count = ref(0)
const handler = (app.config.errorHandler = jest.fn(
(err, instance, info) => {
expect(err).toBe(error)
expect((instance as any).count).toBe(count.value)
expect(info).toBe(`render function`)
}
))
const handler = jest.fn((err, instance, info) => {
expect(err).toBe(error)
expect((instance as any).count).toBe(count.value)
expect(info).toBe(`render function`)
})
const Root = {
setup() {
@@ -326,21 +323,19 @@ describe('api: createApp', () => {
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
app.config.errorHandler = handler
app.mount(nodeOps.createElement('div'))
expect(handler).toHaveBeenCalled()
})
test('config.warnHandler', () => {
const app = createApp()
let ctx: any
const handler = (app.config.warnHandler = jest.fn(
(msg, instance, trace) => {
expect(msg).toMatch(`Component is missing template or render function`)
expect(instance).toBe(ctx.proxy)
expect(trace).toMatch(`Hello`)
}
))
const handler = jest.fn((msg, instance, trace) => {
expect(msg).toMatch(`Component is missing template or render function`)
expect(instance).toBe(ctx.proxy)
expect(trace).toMatch(`Hello`)
})
const Root = {
name: 'Hello',
@@ -349,7 +344,9 @@ describe('api: createApp', () => {
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
app.config.warnHandler = handler
app.mount(nodeOps.createElement('div'))
expect(handler).toHaveBeenCalledTimes(1)
})
@@ -357,104 +354,82 @@ describe('api: createApp', () => {
const isNativeTag = jest.fn(tag => tag === 'div')
test('Component.name', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
name: 'div',
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
test('Component.components', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
components: {
div: () => 'div'
},
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()
})
test('Component.directives', () => {
const app = createApp()
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
directives: {
bind: () => {}
},
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.mount(Root, nodeOps.createElement('div'))
const app = createApp(Root)
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in directive ids as custom directive id: bind`
).toHaveBeenWarned()
})
test('register using app.component', () => {
const app = createApp()
const app = createApp({
render() {}
})
Object.defineProperty(app.config, 'isNativeTag', {
value: isNativeTag,
writable: false
})
const Root = {
setup() {
return {
count: ref(0)
}
},
render() {
return null
}
}
app.component('div', () => 'div')
app.mount(Root, nodeOps.createElement('div'))
app.mount(nodeOps.createElement('div'))
expect(
`Do not use built-in or reserved HTML elements as component id: div`
).toHaveBeenWarned()

View File

@@ -1,4 +1,4 @@
import { createApp, getCurrentInstance, nodeOps } from '@vue/runtime-test'
import { h, render, getCurrentInstance, nodeOps } from '@vue/runtime-test'
import { mockWarn } from '@vue/shared'
import { ComponentInternalInstance } from '../src/component'
@@ -6,7 +6,6 @@ describe('component: proxy', () => {
mockWarn()
test('data', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -23,14 +22,13 @@ describe('component: proxy', () => {
return null
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
instanceProxy.foo = 2
expect(instance!.data.foo).toBe(2)
})
test('renderContext', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -47,14 +45,13 @@ describe('component: proxy', () => {
return null
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
instanceProxy.foo = 2
expect(instance!.renderContext.foo).toBe(2)
})
test('propsProxy', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -72,7 +69,7 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.foo).toBe(1)
expect(instance!.propsProxy!.foo).toBe(1)
expect(() => (instanceProxy.foo = 2)).toThrow(TypeError)
@@ -80,7 +77,6 @@ describe('component: proxy', () => {
})
test('public properties', () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -92,7 +88,7 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
expect(instanceProxy.$data).toBe(instance!.data)
expect(instanceProxy.$props).toBe(instance!.propsProxy)
expect(instanceProxy.$attrs).toBe(instance!.attrs)
@@ -108,7 +104,6 @@ describe('component: proxy', () => {
})
test('sink', async () => {
const app = createApp()
let instance: ComponentInternalInstance
let instanceProxy: any
const Comp = {
@@ -120,14 +115,13 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'))
render(h(Comp), nodeOps.createElement('div'))
instanceProxy.foo = 1
expect(instanceProxy.foo).toBe(1)
expect(instance!.sink.foo).toBe(1)
})
test('has check', () => {
const app = createApp()
let instanceProxy: any
const Comp = {
render() {},
@@ -148,7 +142,7 @@ describe('component: proxy', () => {
instanceProxy = this
}
}
app.mount(Comp, nodeOps.createElement('div'), { msg: 'hello' })
render(h(Comp, { msg: 'hello' }), nodeOps.createElement('div'))
// props
expect('msg' in instanceProxy).toBe(true)

View File

@@ -12,7 +12,6 @@ import { mockWarn } from '@vue/shared'
describe('resolveAssets', () => {
test('should work', () => {
const app = createApp()
const FooBar = () => null
const BarBaz = { mounted: () => null }
@@ -49,8 +48,9 @@ describe('resolveAssets', () => {
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(component1!).toBe(FooBar)
expect(component2!).toBe(FooBar)
expect(component3!).toBe(FooBar)
@@ -78,7 +78,6 @@ describe('resolveAssets', () => {
})
test('not exist', () => {
const app = createApp()
const Root = {
setup() {
resolveComponent('foo')
@@ -87,14 +86,14 @@ describe('resolveAssets', () => {
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect('Failed to resolve component: foo').toHaveBeenWarned()
expect('Failed to resolve directive: bar').toHaveBeenWarned()
})
test('resolve dynamic component', () => {
const app = createApp()
const dynamicComponents = {
foo: () => 'foo',
bar: () => 'bar',
@@ -112,8 +111,10 @@ describe('resolveAssets', () => {
}
}
}
const app = createApp(Root)
const root = nodeOps.createElement('div')
app.mount(Root, root)
app.mount(root)
expect(foo).toBe(dynamicComponents.foo)
expect(bar).toBe(dynamicComponents.bar)
expect(baz).toBe(dynamicComponents.baz)

View File

@@ -16,16 +16,11 @@ export interface App<HostElement = any> {
component(name: string, component: Component): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
mount(
rootComponent:
| Component
// for compatibility with defineComponent() return types
| { new (): ComponentPublicInstance<any, any, any, any, any> },
rootContainer: HostElement | string,
rootProps?: Data
): ComponentPublicInstance
mount(rootContainer: HostElement | string): ComponentPublicInstance
unmount(rootContainer: HostElement | string): void
provide<T>(key: InjectionKey<T> | string, value: T): this
rootComponent: Component
rootContainer: HostElement | null
}
export interface AppConfig {
@@ -79,16 +74,30 @@ export function createAppContext(): AppContext {
}
}
export type CreateAppFunction<HostElement> = (
rootComponent:
| Component
// for compatibility with defineComponent() return types
| { new (): ComponentPublicInstance<any, any, any, any, any> },
rootProps?: Data | null
) => App<HostElement>
export function createAppAPI<HostNode, HostElement>(
render: RootRenderFunction<HostNode, HostElement>
): () => App<HostElement> {
return function createApp(): App {
): CreateAppFunction<HostElement> {
return function createApp(
rootComponent: Component,
rootProps?: Data | null
): App {
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = {
rootComponent,
rootContainer: null,
get config() {
return context.config
},
@@ -165,11 +174,7 @@ export function createAppAPI<HostNode, HostElement>(
return app
},
mount(
rootComponent: Component,
rootContainer: HostElement,
rootProps?: Data | null
): any {
mount(rootContainer: HostElement): any {
if (!isMounted) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ &&
@@ -190,6 +195,7 @@ export function createAppAPI<HostNode, HostElement>(
render(vnode, rootContainer)
isMounted = true
app.rootContainer = rootContainer
return vnode.component!.proxy
} else if (__DEV__) {
warn(
@@ -198,8 +204,12 @@ export function createAppAPI<HostNode, HostElement>(
}
},
unmount(rootContainer: HostElement) {
render(null, rootContainer)
unmount() {
if (isMounted) {
render(null, app.rootContainer!)
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
provide(key, value) {

View File

@@ -99,7 +99,13 @@ export { registerRuntimeCompiler } from './component'
// Types -----------------------------------------------------------------------
export { App, AppConfig, AppContext, Plugin } from './apiCreateApp'
export {
App,
AppConfig,
AppContext,
Plugin,
CreateAppFunction
} from './apiCreateApp'
export { VNode, VNodeTypes, VNodeProps } from './vnode'
export {
Component,

View File

@@ -46,7 +46,7 @@ import { ShapeFlags } from './shapeFlags'
import { pushWarningContext, popWarningContext, warn } from './warning'
import { invokeDirectiveHook } from './directives'
import { ComponentPublicInstance } from './componentProxy'
import { App, createAppAPI } from './apiCreateApp'
import { createAppAPI, CreateAppFunction } from './apiCreateApp'
import {
SuspenseBoundary,
queueEffectWithSuspense,
@@ -174,7 +174,7 @@ export function createRenderer<
options: RendererOptions<HostNode, HostElement>
): {
render: RootRenderFunction<HostNode, HostElement>
createApp: () => App<HostElement>
createApp: CreateAppFunction<HostElement>
} {
type HostVNode = VNode<HostNode, HostElement>
type HostVNodeChildren = VNodeChildren<HostNode, HostElement>