【问题标题】: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 将整个指定的CompAs 相关联,而不仅仅是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】:

忘掉importwhenDefined 的胡言乱语吧。

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

  1. template 升级异步
    当您将其附加到 shadowDOM 时,constructor 代码继续
    并且模板 HTML(现在在 shadowDOM 中)在 the Event Loop 完成后升级
  2. .getElementById 发现 HTMLUnknownElements
    HTMLUnknownElementsHTMLElements,因此你的 constructor.nameHTMLElement,而你是 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
      相关资源
      最近更新 更多