From: daiwei Date: Fri, 24 Oct 2025 03:53:18 +0000 (+0800) Subject: feat: implement defineVaporCustomElement X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e808152f14cfec42987a89a43e0cadf230039977;p=thirdparty%2Fvuejs%2Fcore.git feat: implement defineVaporCustomElement --- diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 6b1c8f0cae..b8e5811700 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -3,7 +3,6 @@ import { type Component, type ComponentCustomElementInterface, type ComponentInjectOptions, - type ComponentInternalInstance, type ComponentObjectPropsOptions, type ComponentOptions, type ComponentOptionsBase, @@ -19,6 +18,7 @@ import { type EmitsOptions, type EmitsToProps, type ExtractPropTypes, + type GenericComponentInstance, type MethodOptions, type RenderFunction, type SetupContext, @@ -200,7 +200,9 @@ const BaseClass = ( type InnerComponentDef = ConcreteComponent & CustomElementOptions -export class VueElement +export abstract class VueElementBase< + Def extends CustomElementOptions & { props?: any } = InnerComponentDef, + > extends BaseClass implements ComponentCustomElementInterface { @@ -208,7 +210,7 @@ export class VueElement /** * @internal */ - _instance: ComponentInternalInstance | null = null + _instance: GenericComponentInstance | null = null /** * @internal */ @@ -220,41 +222,48 @@ export class VueElement /** * @internal */ - _nonce: string | undefined = this._def.nonce - + _nonce: string | undefined /** * @internal */ _teleportTargets?: Set - private _connected = false - private _resolved = false - private _numberProps: Record | null = null - private _styleChildren = new WeakSet() - private _pendingResolve: Promise | undefined - private _parent: VueElement | undefined + protected _def: Def + protected _props: Record + protected _createApp: CreateAppFunction + protected _connected = false + protected _resolved = false + protected _numberProps: Record | null = null + protected _styleChildren: WeakSet = new WeakSet() + protected _pendingResolve: Promise | undefined + protected _parent: VueElementBase | undefined + /** * dev only */ - private _styles?: HTMLStyleElement[] + protected _styles?: HTMLStyleElement[] /** * dev only */ - private _childStyles?: Map - private _ob?: MutationObserver | null = null - private _slots?: Record + protected _childStyles?: Map + protected _ob?: MutationObserver | null = null + protected _slots?: Record constructor( /** * Component def - note this may be an AsyncWrapper, and this._def will * be overwritten by the inner component when resolved. */ - private _def: InnerComponentDef, - private _props: Record = {}, - private _createApp: CreateAppFunction = createApp, + def: Def, + props: Record = {}, + createAppFn: CreateAppFunction = createApp, ) { super() - if (this.shadowRoot && _createApp !== createApp) { + this._def = def + this._props = props + this._createApp = createAppFn + this._nonce = def.nonce + if (this.shadowRoot && createAppFn !== createApp) { this._root = this.shadowRoot } else { if (__DEV__ && this.shadowRoot) { @@ -263,9 +272,9 @@ export class VueElement `defined as hydratable. Use \`defineSSRCustomElement\`.`, ) } - if (_def.shadowRoot !== false) { + if (def.shadowRoot !== false) { this.attachShadow( - extend({}, _def.shadowRootOptions, { + extend({}, def.shadowRootOptions, { mode: 'open', }) as ShadowRootInit, ) @@ -291,7 +300,7 @@ export class VueElement while ( (parent = parent && (parent.parentNode || (parent as ShadowRoot).host)) ) { - if (parent instanceof VueElement) { + if (parent instanceof VueElementBase) { this._parent = parent break } @@ -299,7 +308,7 @@ export class VueElement if (!this._instance) { if (this._resolved) { - this._mount(this._def) + this._mountComponent(this._def) } else { if (parent && parent._pendingResolve) { this._pendingResolve = parent._pendingResolve.then(() => { @@ -313,14 +322,22 @@ export class VueElement } } - private _setParent(parent = this._parent) { - if (parent) { - this._instance!.parent = parent._instance + protected abstract _mountComponent(def: Def): void + protected abstract _updateComponent(): void + protected abstract _unmountComponent(): void + + protected _setParent( + parent: VueElementBase | undefined = this._parent, + ): void { + if (parent && this._instance) { + this._instance.parent = parent._instance this._inheritParentContext(parent) } } - private _inheritParentContext(parent = this._parent) { + protected _inheritParentContext( + parent: VueElementBase | undefined = this._parent, + ): void { // #13212, the provides object of the app context must inherit the provides // object from the parent element so we can inject values from both places if (parent && this._app) { @@ -339,10 +356,7 @@ export class VueElement this._ob.disconnect() this._ob = null } - // unmount - this._app && this._app.unmount() - if (this._instance) this._instance.ce = undefined - this._app = this._instance = null + this._unmountComponent() if (this._teleportTargets) { this._teleportTargets.clear() this._teleportTargets = undefined @@ -375,7 +389,7 @@ export class VueElement this._ob.observe(this, { attributes: true }) - const resolve = (def: InnerComponentDef, isAsync = false) => { + const resolve = (def: Def) => { this._resolved = true this._pendingResolve = undefined @@ -410,51 +424,23 @@ export class VueElement } // initial mount - this._mount(def) + this._mountComponent(def) } const asyncDef = (this._def as ComponentOptions).__asyncLoader if (asyncDef) { - this._pendingResolve = asyncDef().then((def: InnerComponentDef) => { - def.configureApp = this._def.configureApp - resolve((this._def = def), true) + const { configureApp } = this._def + this._pendingResolve = asyncDef().then((def: any) => { + def.configureApp = configureApp + this._def = def + resolve(def) }) } else { resolve(this._def) } } - private _mount(def: InnerComponentDef) { - if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) { - // @ts-expect-error - def.name = 'VueElement' - } - this._app = this._createApp(def) - // inherit before configureApp to detect context overwrites - this._inheritParentContext() - if (def.configureApp) { - def.configureApp(this._app) - } - this._app._ceVNode = this._createVNode() - this._app.mount(this._root) - - // apply expose after mount - const exposed = this._instance && this._instance.exposed - if (!exposed) return - for (const key in exposed) { - if (!hasOwn(this, key)) { - // exposed properties are readonly - Object.defineProperty(this, key, { - // unwrap ref to be consistent with public instance behavior - get: () => unref(exposed[key]), - }) - } else if (__DEV__) { - warn(`Exposed property "${key}" already exists on custom element.`) - } - } - } - - private _resolveProps(def: InnerComponentDef) { + private _resolveProps(def: Def): void { const { props } = def const declaredPropKeys = isArray(props) ? props : Object.keys(props || {}) @@ -478,7 +464,7 @@ export class VueElement } } - protected _setAttr(key: string): void { + private _setAttr(key: string): void { if (key.startsWith('data-v-')) return const has = this.hasAttribute(key) let value = has ? this.getAttribute(key) : REMOVAL @@ -511,12 +497,12 @@ export class VueElement } else { this._props[key] = val // support set key on ceVNode - if (key === 'key' && this._app) { + if (key === 'key' && this._app && this._app._ceVNode) { this._app._ceVNode!.key = val } } if (shouldUpdate && this._instance) { - this._update() + this._updateComponent() } // reflect if (shouldReflect) { @@ -537,69 +523,10 @@ export class VueElement } } - private _update() { - const vnode = this._createVNode() - if (this._app) vnode.appContext = this._app._context - render(vnode, this._root) - } - - private _createVNode(): VNode { - const baseProps: VNodeProps = {} - if (!this.shadowRoot) { - baseProps.onVnodeMounted = baseProps.onVnodeUpdated = - this._renderSlots.bind(this) - } - const vnode = createVNode(this._def, extend(baseProps, this._props)) - if (!this._instance) { - vnode.ce = instance => { - this._instance = instance - instance.ce = this - instance.isCE = true // for vue-i18n backwards compat - // HMR - if (__DEV__) { - instance.ceReload = newStyles => { - // always reset styles - if (this._styles) { - this._styles.forEach(s => this._root.removeChild(s)) - this._styles.length = 0 - } - this._applyStyles(newStyles) - this._instance = null - this._update() - } - } - - const dispatch = (event: string, args: any[]) => { - this.dispatchEvent( - new CustomEvent( - event, - isPlainObject(args[0]) - ? extend({ detail: args }, args[0]) - : { detail: args }, - ), - ) - } - - // intercept emit - instance.emit = (event: string, ...args: any[]) => { - // dispatch both the raw and hyphenated versions of an event - // to match Vue behavior - dispatch(event, args) - if (hyphenate(event) !== event) { - dispatch(hyphenate(event), args) - } - } - - this._setParent() - } - } - return vnode - } - - private _applyStyles( + protected _applyStyles( styles: string[] | undefined, owner?: ConcreteComponent, - ) { + ): void { if (!styles) return if (owner) { if (owner === this._def || this._styleChildren.has(owner)) { @@ -635,7 +562,7 @@ export class VueElement * Only called when shadowRoot is false */ private _parseSlots() { - const slots: VueElement['_slots'] = (this._slots = {}) + const slots: VueElementBase['_slots'] = (this._slots = {}) let n while ((n = this.firstChild)) { const slotName = @@ -648,7 +575,7 @@ export class VueElement /** * Only called when shadowRoot is false */ - private _renderSlots() { + protected _renderSlots(): void { const outlets = this._getSlots() const scopeId = this._instance!.type.__scopeId for (let i = 0; i < outlets.length; i++) { @@ -690,6 +617,7 @@ export class VueElement return res }, []) } + /** * @internal */ @@ -715,9 +643,102 @@ export class VueElement } } -export function useHost(caller?: string): VueElement | null { +export class VueElement extends VueElementBase { + protected _mountComponent(def: InnerComponentDef): void { + if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) { + // @ts-expect-error + def.name = 'VueElement' + } + this._app = this._createApp(def) + this._inheritParentContext() + if (def.configureApp) { + def.configureApp(this._app) + } + this._app._ceVNode = this._createVNode() + this._app.mount(this._root) + + const exposed = this._instance && this._instance.exposed + if (!exposed) return + for (const key in exposed) { + if (!hasOwn(this, key)) { + Object.defineProperty(this, key, { + get: () => unref(exposed[key]), + }) + } else if (__DEV__) { + warn(`Exposed property "${key}" already exists on custom element.`) + } + } + } + + protected _updateComponent(): void { + if (!this._app) return + const vnode = this._createVNode() + vnode.appContext = this._app._context + render(vnode, this._root) + } + + protected _unmountComponent(): void { + if (this._app) { + this._app.unmount() + } + if (this._instance && this._instance.ce) { + this._instance.ce = undefined + } + this._app = this._instance = null + } + + private _createVNode(): VNode { + const baseProps: VNodeProps = {} + if (!this.shadowRoot) { + baseProps.onVnodeMounted = baseProps.onVnodeUpdated = + this._renderSlots.bind(this) + } + const vnode = createVNode(this._def, extend(baseProps, this._props)) + if (!this._instance) { + vnode.ce = instance => { + this._instance = instance + instance.ce = this + instance.isCE = true + if (__DEV__) { + instance.ceReload = newStyles => { + if (this._styles) { + this._styles.forEach(s => this._root.removeChild(s)) + this._styles.length = 0 + } + this._applyStyles(newStyles) + this._instance = null + this._updateComponent() + } + } + + const dispatch = (event: string, args: any[]) => { + this.dispatchEvent( + new CustomEvent( + event, + isPlainObject(args[0]) + ? extend({ detail: args }, args[0]) + : { detail: args }, + ), + ) + } + + instance.emit = (event: string, ...args: any[]) => { + dispatch(event, args) + if (hyphenate(event) !== event) { + dispatch(hyphenate(event), args) + } + } + + this._setParent() + } + } + return vnode + } +} + +export function useHost(caller?: string): VueElementBase | null { const instance = getCurrentInstance() - const el = instance && (instance.ce as VueElement) + const el = instance && (instance.ce as VueElementBase) if (el) { return el } else if (__DEV__) { diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index c4518b0029..0d76547019 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -261,6 +261,7 @@ export { useShadowRoot, useHost, VueElement, + VueElementBase, type VueElementConstructor, type CustomElementOptions, } from './apiCustomElement' diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts new file mode 100644 index 0000000000..bda741a0b7 --- /dev/null +++ b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts @@ -0,0 +1,49 @@ +import { extend, isPlainObject } from '@vue/shared' +import { defineVaporComponent } from '.' +import { + type CreateAppFunction, + type CustomElementOptions, + VueElementBase, +} from '@vue/runtime-dom' +import type { ObjectVaporComponent, VaporComponent } from './component' + +export type VaporElementConstructor

= { + new (initialProps?: Record): VaporElement & P +} + +// TODO type inference + +/*@__NO_SIDE_EFFECTS__*/ +export function defineCustomElement( + options: any, + extraOptions?: Omit, + /** + * @internal + */ + _createApp?: CreateAppFunction, +): VaporElementConstructor { + let Comp = defineVaporComponent(options, extraOptions) + if (isPlainObject(Comp)) Comp = extend({}, Comp, extraOptions) + class VaporCustomElement extends VaporElement { + static def = Comp + constructor(initialProps?: Record) { + super(Comp, initialProps, _createApp) + } + } + + return VaporCustomElement +} + +type VaporInnerComponentDef = VaporComponent & CustomElementOptions + +export class VaporElement extends VueElementBase { + protected _mountComponent(def: VaporInnerComponentDef): void { + throw new Error('Method not implemented.') + } + protected _updateComponent(): void { + throw new Error('Method not implemented.') + } + protected _unmountComponent(): void { + throw new Error('Method not implemented.') + } +}