Should I write v1 Custom Elements in a way that is

2019-08-02 00:17发布

问题:

Due to the renaming of some class methods (attached/detached to connected/disconnected) and removal of createdCallback in favor of constructors (which is a good thing!), I find myself writing my WebComponent base class (class-factory mixin) as follows.

My question is, should I be doing what I'm doing, or do you recommend something else? Note that the attached/detachedCallback methods simply call the connected/disconnectedCallback methods, and also note what I've done with createdCallback and the constructor due to the differences between v0 and v1. Subclasses of my WebComponent base class still use createdCallback rather than a constructor in order to be backwards compatible.

The code is as follows, with parts removed for brevity in order to show just the parts dealing with the v0/v1 APIs directly and how backwards compatibility is maintained:

// Very very stupid hack needed for Safari in order for us to be able to extend
// the HTMLElement class. See:
// https://github.com/google/traceur-compiler/issues/1709
if (typeof window.HTMLElement != 'function') {
    const _HTMLElement = function HTMLElement(){}
    _HTMLElement.prototype = window.HTMLElement.prototype
    window.HTMLElement = _HTMLElement
}

// XXX: Maybe we can improve by clearing items after X amount of time?
const classCache = new Map

function hasHTMLElementPrototype(constructor) {
    if (!constructor) return false
    if (constructor === HTMLElement) return true
    else return hasHTMLElementPrototype(constructor.prototype)
}

/**
 * Creates a WebComponent base class dynamically, depending on which
 * HTMLElement class you want it to extend from. Extend from WebComponent when
 * making a new Custom Element class.
 *
 * @example
 * import WebComponent from './WebComponent'
 * class AwesomeButton extends WebComponent(HTMLButtonElement) { ... }
 *
 * @param {Function} elementClass The class that the generated WebComponent
 * base class will extend from.
 */
export default
function WebComponentMixin(elementClass) {
    if (!elementClass) elementClass = HTMLElement

    if (!hasHTMLElementPrototype(elementClass)) {
        throw new TypeError(
            'The argument to WebComponentMixin must be a constructor that extends from or is HTMLElement.'
        )
    }

    // if a base class that extends the given `elementClass` has already been
    // created, return it.
    if (classCache.has(elementClass))
        return classCache.get(elementClass)

    // otherwise, create it.
    class WebComponent extends elementClass {

        // constructor() is used in v1 Custom Elements instead of
        // createdCallback() as in v0.
        constructor() {
            super()

            // If the following is true, then we know the user should be using
            // `document.registerElement()` to define an element from this class.
            // `document.registerElement()` creates a new constructor, so if the
            // constructor here is being called then that means the user is not
            // instantiating a DOM HTMLElement as expected because it is required
            // that the constructor returned from `document.registerElement` be used
            // instead (this is a flaw of Custom Elements v0 which is fixed in v1
            // where class constructors can be used directly).
            if (document.registerElement && !customElements.define) {

                // TODO: link to docs.
                throw new Error(`
                    You cannot instantiate this class directly without first registering it
                    with \`document.registerElement(...)\`. See an example at http://....
                `)

            }

            // Throw an error if no Custom Elements API exists.
            if (!document.registerElement && !customElements.define) {

                // TODO: link to docs.
                throw new Error(`
                    Your browser does not support the Custom Elements API. You'll
                    need to install a polyfill. See how at http://....
                `)

            }

            // otherwise the V1 API exists, so call the createdCallback, which
            // is what Custom Elements v0 would call by default. Subclasses of
            // WebComponent should put instantiation logic in createdCallback
            // instead of in a custom constructor if backwards compatibility is
            // to be maintained.
            this.createdCallback()
        }

        createdCallback() {
            // code removed for brevity...
        }

        connectedCallback() {
            // code removed for brevity...
        }
        attachedCallback() { this.connectedCallback() } // back-compat

        disconnectedCallback() {
            // code removed for brevity...
        }
        detachedCallback() { this.disconnectedCallback() } // back-compat
    }

    classCache.set(elementClass, WebComponent)
    return WebComponent
}

Any thoughts?