使用纯粹的JS构建 Web Component

发表于:December 15, 2017 at 01:15 PM

Web Component 出现有一阵子了。 Google 费了很大力气去推动它更广泛的应用,但是除 Opera 和 Chrome 以外的多数主流浏览器对它的支持仍然不够理想。

原文链接:https://ayushgp.github.io/html-web-components-using-vanilla-js 译者:阿里云 - 也树

但是  通过 polyfill,你可以从现在开始构建你自己的 Web Component,你可以在这里找到相关支持:https://www.webcomponents.org/polyfills

在这篇文章中,我会演示如何创建带有样式,拥有交互功能并且在各自  文件中优雅组织的 HTML 标签。

介绍

Web Component 是一系列 web 平台的 API,它们可以允许你创建全新可定制、可重用并且  封装的 HTML 标签,从而在普通网页及 web 应用中使用。

定制的组件  基于 Web Component 标准构建,可以在现在浏览器上使用,也可以和任意与 HTML 交互的 JavaScript 库和框架配合使用。

 用于支持 Web Component 的特性正逐渐加入 HTML 和 DOM 的规范,web 开发者使用封装好样式和定制行为的新元素来拓展 HTML 会变得轻而易举。

它赋予了仅仅使用纯粹的 JS/HTML/CSS 就可以创建  可重用组件的能力。如果 HTML 不能满足需求,我们可以创建一个可以满足需求的 Web Component。

举个例子,你的用户数据和一个 ID 有关,你希望有一个可以填入用户 ID 并且可以获取相应数据的组件。HTML 可能是下面这个样子:

<user-card user-id="1"></user-card>

这是一个 Web Component 最基本的应用。下面的教程将会聚焦在如何构建这个用户卡片组件。

Web Component 的四个核心概念

HTML 和 DOM 标准定义了四种新的标准来帮助定义 Web Component。这些标准如下:

  1. 定制元素(Custom Elements): web 开发者可以通过定制元素创建新的 HTML 标签、增强已有的 HTML 标签或是二次开发其它开发者已经完成的组件。这个 API 是 Web Component 的基石。

  2. HTML 模板(HTML Templates): HTML 模板定义了新的元素,描述一个基于 DOM 标准用于客户端模板的途径。模板允许你声明标记片段,它们可以被解析为 HTML。这些片段在页面开始加载时不会被用到,之后运行时会被实例化。

  3. Shadow DOM: Shadow DOM 被设计为构建基于组件的应用的一个工具。它可以解决 web 开发的一些常见问题,比如允许你把组件的 DOM 和作用域隔离开,并且简化 CSS 等等。

  4. HTML 引用(HTML Imports): HTML 模板(HTML Templates)允许你创建新的模板,同样的,HTML 引用(HTML imports)允许你从不同的文件中引入这些模板。通过独立的 HTML 文件管理组件,可以帮助你更好的组织代码。

定义定制元素

我们首先需要声明一个类,定义元素如何表现。这个类需要继承 HTMLElement 类,但让我们先绕过这部分,先来讨论定制元素的生命周期方法。你可以使用下面的生命周期回调函数:

UserCard 文件夹下创建 UserCard.js:

class UserCard extends HTMLElement {
  constructor() {
    super();
    this.addEventListener("click", e => {
      this.toggleCard();
    });
  }

  toggleCard() {
    console.log("Element was clicked!");
  }
}

customElements.define("user-card", UserCard);

这个例子里我们已经创建了一个定义了定制元素行为的类。customElements.define('user-card', UserCard); 函数调用告知 DOM 我们已经创建了一个新的定制元素叫 user-card,它的行为被 UserCard 类定义。现在可以在我们的 HTML 里使用 user-card 元素了。

我们会用到 https://jsonplaceholder.typicode.com/ 的 API 来创建我们的用户卡片。下面是数据的样例:

{
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "Sincere@april.biz",
  address: {
    street: "Kulas Light",
    suite: "Apt. 556",
    city: "Gwenborough",
    zipcode: "92998-3874",
    geo: {
      lat: "-37.3159",
      lng: "81.1496"
    }
  },
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org"
}

