如何正确处理带有子元素的Javascript自定义元素(Web组件)?

Luk*_* Vo 2 html javascript web-component custom-element

我有一个Custom Element应该有很多 HTML 孩子的。我在课堂上初始化它时遇到了这个问题 constructor ' (结果一定没有孩子)。我明白原因并知道如何解决它。但我现在应该如何围绕它设计我的课程呢?请考虑这段代码:

class MyElement extends HTMLElement {
  constructor() {
    super();
  }  
  
  // Due to the problem, these codes that should be in constructor are moved here
  connectedCallback() {
    // Should have check for first time connection as well but ommited here for brevity
    this.innerHTML = `<a></a><div></div>`;
    this.a = this.querySelector("a");
    this.div = this.querySelector("div");
  }
  
  set myText(v) {
    this.a.textContent = v;
  }
  
  set url(v) {
    this.a.href = v;
  }
}

customElements.define("my-el", MyElement);

const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el); // connectedCallback is not called yet since it's not technically connected to the document.

el.myText = "abc"; // Now this wouldn't work because connectedCallback isn't called
el.url = "https://www.example.com/";
Run Code Online (Sandbox Code Playgroud)

由于MyElement将在列表中使用,因此它是预先设置并插入到DocumentFragment. 你如何处理这个问题?

目前,我正在保留预连接属性的列表,并在实际连接时设置它们,但我无法想象这是一个好的解决方案。我还想到了另一种解决方案:有一个init方法(好吧,我刚刚意识到没有什么可以阻止你调用connectedCallback自己),在做任何事情之前必须手动调用它,但我自己没有看到任何需要这样做的组件,它类似于upgrade弱点上面文章中提到:

不得检查元素的属性和子元素,因为在非升级情况下不会出现任何属性和子元素,并且依赖升级会降低元素的可用性。

Jor*_*ton 5

自定义元素很难使用。

ShadowDOM

如果shadowDOM功能和限制适合您的需求,您应该选择它,这很简单:

customElements.define('my-test', class extends HTMLElement{
    constructor(){
        super();
        this.shadow = this.attachShadow({mode: 'open'});
        const div = document.createElement('div');
        div.innerText = "Youhou";
        this.shadow.appendChild(div);
    }
});

const myTest = document.createElement('my-test');
console.log(myTest.shadow.querySelector('div')); //Outputs your div.
Run Code Online (Sandbox Code Playgroud)

更多相关信息请参阅此处

没有shadowDOM

有时,shadowDOM限制太多。它提供了非常好的隔离,但是如果您的组件被设计为在应用程序中使用,而不是分发给每个人在任何项目中使用,那么管理起来确实是一场噩梦。

请记住,我在下面提供的解决方案只是如何解决此问题的一个想法,您可能想要管理的远不止于此,特别是如果您使用attributeChangedCallback,如果您需要支持组件重新加载或许多其他未涵盖的用例通过这个答案。

如果像我一样,你不想要这些ShadowDOM功能,并且有很多理由不想要它(级联 CSS、使用像 fontawesome 这样的库而不必在每个组件中重新声明链接、全局 i18n 机制、能够使用自定义组件(如任何其他 DOM 标签等),有一些线索:

创建一个基类,该基类将对所有组件以相同的方式处理它,我们称之为BaseWebComponent

class BaseWebComponent extends HTMLElement{
    //Will store the ready promise, since we want to always return
    //the same
    #ready = null;

    constructor(){
        super();
    }

    //Must be overwritten in child class to create the dom, read/write attributes, etc.
    async init(){
        throw new Error('Must be implemented !');
    }

    //Will call the init method and await for it to resolve before resolving itself. 
    //Always return the same promise, so several part of the code can
    //call it safely
    async ready(){
        //We don't want to call init more that one time
        //and we want every call to ready() to return the same promise.
        if(this.#ready) return this.#ready
    
        this.#ready = new Promise(resolve => resolve(this.init()));
    
        return this.#ready;
    }

    connectedCallback(){
        //Will init the component automatically when attached to the DOM
        //Note that you can also call ready to init your component before
        //if you need to, every subsequent call will just resolve immediately.
        this.ready();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我创建一个新组件:

class MyComponent extends BaseWebComponent{
    async init(){
        this.setAttribute('something', '54');
        const div = document.createElement('div');
        div.innerText = 'Initialized !'; 
        this.appendChild(div);
    }

}

customElements.define('my-component', MyComponent);

/* somewhere in a javascript file/tag */

customElements.whenDefined('my-component').then(async () => {
    const component = document.createElement('my-component');
    
    //Optional : if you need it to be ready before doing something, let's go
    await component.ready();
    console.log("attribute value : ", component.getAttribute('something'));

    //otherwise, just append it
    document.body.appendChild(component);
});

Run Code Online (Sandbox Code Playgroud)

我不知道有什么方法可以在没有 shdowDOM 的情况下以符合规范的方式初始化组件,而这并不意味着自动调用方法。

您应该能够调用this.ready() constructor 不是调用connectedCallback,因为它是异步的,应该在函数开始填充组件document.createElement之前创建组件。init但它可能容易出错,并且无论如何您都必须等待该承诺解决才能执行需要初始化组件的代码。