上下文感知的 Web 组件比您想象的更简单

Avatar of John Rhea
John Rhea

DigitalOcean 为您的旅程每个阶段提供云产品。 立即开始使用 价值 200 美元的免费积分!

Web 组件的另一个我们尚未谈论过的方面是,每当 Web 组件被添加到页面或从页面中移除时,都会调用一个 JavaScript 函数。 这些生命周期回调可用于许多用途,包括使元素感知其上下文。

文章系列

Web 组件的四个生命周期回调

您可以使用 Web 组件 四个生命周期回调

  • connectedCallback:当自定义元素附加到元素时,此回调会触发。
  • disconnectedCallback:当元素从文档中移除时,此回调会触发。
  • adoptedCallback:当元素添加到新文档时,此回调会触发。
  • attributeChangedCallback:当属性发生更改、添加或移除时,此回调会触发,前提是该属性正在被观察。

让我们看看这些回调在实际中的应用。

我们的末日人物组件

Two renderings of the web component side-by-side, the left is a human, and the right is a zombie.

我们将从创建一个名为 <postapocalyptic-person> 的 Web 组件开始。 末日后的每个人都是人类或僵尸,我们根据应用于 <postapocalyptic-person> 组件父元素的类来识别他们的身份——无论是 .human 还是 .zombie。 我们不会对其进行任何花哨的操作(至少现在不会),但我们将添加一个 shadowRoot,我们可以使用它根据分类附加相应的图像。

