From e2d6ff845b224c6f783212f264ebd03f6b572c4f Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 5 Apr 2021 11:54:35 -0400 Subject: [PATCH] wip: root mount api compat --- packages/runtime-core/src/apiCreateApp.ts | 141 +++++++++++++++++- packages/runtime-core/src/component.ts | 7 +- .../src/componentPublicInstance.ts | 23 ++- packages/runtime-core/src/renderer.ts | 29 ++-- packages/runtime-dom/src/index.ts | 22 +-- packages/shared/src/deprecations.ts | 16 +- packages/vue-compat/src/apiGlobal.ts | 20 ++- 7 files changed, 228 insertions(+), 30 deletions(-) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 92cce584..8ef0a2a2 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -2,19 +2,28 @@ import { ConcreteComponent, Data, validateComponentName, - Component + Component, + createComponentInstance, + setupComponent, + finishComponentSetup } from './component' import { ComponentOptions } from './componentOptions' import { ComponentPublicInstance } from './componentPublicInstance' 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' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { version } from '.' +import { + isFunction, + NO, + isObject, + warnDeprecation, + DeprecationTypes +} from '@vue/shared' export interface App { version: string @@ -39,6 +48,11 @@ export interface App { _props: Data | null _container: HostElement | null _context: AppContext + + /** + * @internal 2.x compat only + */ + _createRoot?(options: ComponentOptions): ComponentPublicInstance } export type OptionMergeFunction = ( @@ -298,6 +312,129 @@ export function createAppAPI( } }) + if (__COMPAT__) { + /** + * Vue 2 supports the behavior of creating a component instance but not + * mounting it, which is no longer possible in Vue 3 - this internal + * function simulates that behavior. + */ + app._createRoot = options => { + const vnode = createVNode( + rootComponent as ConcreteComponent, + options.propsData || null + ) + vnode.appContext = context + + const hasNoRender = + !isFunction(rootComponent) && + !rootComponent.render && + !rootComponent.template + const emptyRender = () => {} + + // create root instance + const instance = createComponentInstance(vnode, null, null) + // suppress "missing render fn" warning since it can't be determined + // until $mount is called + if (hasNoRender) { + instance.render = emptyRender + } + setupComponent(instance, __NODE_JS__) + vnode.component = instance + + // $mount & $destroy + // these are defined on ctx and picked up by the $mount/$destroy + // public property getters on the instance proxy. + // Note: the following assumes DOM environment since the compat build + // only targets web. It essentially includes logic for app.mount from + // both runtime-core AND runtime-dom. + instance.ctx._compat_mount = (selectorOrEl: string | Element) => { + if (isMounted) { + __DEV__ && warn(`Root instance is already mounted.`) + return + } + + let container: Element + if (typeof selectorOrEl === 'string') { + // eslint-disable-next-line + const result = document.querySelector(selectorOrEl) + if (!result) { + __DEV__ && + warn( + `Failed to mount root instance: selector "${selectorOrEl}" returned null.` + ) + return + } + container = result + } else { + if (!selectorOrEl) { + __DEV__ && + warn( + `Failed to mount root instance: invalid mount target ${selectorOrEl}.` + ) + return + } + container = selectorOrEl + } + + const isSVG = container instanceof SVGElement + + // HMR root reload + if (__DEV__) { + context.reload = () => { + const cloned = cloneVNode(vnode) + // compat mode will use instance if not reset to null + cloned.component = null + render(cloned, container, isSVG) + } + } + + // resolve in-DOM template if component did not provide render + // and no setup/mixin render functions are provided (by checking + // that the instance is still using the placeholder render fn) + if (hasNoRender && instance.render === emptyRender) { + // root directives check + if (__DEV__) { + for (let i = 0; i < container.attributes.length; i++) { + const attr = container.attributes[i] + if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { + warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT) + break + } + } + } + instance.render = null + ;(rootComponent as ComponentOptions).template = container.innerHTML + finishComponentSetup(instance, __NODE_JS__, true /* skip options */) + } + + // clear content before mounting + container.innerHTML = '' + + // TODO hydration + render(vnode, container, isSVG) + + if (container instanceof Element) { + container.removeAttribute('v-cloak') + container.setAttribute('data-v-app', '') + } + + isMounted = true + app._container = container + // for devtools and telemetry + ;(container as any).__vue_app__ = app + if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { + devtoolsInitApp(app, version) + } + + return instance.proxy! + } + + instance.ctx._compat_destroy = app.unmount + + return instance.proxy! + } + } + return app } } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 6e2c1d53..635bccc4 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -674,9 +674,10 @@ export function registerRuntimeCompiler(_compile: any) { compile = _compile } -function finishComponentSetup( +export function finishComponentSetup( instance: ComponentInternalInstance, - isSSR: boolean + isSSR: boolean, + skipOptions?: boolean ) { const Component = instance.type as ComponentOptions @@ -719,7 +720,7 @@ function finishComponentSetup( } // support for 2.x options - if (__FEATURE_OPTIONS_API__) { + if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) { currentInstance = instance pauseTracking() applyOptions(instance, Component) diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index fd0351f6..c2cc5c16 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -11,7 +11,9 @@ import { isGloballyWhitelisted, NOOP, extend, - isString + isString, + warnDeprecation, + DeprecationTypes } from '@vue/shared' import { ReactiveEffect, @@ -233,6 +235,25 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) } as PublicPropertiesMap) +if (__COMPAT__) { + extend(publicPropertiesMap, { + $mount: i => { + if (__DEV__) { + warnDeprecation(DeprecationTypes.$MOUNT) + } + // root mount override from apiCreateApp.ts + return i.ctx._compat_mount || NOOP + }, + $destroy: i => { + if (__DEV__) { + warnDeprecation(DeprecationTypes.$DESTROY) + } + // root destroy override from apiCreateApp.ts + return i.ctx._compat_destroy || NOOP + } + } as PublicPropertiesMap) +} + const enum AccessTypes { SETUP, DATA, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 2c1b2bbd..ea065d58 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1292,11 +1292,16 @@ function baseCreateRenderer( isSVG, optimized ) => { - const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( - initialVNode, - parentComponent, - parentSuspense - )) + // 2.x compat may pre-creaate the component instance before actually + // mounting + const compatMountInstance = __COMPAT__ && initialVNode.component + const instance: ComponentInternalInstance = + compatMountInstance || + (initialVNode.component = createComponentInstance( + initialVNode, + parentComponent, + parentSuspense + )) if (__DEV__ && instance.type.__hmrId) { registerHMR(instance) @@ -1313,12 +1318,14 @@ function baseCreateRenderer( } // resolve props and slots for setup context - if (__DEV__) { - startMeasure(instance, `init`) - } - setupComponent(instance) - if (__DEV__) { - endMeasure(instance, `init`) + if (!(__COMPAT__ && compatMountInstance)) { + if (__DEV__) { + startMeasure(instance, `init`) + } + setupComponent(instance) + if (__DEV__) { + endMeasure(instance, `init`) + } } // setup() is async. This component relies on async logic to be resolved diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index b2a87a60..1a4265aa 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -72,17 +72,6 @@ export const createApp = ((...args) => { const container = normalizeContainer(containerOrSelector) if (!container) return - // 2.x compat check - if (__COMPAT__ && __DEV__) { - for (let i = 0; i < container.attributes.length; i++) { - const attr = container.attributes[i] - if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { - warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT) - break - } - } - } - const component = app._component if (!isFunction(component) && !component.render && !component.template) { // __UNSAFE__ @@ -90,7 +79,18 @@ export const createApp = ((...args) => { // The user must make sure the in-DOM template is trusted. If it's // rendered by the server, the template should not contain any user data. component.template = container.innerHTML + // 2.x compat check + if (__COMPAT__ && __DEV__) { + for (let i = 0; i < container.attributes.length; i++) { + const attr = container.attributes[i] + if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { + warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT) + break + } + } + } } + // clear content before mounting container.innerHTML = '' const proxy = mount(container, false, container instanceof SVGElement) diff --git a/packages/shared/src/deprecations.ts b/packages/shared/src/deprecations.ts index f3d23750..ff097f3d 100644 --- a/packages/shared/src/deprecations.ts +++ b/packages/shared/src/deprecations.ts @@ -1,5 +1,7 @@ export const enum DeprecationTypes { - DOM_TEMPLATE_MOUNT + DOM_TEMPLATE_MOUNT, + $MOUNT, + $DESTROY } type DeprecationData = { @@ -14,6 +16,18 @@ const deprecations: Record = { `In Vue 3, the container is no longer considered part of the template ` + `and will not be processed/replaced.`, link: `https://v3.vuejs.org/guide/migration/mount-changes.html` + }, + + [DeprecationTypes.$MOUNT]: { + message: + `vm.$mount() has been deprecated. ` + + `Use createApp(RootComponent).mount() instead.`, + link: `https://v3.vuejs.org/guide/migration/global-api.html#mounting-app-instance` + }, + + [DeprecationTypes.$DESTROY]: { + message: `vm.$destroy() has been deprecated. Use app.unmount() instead.`, + link: `https://v3.vuejs.org/api/application-api.html#unmount` } } diff --git a/packages/vue-compat/src/apiGlobal.ts b/packages/vue-compat/src/apiGlobal.ts index 10ad71e9..1a9f95e2 100644 --- a/packages/vue-compat/src/apiGlobal.ts +++ b/packages/vue-compat/src/apiGlobal.ts @@ -13,6 +13,7 @@ import { RenderFunction, isRuntimeOnly } from '@vue/runtime-dom' +import { extend } from '@vue/shared' // TODO make these getter/setters and trigger deprecation warnings export type LegacyConfig = AppConfig & { @@ -89,6 +90,7 @@ export type GlobalVue = Pick & { export const Vue: GlobalVue = function Vue(options: ComponentOptions = {}) { const app = createApp(options) + // copy over global config mutations for (const key in singletonApp.config) { if ( @@ -99,8 +101,13 @@ export const Vue: GlobalVue = function Vue(options: ComponentOptions = {}) { app.config[key] = singletonApp.config[key] } } + + // TODO copy prototype augmentations as config.globalProperties + if (options.el) { return app.mount(options.el) + } else { + return app._createRoot!(options) } } as any @@ -109,7 +116,18 @@ const singletonApp = createApp({}) Vue.version = __VERSION__ Vue.config = singletonApp.config -Vue.extend = defineComponent +Vue.extend = ((baseOptions: ComponentOptions = {}) => { + return function ExtendedVueConstructor(inlineOptions?: ComponentOptions) { + if (!inlineOptions) { + return new Vue(baseOptions) + } else { + const mergedOptions = extend({}, baseOptions) + mergedOptions.mixins = [inlineOptions, ...(mergedOptions.mixins || [])] + return new Vue(mergedOptions) + } + } +}) as any + Vue.nextTick = nextTick Vue.set = (target, key, value) => {