前端笔记 11:使用Web Components进行原生组件化开发

Posted Naisu Xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端笔记 11:使用Web Components进行原生组件化开发相关的知识,希望对你有一定的参考价值。

目的

前端开发中组件化开发是一种趋势,方便界面中各种自定义组件的管理与复用。现在流行的 React 和 Vue 等框架都是组件化开发的。目前前端原生也支持一定程度上的组件化开发,这个称为Web Components,这篇文章将对相关内容做个说明。

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。

基础说明

原生的html标准中提供很多的组件,比如 p 、div 、button 这些,这些元素的默认样式和功能都朴素简单了,通常使用时我们需要根据功能需求再编写很多样式和脚本,大多数时候我们甚至需要将一些空间嵌套组合使用。这样的开发对于这些自己设计的组件的管理、维护、复用等工作都是比较麻烦的。

比较方便的一种方式是所有组件相关的html和css代码都封装到js中,这样每个组件就是一份js代码了,使用时只要调用相关函数将传教组件插入到DOM中就行。目前前端原生的组件化开发最根本的也是用js,再此基础上针对组件化需求添加了一些新特性。这整一个被称为Web Components。

Web Components主要有 四部分 三部分 组成:

  • Custom elements(自定义元素)
  • Shadow DOM(影子DOM)
  • HTML templates(HTML模板)
  • HTML Imports(HTML导入)
    前面几部分编写的代码放一个html文件中就成了一个单文件的组件,可以使用HTML Imports功能在正式页面中导入组件的html文件来使用组件;这个东西现在被废弃了,目前还没有非常好的代替这个功能的东西;

技术点介绍

Web Components 中涉及的一些技术可以在MDN上找到说明:
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

Custom elements

这东西就是组件化开发的核心了,用来创建自定义组件。这个其实很简答,创建一个继承自HTMLElement的类就行:

class YourComponent extends HTMLElement {
  constructor() {
    super();
    // 组件的功能代码写在这里
  }
}

接下来我们需要使用 CustomElementRegistry.define() 方法将上面的类和一个自定义的标签绑定,这样你就可以在页面中直接使用这里的标签来使用自定义的组件了:

customElements.define('your-component', YourComponent);
// 官方规定自定义标签必须大于一个单词,单词间用-连接

将两者结合一下就是下面的样子:

customElements.define('your-component',
  class extends HTMLElement {
    constructor() {
      super();
      // 组件的功能代码写在这里
    }
  }
);

知道上面两点内容后就可以创建自己的组件来使用了:

上图演示中用上了自定义的标签元素,这个内部元素内部其实就是在声明的类中用js代码创建的。上面演示中自定义组件内部的内容是在类中写死的,实际上我们希望属性是可以在使用时自由设置的。可以通过下面方式来自由设置属性:

除了上面的基础用法Custom elements还有一些生命周期相关的回调函数可用:

customElements.define('your-component',
  class extends HTMLElement {
    static get observedAttributes() { return ['要监听的属性列表'] } // 配合下面attributeChangedCallback()使用
    constructor() {
      super();
    }
    connectedCallback() {} // 当组件首次被插入文档DOM时被调用
    disconnectedCallback() {} // 当组件从文档DOM中删除时被调用
    adoptedCallback() {} // 当组件被移动到新的文档时被调用
    attributeChangedCallback(name, oldValue, newValue) {} // 当组件增加、删除、修改自身属性时被调用
  }
);

Shadow DOM

这东西主要用于让组件内的各个元素和DOM隔离,这样自定义的组件才能真正成为独立的组件,不会被页面中其它样式等的污染。

在上一节其实我们已经实现了自定义组件,但是这里还存在一个比较大的问题。上面的自定义组件内部的各种元素对外都是可见的,这会引起很多问题,比如内部的元素会被外部样式所修改。这时候就要用到Shadow DOM了:

上面的 attachShadow() 中mode参数可以设置为 closedopen ,区别在于页面中JS对shadow DOM内部元素的访问性:

HTML templates

前面内容中创建组件用的都是JS,但只用JS来创建组件的话组件的结构和样式稍微复杂点就变得很麻烦了,这个时候要用上HTML templates了,它可以让你用原生html、css、js语言来编写组件。HTML templates结合前面的内容使用时差不多是下面这样的结构:

  <template id='your-component-template'>
	<!-- 这里编写组件的结构、样式、脚本等 -->
  </template>
  <script>
    customElements.define('your-component',
      class extends HTMLElement {
        constructor() {
          super();
          let template = document.getElementById('your-component-template');
          let templateContent = template.content;
          const shadowRoot = this.attachShadow({ mode: 'closed' });
          shadowRoot.appendChild(templateContent.cloneNode(true)); // 克隆template内部内容添加到ShadowDOM中
        }
      }
    );
  </script>