customElements.define(
  "postapocalyptic-person",
  class extends HTMLElement {
    constructor() {
      super();
      const shadowRoot = this.attachShadow({ mode: "open" });
    }
}

我们的 HTML 代码如下所示

<div class="humans">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<div class="zombies">
  <postapocalyptic-person></postapocalyptic-person>
</div>

使用 connectedCallback 插入人物

<postapocalyptic-person> 加载到页面上时,会调用 connectedCallback() 函数。

connectedCallback() {
  let image = document.createElement("img");
  if (this.parentNode.classList.contains("humans")) {
    image.src = "https://assets.codepen.io/1804713/lady.png";
    this.shadowRoot.appendChild(image);
  } else if (this.parentNode.classList.contains("zombies")) {
    image.src = "https://assets.codepen.io/1804713/ladyz.png";
    this.shadowRoot.appendChild(image);
  }
}

这确保了当 <postapocalyptic-person> 是人类时,会输出人类的图像,当组件是僵尸时,会输出僵尸的图像。

使用 connectedCallback 时要小心。 它比您想象的运行频率更高,每当元素移动时都会触发,甚至可能在节点不再连接之后运行(令人困惑),这会导致性能成本很高。 您可以使用 this.isConnected 来判断元素是否已连接。

在添加人物时使用 connectedCallback() 计数

让我们稍微复杂一点,在其中添加几个按钮。 一个按钮将添加一个 <postapocalyptic-person>,使用“抛硬币”的方法来决定它是人类还是僵尸。 另一个按钮将做相反的操作,随机移除一个 <postapocalyptic-person>。 我们将在过程中跟踪有多少人类和僵尸在视野中。

<div class="btns">
  <button id="addbtn">Add Person</button>
  <button id="rmvbtn">Remove Person</button> 
  <span class="counts">
    Humans: <span id="human-count">0</span> 
    Zombies: <span id="zombie-count">0</span>
  </span>
</div>

以下是我们的按钮将执行的操作

let zombienest = document.querySelector(".zombies"),
  humancamp = document.querySelector(".humans");

document.getElementById("addbtn").addEventListener("click", function () {
  // Flips a "coin" and adds either a zombie or a human
  if (Math.random() > 0.5) {
    zombienest.appendChild(document.createElement("postapocalyptic-person"));
  } else {
    humancamp.appendChild(document.createElement("postapocalyptic-person"));
  }
});
document.getElementById("rmvbtn").addEventListener("click", function () {
  // Flips a "coin" and removes either a zombie or a human
  // A console message is logged if no more are available to remove.
  if (Math.random() > 0.5) {
    if (zombienest.lastElementChild) {
      zombienest.lastElementChild.remove();
    } else {
      console.log("No more zombies to remove");
    }
  } else {
    if (humancamp.lastElementChild) {
      humancamp.lastElementChild.remove();
    } else {
      console.log("No more humans to remove");
    }
  }
});

以下是 connectedCallback() 中的代码,用于在添加人物时计数人类和僵尸

connectedCallback() {
  let image = document.createElement("img");
  if (this.parentNode.classList.contains("humans")) {
    image.src = "https://assets.codepen.io/1804713/lady.png";
    this.shadowRoot.appendChild(image);
    // Get the existing human count.
    let humancount = document.getElementById("human-count");
    // Increment it
    humancount.innerHTML = parseInt(humancount.textContent) + 1;
  } else if (this.parentNode.classList.contains("zombies")) {
    image.src = "https://assets.codepen.io/1804713/ladyz.png";
    this.shadowRoot.appendChild(image);
    // Get the existing zombie count.
    let zombiecount = document.getElementById("zombie-count");
    // Increment it
    zombiecount.innerHTML = parseInt(zombiecount.textContent) + 1;
  }
}

使用 disconnectedCallback 更新计数

接下来,我们可以使用 disconnectedCallback() 在移除人类和僵尸时递减数量。 但是,我们无法检查父元素的类,因为在调用 disconnectedCallback 时,具有对应类的父元素已经消失了。 我们可以为元素设置一个属性,或为对象添加一个属性,但由于图像的 src 属性已经由其父元素确定,我们可以将其用作代理,以确定正在移除的 Web 组件是人类还是僵尸。

disconnectedCallback() {
  let image = this.shadowRoot.querySelector('img');
  // Test for the human image
  if (image.src == "https://assets.codepen.io/1804713/lady.png") {
    let humancount = document.getElementById("human-count");
    humancount.innerHTML = parseInt(humancount.textContent) - 1; // Decrement count
  // Test for the zombie image
  } else if (image.src == "https://assets.codepen.io/1804713/ladyz.png") {
    let zombiecount = document.getElementById("zombie-count");
    zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1; // Decrement count
  }
}

当心小丑!

现在(我在这里说的是经验之谈,当然),除了成群结队的僵尸向你的位置逼近外,唯一更可怕的东西就是一个小丑——只要一个就够了! 所以,即使我们已经处理了可怕的末日僵尸,我们也要增加小丑进入场景的可能性,使恐怖加倍。 事实上,我们将以一种方式实现这一点,即屏幕上的任何人类或僵尸都可能潜藏着小丑!

我收回我之前说过的话:一只僵尸小丑比一群“正常”的小丑还要可怕。 假设,如果发现任何类型的小丑——无论是人类还是僵尸——我们将通过将它们发送到完全不同的文档——一个 <iframe> 监狱,将它们与人类和僵尸群体隔离开来。 (我听说“扮小丑”可能比僵尸感染更具传染性。)

当我们将一个可疑的小丑从当前文档移动到 <iframe> 时,它不会销毁并重新创建原始节点; 相反,它会采用并连接该节点,首先调用 adoptedCallback,然后调用 connectedCallback

除了具有 .clowns 类的主体之外,我们不需要在 <iframe> 文档中添加任何内容。 只要这个文档位于主文档的 iframe 中——而不是单独查看——我们甚至不需要 <postapocalyptic-person> 实例化代码。 我们将为人类预留一个空间,为僵尸预留另一个空间,当然还有小丑的监狱…… 呃…… 的 <iframe>…… 乐趣。

<div class="btns">
  <button id="addbtn">Add Person</button>
  <button id="jailbtn">Jail Potential Clown</button>
</div>
<div class="humans">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<div class="zombies">
  <postapocalyptic-person></postapocalyptic-person>
</div>
<iframe class="clowniframeoffun” src="adoptedCallback-iframe.html">
</iframe>

我们的“添加人物”按钮与上一个示例中的工作原理相同:它抛硬币,随机插入人类或僵尸。 当我们点击“监禁潜在小丑”按钮时,会再抛一次硬币,将僵尸或人类送入 <iframe> 监狱。

document.getElementById("jailbtn").addEventListener("click", function () {
  if (Math.random() > 0.5) {
    let human = humancamp.querySelector('postapocalyptic-person');
    if (human) {
      clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(human));
    } else {
      console.log("No more potential clowns at the human camp");
    }
  } else {
    let zombie = zombienest.querySelector('postapocalyptic-person');
    if (zombie) {
      clowncollege.contentDocument.querySelector('body').appendChild(document.adoptNode(zombie));
    } else {
      console.log("No more potential clowns at the zombie nest");
    }
  }
});