创建模板

现在,让我们创建一个将在屏幕上渲染的模板。创建一个名为 UserCard.html 的新文件,内容如下:

<template id="user-card-template">
  <div>
    <h2><span></span> ( <span></span>)</h2>
    <p>Website: <a></a></p>
    <div>
      <p></p>
    </div>
    <button class="card__details-btn">More Details</button>
  </div>
</template>
<script src="/UserCard/UserCard.js"></script>

注意:我们在类名前加了一个 card__ 前缀。在较早版本的浏览器中,我们不能使用 shadow DOM 来隔离组件 DOM。这样当我们为组件编写样式时,可以避免意外的样式覆盖。

编写样式

我们创建好了卡片的模板,现在来用 CSS 装饰它。创建一个 UserCard.css 文件,内容如下:

.card__user-card-container {
  text-align: center;
  display: inline-block;
  border-radius: 5px;
  border: 1px solid grey;
  font-family: Helvetica;
  margin: 3px;
  width: 30%;
}

.card__user-card-container:hover {
  box-shadow: 3px 3px 3px;
}

.card__hidden-content {
  display: none;
}

.card__details-btn {
  background-color: #dedede;
  padding: 6px;
  margin-bottom: 8px;
}

现在,在 UserCard.html 文件的最前面引入这个 CSS 文件:

<link rel="stylesheet" href="/UserCard/UserCard.css" />

样式已经就绪,接下来可以继续完善我们组件的功能。

connectedCallback

 现在我们需要定义创建元素并且添加到 DOM 中会发生什么。注意这里 constructorconnectedCallback 方法的区别。

constructor 方法是元素被实例化时调用,而 connectedCallback 方法是每次元素插入 DOM 时被调用。connectedCallback 方法在执行初始化代码时是很有用的,比如获取数据或渲染。

 小贴士: 在 UserCard.js 的顶部,定义一个常量 currentDocument。它在被引入的 HTML 脚本中是必要的, 允许这些脚本有途径操作引入模板的 DOM。像下面这样定义:

const currentDocument = document.currentScript.ownerDocument;

接下来定义我们的 connectedCallback 方法:

// 元素插入 DOM 时调用
connectedCallback() {
  const shadowRoot = this.attachShadow({mode: 'open'});

  // 选取模板并且克隆它。最终将克隆后的节点添加到 shadowDOM 的根节点。
  // 当前文档需要被定义从而获取引入 HTML 的 DOM 权限。
  const template = currentDocument.querySelector('#user-card-template');
  const instance = template.content.cloneNode(true);
  shadowRoot.appendChild(instance);

  // 从元素中选取 user-id 属性
  // 注意我们要像这样指定卡片:
  // <user-card user-id="1"></user-card>
  const userId = this.getAttribute('user-id');

  // 根据 user ID 获取数据,并且使用返回的数据渲染
  fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then((response) => response.text())
      .then((responseText) => {
          this.render(JSON.parse(responseText));
      })
      .catch((error) => {
          console.error(error);
      });
}

渲染用户数据

我们已经定义好了 connectedCallback 方法,并且把克隆好的模板绑定到了 shadow root 上。现在我们需要填充模板内容,然后在 fetch 方法获取数据后触发 render 方法。下面来编写 rendertoggleCard 方法。

render(userData) {
  // 使用操作 DOM 的 API 来填充卡片的不同区域
  // 组件的所有元素都存在于 shadow dom 中,所以我们使用了 this.shadowRoot 这个属性来获取 DOM
  // DOM 只可以在这个子树种被查找到
  this.shadowRoot.querySelector('.card__full-name').innerHTML = userData.name;
  this.shadowRoot.querySelector('.card__user-name').innerHTML = userData.username;
  this.shadowRoot.querySelector('.card__website').innerHTML = userData.website;
  this.shadowRoot.querySelector('.card__address').innerHTML = `<h4>Address</h4>
    ${userData.address.suite}, <br />
    ${userData.address.street},<br />
    ${userData.address.city},<br />
    Zipcode: ${userData.address.zipcode}`
}

