使用纯粹的JS构建 Web Component

更新日期: 2017-12-19阅读: 2.9k标签: Component
原文链接:https://ayushgp.github.io/htm...
译者:阿里云 - 也树

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

但是通过 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 中会发生什么。注意这里 constructor 和 connectedCallback 方法的区别。

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 方法。下面来编写 render 和 toggleCard 方法。

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!

链接: https://www.fly63.com/article/detial/258

javascript字符串进行编码的方法:escape编码、encodeURI编码、encodeURIComponent编码

js对字符串进行编码的方法。ECMAScript v3 反对使用escape方法,用 encodeURI() 和 encodeURIComponent() 替代它。encodeURI对URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的。

ReactJS Components: 基础指南

创建和管理React组件的各种方式,涌现的大量状态管理工具等等都是这些挑战的焦点。我们今天能做的就是在React(基于社区选择)中将最常用的做法引入桌面并讨论它们。

从VantComponent 谈小程序维护

在开发小程序的时候,我们总是期望用以往的技术规范和语法特点来书写当前的小程序,所以才会有各色的小程序框架,例如 mpvue、taro 等这些编译型框架

react如何通过shouldComponentUpdate来减少重复渲染

在react开发中,经常会遇到组件重复渲染的问题,父组件一个state的变化,就会导致以该组件的所有子组件都重写render,尽管绝大多数子组件的props没有变化

react中的element、component、instance的理解

在React中无论是class形式(render函数)还是function形式(return的内容)的组件,最后返回的jsx其实质是React.createElement函数的结果,而React.createElement函数返回的结果是一个对象树,我们可以称之为元素描述树

React中PureComponent 和 Component区别

我们来看一看 Component 和 PureComponent 的区别,我们先从问题出发,通过解决实际的问题来查询出 PureComponent 和 Component 之间区别。这里创建 Greeting 的组件,其中我们用 setInterval 每间隔 2 秒就更新状态title一次

如何评价 Vue 的 Function-based Component?

react 的不可变,纯函数。直接导致 hooks 必须使用 const 关键字,不能是 let,这也是 hooks 的奇迹之一;Hooks对Fiber更好 -> Hooks是Fiber的产物 -> 没有Fiber就不是Hooks

React Server Component 可能并没有那么香

前段时间 React 团队发布了一项用于解决 React 页面在多接口请求下的性能问题的解决方案 React Server Components。当然该方案目前还在草案阶段,官方也只是发了视频和一个示例 demo 来说明这个草案。

Vue.js 中使用defineAsyncComponent 延迟加载组件

使用 Vue 3 的 defineAsyncComponent 特性可以让我们延迟加载组件。这意味着它们仅在需要时从服务器加载。这是改善初始页面加载的好方法,因为我们的应用程序将以较小的块加载,而不必在页面加载时加载每个组件。

Web Components中引入外部CSS的8种方法

开发中,还是会遇到需要引入外部CSS到Shadow DOM情况,那么如何处理呢?作者就最近遇到的情况给出如下几种方案。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!