使用 adoptedCallback 揭示小丑

adoptedCallback 中,我们将根据它们对应的图像来判断小丑是僵尸类型还是人类类型,然后相应地更改图像。 connectedCallback 将在那之后被调用,但我们没有任何需要它做的事情,而且它所做的事情不会干扰我们的更改。 因此,我们可以保持原样。

adoptedCallback() {
  let image = this.shadowRoot.querySelector("img");
  if (this.parentNode.dataset.type == "clowns") {
    if (image.src.indexOf("lady.png") != -1) { 
      // Sometimes, the full URL path including the domain is saved in `image.src`.
      // Using `indexOf` allows us to skip the unnecessary bits. 
      image.src = "ladyc.png";
      this.shadowRoot.appendChild(image);
    } else if (image.src.indexOf("ladyz.png") != -1) {
      image.src = "ladyzc.png";
      this.shadowRoot.appendChild(image);
    }
  }
}

使用 attributeChangedCallback 检测隐藏的小丑

最后,我们有 attributeChangedCallback。 与其他三个生命周期回调不同,我们需要观察 Web 组件的属性,以便回调触发。 我们可以通过在自定义元素的类中添加一个 observedAttributes() 函数来实现这一点,并让该函数返回一个属性名称数组。

static get observedAttributes() {
  return [“attribute-name”];
}

然后,如果该属性发生更改——包括添加或移除——attributeChangedCallback 会触发。

现在,你需要担心的是,你认识和爱的人(或者那些在变成僵尸之前你认识和爱的人)中,有些人可能偷偷地是伪装成人的小丑。我已经设置了一个小丑探测器,它会观察一群人类和僵尸,当你点击“揭示小丑”按钮时,探测器会(通过完全科学且完全可信的方式,**不是**基于随机数字选择索引)将 data-clown="true" 应用于组件。当应用了此属性时,attributeChangedCallback 会触发并更新组件的图像以揭示其小丑般的颜色。

我还应该注意到,attributeChangedCallback 接受三个参数

  • 属性的名称
  • 属性的先前值
  • 属性的新值

此外,回调允许你根据属性的变化程度,或根据两种状态之间的转换来进行更改。

以下是我们的 attributeChangedCallback 代码

attributeChangedCallback(name, oldValue, newValue) {
  let image = this.shadowRoot.querySelector("img");
  // Ensures that `data-clown` was the attribute that changed,
  // that its value is true, and that it had an image in its `shadowRoot`
  if (name="data-clown" && this.dataset.clown && image) {
    // Setting and updating the counts of humans, zombies,
    // and clowns on the page
    let clowncount = document.getElementById("clown-count"),
    humancount = document.getElementById("human-count"),
    zombiecount = document.getElementById("zombie-count");
    if (image.src.indexOf("lady.png") != -1) {
      image.src = "https://assets.codepen.io/1804713/ladyc.png";
      this.shadowRoot.appendChild(image);
      // Update counts
      clowncount.innerHTML = parseInt(clowncount.textContent) + 1;
      humancount.innerHTML = parseInt(humancount.textContent) - 1;
    } else if (image.src.indexOf("ladyz.png") != -1) {
      image.src = "https://assets.codepen.io/1804713/ladyzc.png";
      this.shadowRoot.appendChild(image);
      // Update counts
      clowncount.innerHTML = parseInt(clowncount.textContent) + 1;
      zombiecount.innerHTML = parseInt(zombiecount.textContent) - 1;
    }
  }
}

就是这样!我们不仅发现 Web 组件回调和创建上下文感知自定义元素比你想象的更容易,而且检测末日小丑,尽管可怕,但也比你想象的更容易。你可以使用这些 Web 组件回调函数来检测哪些狡猾的末日小丑?