toggleCard() {
  let elem = this.shadowRoot.querySelector('.card__hidden-content');
  let btn = this.shadowRoot.querySelector('.card__details-btn');
  btn.innerHTML = elem.style.display == 'none' ? 'Less Details' : 'More Details';
  elem.style.display = elem.style.display == 'none' ? 'block' : 'none';
}

既然组件已经完成,我们就可以把它用在任意项目中了。为了继续教程,我们需要创建一个 index.html 文件,然后写入下面的代码:

<html>
  <head>
    <title>Web Component</title>
  </head>

  <body>
    <user-card user-id="1"></user-card>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.14/webcomponents-hi.js"></script>
    <link rel="import" href="./UserCard/UserCard.html" />
  </body>
</html>

因为并不是所有浏览器都支持 Web Component,我们需要引入 webcomponents.js 这个文件。注意我们用到 HTML 引用语句来引入我们的组件。

为了运行这些代码,你需要创建一个静态文件服务器。如果你不清楚如何创建,你可以使用像 static-server 或者 json-server 这样的简易静态服务。教程里,我们安装 static-server:

$ npm install -g static-server

接着在你的项目目录下,使用下面的命令运行服务器:

$ static-server

打开你的浏览器并访问localhost:3000,你就可以看到我们刚刚创建的组件了。

小贴士和技巧

还有很多关于 Web Component 的东西没有在这篇短文中写到,我想简单的陈述一些开发 Web Component 的小贴士和技巧。

组件的命名

拓展组件

创建组件时可以使用继承的方式。举个例子,如果想要为两种不同的用户创建一个 UserCard,你可以先创建一个基本的 UserCard 然后将它拓展为两种特定的用户卡片。想要了解更多组件继承的知识,可以查看Google web developers’ article

Lifecycle Callbacks 生命周期回调函数

我们创建了当元素加入 DOM 后自动触发的 connectedCallback 方法。我们同样有元素从 DOM 中移除时触发的 disconnectedCallback 方法。 attributesChangedCallback(attribute, oldval, newval)方法会在我们改变定制组件的属性时被触发。

组件元素是类的实例

既然组件元素是类的实例,就可以在这些类中定义公用方法。这些公用方法可以用来允许其它定制组件/脚本来和这些组件产生交互,而不是只能改变这些组件的属性。

定义私有方法

可以通过多种方式定义私有方法。我倾向于使用(立即执行函数),因为它们易写和易理解。举个例子,如果你创建的组件有非常复杂的内部功能,你可以像下面这样做:

(function() {

  // 使用第一个self参数来定义私有函数
  // 当调用这些函数时,从类中传递参数
  function _privateFunc(self, otherArgs) { ... }

  // 现在函数只可以在你的类的作用域中可用
  class MyComponent extends HTMLElement {
    ...

    // 定义下面这样的函数可以让你有途径和这个元素交互
    doSomething() {
      ...
      _privateFunc(this, args)
    }
    ...
  }

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

冻结类

为了防止新的属性被添加,需要冻结你的类。这样可以防止类的已有属性被移除,或者已有属性的可枚举、可配置或可写属性被改变,同样也可以防止原型被修改。你可以使用下面的方法:

class MyComponent extends HTMLElement { ... }
const FrozenMyComponent = Object.freeze(MyComponent);
customElements.define('my-component', FrozenMyComponent);

注意: 冻结类会阻止你在运行时添加补丁并且会让你的代码难以调试。

结论

这篇关于 Web Component 的教程作用非常有限。这可以部分归咎于对 Web Component 的影响很大的 React。我希望这篇文章可以提供给你足够的信息来让你尝试不添加任何依赖来构建自己的定制组件。你可以在 定制组件 API 规范(Custom components API spec) 找到更多关于 Web Component 的信息。

你可以在这里阅读第二部分的教程:使用纯粹的 JS 构建 Web Component - Part 2!