【问题标题】:Failing to access setter method of a web-component无法访问 Web 组件的 setter 方法
【发布时间】:2021-11-18 10:55:11
【问题描述】:

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

这是一个有点复杂的问题,可能是我遗漏了一些基本的东西。这似乎与一个 Web 组件通过模板克隆使用另一个 Web 组件这一事实有关。有人建议 in this question 问题可能是由于未加载/定义子组件。我不明白这一点,特别是因为我无法让建议的解决方案发挥作用。我还假设正在运行的任何 JS 引擎浏览器都足够智能以解决 import 依赖项,并且如果其导入尚未准备好,则不会运行代码。我是不是过于简单化了?

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

  • component_a.js

    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 };

  • component_b.js

    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 };

  • question.js

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

  • question.html

    <!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 上,负载 &lt;comp-b&gt; 被创建并插入到页面中。它的 setter 方法被调用,参数为 10。在其构造函数 &lt;comp-a&gt; 中创建 &lt;comp-b&gt; 期间,通过提供的模板附加。一旦实例化,它就会被变量s 引用,并且应该调用它的setter 方法——产生A=20innerHTML。所以页面应该是:

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 将整个指定的CompA 与s 相关联,而不仅仅是Element? 请随时提出进一步可能的问题诊断建议?

【问题讨论】:

  • 嗨!请将代码缩减为 minimal reproducible example 来演示问题,最好是使用 Stack Snippets([&lt;&gt;] 工具栏按钮)的 runnable 代码; here's how to do one.
  • @T.J.在这里很难模拟导入,不是吗?
  • @connexo - 是的,很遗憾,但这无关紧要。问题不在于导入/导出。在这种情况下,minimal reproducible example sn-p 是完全可行的。
  • @T.J.Crowder kind of 是关于导入的,因为假设CompA 导入CompB 应该运行对customElements.define('comp-b', CompB) 的调用,所以基本上OP 不应该遇到任何问题。

标签: javascript web-component


【解决方案1】:

忘掉import 和whenDefined 的胡言乱语吧。

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

  1. template 升级异步
    当您将其附加到 shadowDOM 时,constructor 代码继续,
    并且模板 HTML(现在在 shadowDOM 中)在 the Event Loop 完成后升级。
  2. .getElementById 发现 HTMLUnknownElements
    HTMLUnknownElements 是 HTMLElements,因此你的 constructor.name 是 HTMLElement,而你是 can do anything you want with them。
    它们只是没有 升级 Web 组件还,
    正如您在 matches(":defined") 代码中找到的那样

是的,唉几乎每个博客都显示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 将被解析/升级同步(渲染阻塞)

不想使用innerHTML?使用 .createElement("div") 构建您的 HTML

.createElement("template") 和 &lt;template&gt; 升级为 A-sync。
那就了解how to delay your code (in the connectedCallback)

一般情况下

不在constructor 中更新DOM(我不是说shadowDOM!!),这项工作应该在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);
} 

您正在创建依赖项。
何时或在何处定义 Web 组件并不重要,就像乐高积木就是乐高积木一样。

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

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

})

(几乎)总是做这项工作。当您开始为自己的元素使用 BaseClasses 时,请使用 export。

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

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

【讨论】:

    【解决方案2】:

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

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

    另一个解决方案(这是我目前正在为客户开发的 Web 组件库中使用的)是为每个组件提供一个静态 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 中的属性进行排序即可。

    【讨论】:

      猜你喜欢
      • 2018-01-02
      • 2021-04-17
      • 2020-10-04
      • 1970-01-01
      • 2021-03-26
      • 1970-01-01
      • 2018-02-23
      • 1970-01-01
      • 2017-01-13
      相关资源
      最近更新 更多