上面的演示中可以看到有了HTML templates之后编写复杂的自定义组件就方便多了。另外template中的样式中可以使用 :host :host() :host-context(),比如下面这样:

:host {
  /* 选择Shadow DOM本身 */
}

:host-context(xxx) {
  /* 选择Shadow DOM父节点,这里通常就是页面中的自定义标签元素本身了 */
}

除了上面的基础使用外HTML templates还提供了一个更进一步的功能slot。这东西可以让你在templates的html结构中插入一个插槽,这样你在使用自定义组件的时候可以向这个插槽中插入各种各样的东西:


如果templates中只有一个slot,那就可以不用指定name,你在网页上使用自定义标签之间的所有内容都会放到这个slot间。

整合成独立组件文件

上面演示中组件和页面都是在同一个文件中的,实际使用中我们通常是希望组件可以封装在独立的文件中的。

在HTML Imports弃用之前我们可以把组件相关的代码(比如template、script)这些写到一个html文件中,然后在真实页面头部中使用 <link rel="import" href="your-component.html"> 方式引用组件。

在HTML Imports已经被废弃并且ES Module还无法import html文件的现在我们只能把template部分代码作为字符串嵌入到js代码中来使用了。最终组件就是一个独立的js文件,比如下面这样:

示例演示与说明

示例一

export default class YourComponent extends HTMLElement {

  static get observedAttributes() { return ['color'] }

  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
    <style>
    :host {
      color: ${this.color};
    }
    </style>
    <p id='text'><slot></slot></p>
    `;
  }

  get color() {
    return this.getAttribute('color') || 'blue'; // 如果没有设置颜色则返回bule作为默认颜色
  }

  set color(value) {
    this.setAttribute('color', value);
  }

  connectedCallback() {
    this.text = this.shadowRoot.getElementById('text');
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name == 'color' && this.text) {
      this.text.style.color = newValue;
    }
  }

}

if (!customElements.get('your-component')) {
  customElements.define('your-component', YourComponent);
}


上面是个简单的组件,仅仅只是控制了下文字的颜色,不过基本上展示出了原生单文件组件的一些用法,真正应用的时候更多的只是拓展组件结构,处理更多的属性。

上面演示中对于color这个属性,可以通过组件标签中添加属性来设置,也可以在页面上用CSS进行设置,另外虽然上面没有演示,其实也可以通过js使用 color(value) 或 setAttribute(name,value) 方法来设置。

示例二

export default class YourComponent extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
    <style>
    :host>.btn {
      width: 5rem;
      height: 2rem;
      line-height: 2rem;
      margin: 0.2rem;
      font-size: 1.25rem;
      text-align: center;
    }
    :host(:not([type="ok"]):not([type="error"]))>.btn {
      color: blue; 
    }
    :host([type="ok"])>.btn { 
      color: white; 
      background-color: green;
    }
    :host([type="error"])>.btn { 
      color: white; 
      background-color: red;
    }
    :host>.btn:hover {
      opacity: 0.6; 
    }
    :host>.btn:active {
      opacity: 0.2; 
    }
    :host-context(.round)>.btn {
      border-radius: 0.5rem;
    }
    </style>
    <button class='btn'><slot></slot></button>
    `;
  }
}

if (!customElements.get('your-component')) {
  customElements.define('your-component', YourComponent);
}


上面的示例主要使用了 :host() 和 :host-context() 这两个选择器,很多时候纯css也可以实现很多功能。

总结

Web Components 使用总体来说并不复杂。就我个人而言比起Vue、React这类需要编译的框架,我更加喜欢浏览器原生就能解析使用的开发方式。

更多内容可以参考MDN相关的例程:
https://github.com/mdn/web-components-examples

另外也可以参考第三方的原生组件库:
https://github.com/XboxYan/xy-ui

以上是关于前端笔记 11:使用Web Components进行原生组件化开发的主要内容,如果未能解决你的问题,请参考以下文章

Web Components系列 —— 概述

#yyds干货盘点# Web Components系列 ——自定义组件的生命周期

web前端学习笔记:JavaScript数组

web前端工程发展史阅读笔记

web前端笔记

不安分的 Go 语言开始入侵 Web 前端领域了