无法访问 Web 组件的 setter 方法

Failing to access setter method of a web-component

我构建了一个 Web 组件,并在其构造函数中使用模板来初始化其子结构。此子结构的一部分是另一个 Web 组件,我希望调用其 setter 方法。我通过查询通过模板创建的 DOM 树来获取子组件,但是通过这个只能访问标准 Element 属性。

这是一个有点复杂的问题,可能是我遗漏了一些基本的东西。这似乎与一个 Web 组件通过模板克隆使用另一个 Web 组件这一事实有关。有人建议 问题可能是由于子组件不是 loaded/defined。我不明白这一点,尤其是因为我无法使建议的解决方案起作用。我还假设 运行ning 的任何 JS 引擎浏览器都足够智能,可以解决 import 依赖关系,并且如果其导入未准备好,则不会 运行 代码。我是不是把这个想得太简单了?

确定性失败的简单可重现示例是必不可少的。所以我设法创建了一个简单的副本来演示这个问题。为了保持一致性,我使用了与原始设计相同的多文件结构:


    class CompA extends HTMLElement
    {
        constructor()
        {
            super();
            this.attachShadow({mode: "open"});
          this.shadowRoot.append(CompA.template.content.cloneNode(true));
    
                this._value = 0;
          this.shadowRoot.getElementById('top').innerHTML = "A=" + this._value;
        }
    
        set value(x)
      {
        this._value = 2*x;
        this.shadowRoot.getElementById('top').innerHTML = "A=" + this._value;
        console.log('Value set on CompA');
      }
    }
    
    CompA.template = document.createElement("template");
    CompA.template.innerHTML = `<div id='top'></div>`;
    
    customElements.define("comp-a", CompA);
    
    export { CompA };


    import { CompA } from "./component_a.js"
    
    class CompB extends HTMLElement
    {
        constructor()
        {
            super();
            this.attachShadow({mode: "open"});
          this.shadowRoot.append(CompB.template.content.cloneNode(true));
    
          let s = this.shadowRoot.getElementById('subcomponent');
          console.log(s.constructor.name);
          console.log(s.matches(':defined'));
          s.value = 1;
        }
    
        set value(x)
      {
        this.shadowRoot.getElementById('subcomponent').value = x;
        console.log('Value set on CompB');
      }
    }
    
    CompB.template = document.createElement("template");
    CompB.template.innerHTML = `<div>
      <span>Component B:</span>
      <comp-a id='subcomponent'></comp-a>
    </div>`;
    
    customElements.define("comp-b", CompB);
    
    export { CompB };


    import { CompB } from "./component_b.js"
    
    window.onload = (event) =>
    {
      let x = document.createElement('comp-b');
      document.body.append(x);
      x.value = 10;
    }


    <!doctype html>
    <html lang=en>
    <head>
        <meta charset=utf-8>
        <title>question</title>
      <script type='module' src='./question.js'></script>
    </head>
    <body>
    </body>
    </html>

预期行为: 在 question.html 加载 <comp-b> 被创建并插入到页面上。它的 setter 方法使用参数 10 调用。在其构造函数中创建 <comp-b> 期间,<comp-a> 通过提供的模板附加。一旦实例化,它就会被变量 s 引用,并且应该调用它的 setter 方法 - 生成 A=20 innerHTML。所以页面应该是:

Component B:
A=20

具有预期的控制台输出:

CompB
true
Value set on CompA
Value set on CompA
Value set on CompB

观察到的行为: 变量 s 确实指向了正确的元素,但是 s.value = 1; 没有调用 CompA 的 setter 而是简单地将值 1 的 属性 分配给元素,从而保持其默认值。页面是:

Component B:
A=0

控制台输出:

HTMLElement
false
Value set on CompB

问题: 有人能解释一下为什么会失败以及如何强制 JS 将整个指定的 CompAs 相关联,而不仅仅是 Element 吗? 请随时提出进一步可能的问题诊断建议?

像这样包装对子组件 API 的任何访问,假设 s 持有对您的 <comp-a> DOM 节点的引用:

customElements.whenDefined('comp-a').then(() => s.value = 1);

另一个解决方案(这是我在我目前为客户开发的网络组件库中使用的)是给每个组件一个静态的 getter TAG_NAME 像这样:

export class CompA extends HTMLElement {
    static get TAG_NAME() { return 'comp-a'; }
}

并从组件文件中删除对 customElements.define 的调用。

然后,创建一个名为 components.js:

的文件
export { CompA } from './comp-a/comp-a.js';
export { CompB } from './comp-b/comp-b.js';

const componentsToRegister = { 
    CompA, 
    CompB,
}

for (const clazz of Object.values(componentsToRegister)) {
   customElements.define(clazz.TAG_NAME, clazz);
} 

这允许非常明确地控制哪些组件以什么顺序注册,只需对 componentsToRegister 中的属性进行排序即可。

忘记 importwhenDefined 胡言乱语。

您的问题有 2 个根本原因:

  1. template升级a-sync
    当你将它附加到影子DOM时,constructor代码继续
    一旦 the Event Loop 完成,模板 HTML(现在处于影子DOM)升级
  2. .getElementById 找到 HTMLUnknownElements
    HTMLUnknownElementsHTMLElements,因此您的 constructor.name 表示 HTMLElement,而您 can do anything you want with them.
    它们只是不是升级 Web组件,
    正如您在 matches(":defined") 代码
  3. 中找到的那样

而且,是的,几乎每个博客都显示 createElement("template") 模式。
你也不需要这个庞然大物:

CompB.template = document.createElement("template");
CompB.template.innerHTML = `<div>
  <span>Component B:</span>
  <comp-a id='subcomponent'></comp-a>
</div>`;

解决方案

让你的constructor做到:

super()
  .attachShadow({mode: "open"})
  .innerHTML = `<div>
                   <span>Component B:</span>
                   <comp-a id='subcomponent'></comp-a>
                </div>`;

并且您的 HTML 将是 parsed/upgraded 同步的 (渲染阻塞)

不想使用 innerHTML?使用 .createElement("div")

构建您的 HTML

.createElement("template")<template>升级为A-sync。
然后理解

一般来说

做DOM(我不是说shadowDOM!!) constructor,该工作应该在 connectedCallback 完成。有些情况下 constructor 中没有 DOM(想想 SSR 和 .createElement("my-component")


PS。我不喜欢这种模式:

export { CompA } from './comp-a/comp-a.js';
export { CompB } from './comp-b/comp-b.js';

const componentsToRegister = { 
    CompA, 
    CompB,
}

for (const clazz of Object.values(componentsToRegister)) {
   customElements.define(clazz.TAG_NAME, clazz);
} 

您正在创建依赖项。
WhenWhere Web Component 的定义应该无关紧要,就像乐高积木就是乐高积木一样。

“导出”一个 class 被高估了。

customElements.define("my-element", class extends HTMLElement{

})

(几乎)总能完成任务。当您开始为自己的元素使用 BaseClasses 时,请使用 export

您不需要(总是)导出 class,您可以 steal someone else Components

<script>
customElements.define( "poker-card", 
  class extends customElements.get("card-t") {})
</script>