使用纯粹的JS构建 Web Component

简介: 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 类,但让我们先绕过这部分,先来讨论定制元素的生命周期方法。你可以使用下面的生命周期回调函数:

  • connectedCallback — 每当元素插入 DOM 时被触发。
  • disconnectedCallback — 每当元素从 DOM 中移除时被触发。
  • attributeChangedCallback — 当元素上的属性被添加、移除、更新或取代时被触发。

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 的小贴士和技巧。

组件的命名

  • 定制元素的名称必须包含一个短横线。所以 <my-tabs><my-amazing-website> 是合法的名称, 而<foo><foo_bar> 不行。
  • 在 HTML 添加新标签时需要确保向前兼容,不能重复注册同一个标签。
  • 定制元素标签不能是自闭合的,因为 HTML 只允许一部分元素可以自闭合。需要写成像 <app-drawer></app-drawer> 这样的闭合标签形式。

拓展组件

创建组件时可以使用继承的方式。举个例子,如果想要为两种不同的用户创建一个 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!

目录
相关文章
|
7天前
|
JavaScript 中间件 关系型数据库
构建高效的后端服务:Node.js 与 Express 的实践指南
在后端开发领域,Node.js 与 Express 的组合因其轻量级和高效性而广受欢迎。本文将深入探讨如何利用这一组合构建高性能的后端服务。我们将从 Node.js 的事件驱动和非阻塞 I/O 模型出发,解释其如何优化网络请求处理。接着,通过 Express 框架的简洁 API,展示如何快速搭建 RESTful API。文章还将涉及中间件的使用,以及如何结合 MySQL 数据库进行数据操作。最后,我们将讨论性能优化技巧,包括异步编程模式和缓存策略,以确保服务的稳定性和扩展性。
|
3天前
|
缓存 前端开发 JavaScript
构建高性能与用户体验并重的现代Web应用
构建高性能与用户体验并重的现代Web应用
15 5
|
4天前
|
监控 前端开发 JavaScript
探索微前端架构:构建可扩展的现代Web应用
【10月更文挑战第29天】本文探讨了微前端架构的核心概念、优势及实施策略,通过将大型前端应用拆分为多个独立的微应用,提高开发效率、增强可维护性,并支持灵活的技术选型。实际案例包括Spotify和Zalando的成功应用。
|
3天前
|
前端开发 JavaScript jenkins
构建高效、可维护的Web应用
构建高效、可维护的Web应用
15 2
|
9天前
|
前端开发 JavaScript API
前端框架新探索:Svelte在构建高性能Web应用中的优势
【10月更文挑战第26天】近年来,前端技术飞速发展,Svelte凭借独特的编译时优化和简洁的API设计,成为构建高性能Web应用的优选。本文介绍Svelte的特点和优势,包括编译而非虚拟DOM、组件化开发、状态管理及响应式更新机制,并通过示例代码展示其使用方法。
24 2
|
8天前
|
人工智能 搜索推荐 PHP
PHP在Web开发中的璀璨星辰:构建动态网站的幕后英雄###
【10月更文挑战第25天】 本文将带您穿越至PHP的宇宙,揭示其作为Web开发常青树的奥秘。通过生动实例与深入解析,展现PHP如何以简便、高效、灵活的姿态,赋能开发者打造动态交互式网站,同时不忘探讨其在新时代技术浪潮中面临的挑战与机遇,激发对技术创新与应用的无限思考。 ###
16 1
|
9天前
|
前端开发 JavaScript 开发者
构建响应式设计的现代Web应用:实用技巧与工具
【10月更文挑战第24天】本文介绍了构建响应式Web应用的实用技巧和工具,涵盖流体网格布局、弹性图片、CSS媒体查询、CSS Grid和Flexbox、响应式导航菜单、图片和字体的响应式处理,以及测试和调试工具。掌握这些技能将帮助开发者提升用户体验和项目适应性。
|
10天前
|
JSON API 数据格式
如何使用Python和Flask构建一个简单的RESTful API。Flask是一个轻量级的Web框架
本文介绍了如何使用Python和Flask构建一个简单的RESTful API。Flask是一个轻量级的Web框架,适合小型项目和微服务。文章从环境准备、创建基本Flask应用、定义资源和路由、请求和响应处理、错误处理等方面进行了详细说明,并提供了示例代码。通过这些步骤,读者可以快速上手构建自己的RESTful API。
22 2
|
10天前
|
缓存 前端开发 JavaScript
构建高效、可维护的Web应用
【10月更文挑战第23天】构建高效、可维护的Web应用
22 1
|
2天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。