Web Component技术

严重警告

在 Chrome 版本 76.0.3809.132(正式版本)(64 位)中测试发现,customElements.define()必须在 js 文件中调用,且引用此 js 文件时必须在script标签上添加defer属性,否则this.getAttribute('属性名称')无法获取到值。

基本概况

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。其中,Web Components由三项主要技术组成,分别为Custom elements(自定义元素)、Shadow DOM(影子 DOM)、HTML templates(HTML 模板)等。

Web Components特征

  • 封装性:将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。
  • Shadow DOM 接口:将一个隐藏的、独立的 DOM 附加到一个元素上。

独立自定义元素

继承自 HTML 元素基类 HTMLElement。

示例一:不内嵌元素

<cusElement></cusElement>


<script type="text/javascript">
class cusElement extends HTMLElement {
  // 必须是一个包含元素需要变更通知的所有属性名称的数组
  static observedAttributes = ["color", "size"];

  constructor() {
    super();

    // 注意:一旦创建了这个对象,则自定义元素的容器大小就失效,需要重新设置大小
    this.shadow = this.attachShadow({ mode: 'closed' });

    this.init();
  }

  init() {
    // 无效,在constructor构造函数中调用无效
    const color = this.getAttribute("color");
    console.log("颜色: ", color);
  }

  // 每当元素添加到文档中时调用
  connectedCallback() {
    console.log("自定义元素添加至页面。");

    // 获取 自定义元素 外的其他元素
    console.log(document.querySelector("#cusElementId"));
    console.log(document.querySelector("body"));
    console.log(document.querySelector('[id^="GalleryStatus"]'));


    /**
     * 特别注意
     * 这种通过this.querySelector获取自定义元素内部的元素,只能通过<script src="./temp.js" defer></script>外部引入脚本的方式,才能生效。
     */
    console.log(this.querySelector("h1"));


    // 获取属性
    const color = this.getAttribute("color");
    console.log("颜色: ", color);

    window.setTimeout(() => {
      // 设置属性
      this.setAttribute("color", "#0ff");
    });
  }

  // 每当元素从文档中移除时调用
  disconnectedCallback() {
    console.log("自定义元素从页面中移除。");
  }

  // 每当元素被移动到新文档中时调用
  adoptedCallback() {
    console.log("自定义元素移动至新页面。");
  }

  // 在属性更改、添加、移除或替换时调用
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(
      `属性 ${name} 的旧值: ${oldValue} 已经出现更新,新值: ${newValue}`
    );
  }
}

const EL_KEY = "cus-element";
if (!customElements.get(EL_KEY)) {
  customElements.define(EL_KEY, cusElement);
}
</script>

示例二:内嵌元素

<cus-element id="cusElementId" color="#333" size="10">
  <h1>Web Component</h1>
  <p>
    Web Component
    是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的
    web 应用中使用它们。
  </p>
  <h3>独立自定义元素</h3>
  <p>继承自 HTML 元素基类 HTMLElement。</p>
</cus-element>


<script type="text/javascript">
class cusElement extends HTMLElement {
  // 必须是一个包含元素需要变更通知的所有属性名称的数组
  static observedAttributes = ["color", "size"];

  constructor() {
    super();

    this.init();
  }

  init() {
    // 无效,在constructor构造函数中调用无效
    const color = this.getAttribute("color");
    console.log("颜色: ", color);
  }

  // 每当元素添加到文档中时调用
  connectedCallback() {
    console.log("自定义元素添加至页面。");

    // 获取属性
    const color = this.getAttribute("color");
    console.log("颜色: ", color);

    window.setTimeout(() => {
      // 设置属性
      this.setAttribute("color", "#0ff");
    });
  }

  // 每当元素从文档中移除时调用
  disconnectedCallback() {
    console.log("自定义元素从页面中移除。");
  }

  // 每当元素被移动到新文档中时调用
  adoptedCallback() {
    console.log("自定义元素移动至新页面。");
  }

  // 在属性更改、添加、移除或替换时调用
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(
      `属性 ${name} 的旧值: ${oldValue} 已经出现更新,新值: ${newValue}`
    );
  }
}

const EL_KEY = "cus-element";
customElements.define(EL_KEY, cusElement);
</script>

实现案例

<cusElement></cusElement>

<script>
(function (global, factory) {
  'use strict';

  factory();
}(this, function () {
  'use strict';

  /**
   * 公共函数
   */
  function appendHTML(parentEl, html) { var divTemp = document.createElement("div"); var nodes = null; var fragment = document.createDocumentFragment(); divTemp.innerHTML = html; nodes = divTemp.childNodes; for (var i = 0; i < nodes.length; i++) { fragment.appendChild(nodes[i].cloneNode(true)) } parentEl.appendChild(fragment); nodes = null; fragment = null } function trim(string) { return (string || "").replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, "") } function hasClass(el, cls) { if (!el || !cls) return false; if (cls.indexOf(' ') !== -1) { throw new Error('className should not contain space.'); } if (el.classList) { return el.classList.contains(cls) } if (el.className == '') { return false } return el.className.indexOf(cls) != -1 } function addClass(el, cls) { if (!el) return; var curClass = el.className; var classes = (cls || '').split(' '); for (var i = 0, j = classes.length; i < j; i++) { var clsName = classes[i]; if (!clsName) continue; if (el.classList) { el.classList.add(clsName) } else { if (!hasClass(el, clsName)) { curClass += ' ' + clsName } } } if (!el.classList) { el.className = curClass } }; function removeClass(el, cls) { if (!el || !cls) return; var classes = cls.split(' '); var curClass = ' ' + el.className + ' '; for (var i = 0, j = classes.length; i < j; i++) { var clsName = classes[i]; if (!clsName) continue; if (el.classList) { el.classList.remove(clsName) } else { if (hasClass(el, clsName)) { curClass = curClass.replace(' ' + clsName + ' ', ' ') } } } if (!el.classList) { el.className = trim(curClass) } }; function toggleClass(el, cls) { if (!el || !cls) return; if (hasClass(el, cls)) { removeClass(el, cls); return } addClass(el, cls) }

  /**
   * 公共请求参数
   */
  const REQUEST_PARAMS = {
    p_channel: "",
    p_module: "module"
  };

  // 生产环境
  const PROD_BASE_URL = "https://xxx.xxx.com/ares/v1/";
  // 测试环境
  const DEVT_BASE_URL = "https://dev-xxx.xxx.com/ares/v1/";

  const COMMON_DATA = {
    baseURL: ""
  };






  // 注册组件键名
  const EL_KEY = "cus-el-carousel-display";

  if (!customElements.get(EL_KEY)) {
    customElements.define(EL_KEY, 
      class cusElement extends HTMLElement {
        static observedAttributes = [];

        constructor() {
          super();

          // shadow DOM 设置'closed',即不可以从外部获取 Shadow DOM
          // 注意,创建了该元素,就不可以在该组件内嵌任何HTML片段,会无效
          this.shadow = this.attachShadow({mode: 'closed'});

          this.renderStyle();
          this.init();
        } 

        /**
         * 渲染样式
         */
        renderStyle() {
          const style = document.createElement('style');
          style.textContent = `
            * {-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}
            * {-webkit-box-sizing: border-box;box-sizing: border-box;}
            .${EL_KEY} {}
          `;

          this.shadow.appendChild(style);
        }

        init(options = {}) {
          this.initGlobal();
          
          this.renderUI();
          this.activateEvent();
          this.resizeEvent();
        }

        initGlobal(options = {}) {
          const debugAtrr = options.debug || this.getAttribute('debug');
          const channelAttr = options.channel || this.getAttribute('channel');

          // 调试模式
          this.debug = debugAtrr === "true" ? true : false;
          this.channel = channelAttr || "";
        }

        /**
         * 渲染UI
         */
        renderUI() {
          const mainUI = `
            <div class="${EL_KEY} ${this.debug ? 'debug' : ''}">
            </div>
          `;

          // 清空旧内容
          const mainEl = this.shadow.querySelector(`.${EL_KEY}`);
          mainEl?.remove();

          // 追加新内容
          appendHTML(this.shadow, mainUI);
        }

        /**
         * 激活事件
         */
        activateEvent() {
          this.closeModel();
        }

        // 关闭事件
        closeModel() {
          const closeEl = this.shadow.querySelector("div");
          console.info('closeEl: ', closeEl);
        }

        /**
         * 窗口尺寸变化
         */
        resizeEvent() {
          const _this = this;

          window.onresize = function () {
            _this.renderUI();
            _this.activateEvent();
          }
        }

        connectedCallback() {
          this.setAttribute("style", "position: relative; display: block; width: 100%; height: 100%");
        }

        /**
         * 监听外部传递属性变化
         */
        attributeChangedCallback(name, oldValue, newValue) {
          const value = newValue || oldValue;

          if (name === "") {
            //
          }
        }
      }
    );
  }








  /**
   * 业务系统
   */
  function initModuleName(options = {}) {
    const {
      // 渠道类型
      channel = "",
      // 调试模式
      debug = false,
      // 环境,默认为测试环境,"development"-测试环境 "production"-生产环境
      env = "development"
    } = options || {};

    const baseURL = env === "production" ? PROD_BASE_URL : DEVT_BASE_URL;

    if (!channel) throw new Error("channel参数必填,不能为空");

    REQUEST_PARAMS.p_channel = channel;

    // 启动程序
  }


  return { initModuleName };
}));
</script>

自定义内置元素

继承自标准的 HTML 元素,例如 HTMLImageElement 或 HTMLParagraphElement。它们的实现定义了标准元素的行为。

<div is="cus-element"></div>

<script>
class cusElement extends HTMLElement {
  constructor() {
    super();
  }
}
  

if (!customElements.get("cus-element")) {
  customElements.define('cus-element', cusElement, {
    extends: "div"
  });
}
</script>

基本语法

Custom elements(自定义元素)

一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。

customElements.get

指定名字的自定义元素的构造函数,如果没有使用该名称的自定义元素定义,则为undefined。

const EL_KEY = "cus-element";

if (!customElements.get(EL_KEY)) {
  customElements.define(EL_KEY, cusElement);
}

可以被挂载的 shadow DOM 元素

  • 任何带有有效的名称且可独立存在的(autonomous)自定义元素。
  • <article><aside><blockquote><body><div><footer><h1><h2><h3><h4><h5><h6><header><main><nav><p><section><span>

注意事项

不是每一种类型的元素都可以附加到 shadow root(影子根)下面。出于安全考虑,一些元素不能使用 shadow DOM(例如<a>),以及许多其他的元素。

Shadow DOM(影子 DOM)

Shadow DOM 接口,可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。

HTML templates(HTML 模板)

<template><slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

  • 是一种用于保存客户端内容机制,该内容在加载页面时不会呈现,但随后可以 (原文为 may be) 在运行时使用 JavaScript 实例化。
  • 解析器在加载页面时确实会处理<template>元素的内容,但这样做只是为了确保这些内容有效;但元素内容不会被渲染。
  • <slot>元素是 Web 组件内的一个占位符。该占位符可以在后期使用自己的标记语言填充,这样您就可以创建单独的 DOM 树,并将它与其它的组件组合在一起。使用槽 (slots) 可以添加灵活度。
  • templates模板中style样式只给它添加到一个标准的 DOM 中是不起作用,而是在模板内部添加style才能生效。

实现web component基本方法

  1. 创建一个类或函数来指定 web 组件的功能。
  2. 使用 CustomElementRegistry.define() 方法注册您的新自定义元素 ,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
  3. 如果需要的话,使用Element.attachShadow() 方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。
  4. 如果需要的话,使用 <template><slot> 定义一个 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到您的 shadow DOM 中。
  5. 在页面任何您喜欢的位置使用自定义元素,就像使用常规 HTML 元素那样。

自定义标签与Shadow DOM

<cus-element></cus-element>

customElements.define

customElements.define(
  name, // 表示所创建的元素名称的符合 DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线。
  ComponentName, // 用于定义元素行为的 类 。
  options // 一个包含 extends 属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
);

正常使用<template>模板

<template id="template">
  <style>
    .template-wrapper {
      position: relative;
    }
  </style>
  <div class="template-wrapper">内容模板元素</div>
  <slot name="slotcontent"></slot>
</template>

<script type="text/javascript">
  const template = document.getElementById('template');
  const templateContent = template.content;
  document.body.appendChild(templateContent);
</script>

在 Web Components 中使用模板

<template id="template">
  <style>
    .template-wrapper {
      position: relative;
    }
  </style>
  <div class="template-wrapper">内容模板元素</div>
  <slot name="slotcontent"></slot>
</template>

<cus-element>
  <div slot='slotcontent'>使用槽 (slots) 添加灵活度</div>
</cus-element>


<script type="text/javascript">
  class cusElement extends HTMLElement {
    constructor() {
      super();

      this.init();
    }

    init() {
      // shadow DOM 设置'closed',即不可以从外部获取 Shadow DOM
      const shadow = this.attachShadow({mode: 'closed'});

      const template = document.getElementById('template');
      const templateContent = template.content;

      // 使用Node.cloneNode() 方法添加了模板的拷贝到阴影的根结点上。
      shadow.appendChild(templateContent.cloneNode(true));
    }
  }

  customElements.define('cus-element', cusElement);  
</script>

生命周期回调函数

connectedCallback

当 custom element 首次被插入文档 DOM 时,被调用。

if (!customElements.get("cus-element")) {
  customElements.define(
    'cus-element', 
    class cusElement extends HTMLElement {
      constructor() {
        super();

        this.init();
      }

      init() {
        //
      }

      connectedCallback() {
        console.log("自定义元素添加至页面。");
      }
    }
  );
}

disconnectedCallback

当 custom element 从文档 DOM 中删除时,被调用。

if (!customElements.get("cus-element")) {
  customElements.define(
    'cus-element', 
    class cusElement extends HTMLElement {
      constructor() {
        super();

        this.init();
      }

      init() {
        //
      }

      disconnectedCallback() {
        console.log("自定义元素从页面中移除。");
      }
    }
  );
}

adoptedCallback

当 custom element 被移动到新的文档时,被调用。

if (!customElements.get("cus-element")) {
  customElements.define(
    'cus-element', 
    class cusElement extends HTMLElement {
      constructor() {
        super();

        this.init();
      }

      init() {
        //
      }
  
      adoptedCallback() {
        console.log("自定义元素移动至新页面。");
      }
    }
  );
}

attributeChangedCallback

当 custom element 增加、删除、修改自身属性时,被调用。

配置步骤

  • 步骤一:将需要变更通知的所有属性名称赋值给一个名为 observedAttributes 的静态属性。
  • 步骤二:attributeChangedCallback() 生命周期回调的实现。

代码示例

if (!customElements.get("cus-element")) {
  customElements.define(
    'cus-element', 
    class cusElement extends HTMLElement {
      static observedAttributes = ["name", "title"];

      constructor() {
        super();

        this.init();
      }

      init() {
        //
      }

      attributeChangedCallback(name, oldValue, newValue) {
        const value = newValue || oldValue;
        
        console.log(`属性 ${name} 已变更。`, oldValue, newValue);
      }
    }
  );
}

通信实现机制

CustomEvent自定义事件

// 发送数据
const event = new CustomEvent("ysun-turntable-event", {
  detail: {
    type: "render-ui",
    data: data
  },
  bubbles: true,
  composed: true
});

document.dispatchEvent(event);


// 监听外部事件
document.addEventListener(
  "ysun-turntable-event",
  function (e) {
    const { type, data } = e.detail || {};

    // 渲染UI
    if (type === "render-ui") {
      const { list } = data || {}; 
    }

    // 旋转
    if (type === "start-rorate") {
      //
    }
  },
  false
);

postMessage


事件(Event)处理机制

indicatorEvent() {
  const _this = this;
  const indicatorEls = this.shadow.querySelectorAll(".carousel-indicator-item");
  const indicatorElsList = Array.from(indicatorEls);
  for (let i = 0; i < indicatorElsList.length; i++) {
    const el = indicatorElsList[i];

    el?.addEventListener("click", function (e) {
      _this.indicatorModel(i);
    }, false);
  }
}

Omi

Omi 是腾讯开源的前端跨框架跨平台的框架。

Stencil

Stencil 是用于生成 Web Components 的编译器

Lit

Spectrum

Web Components 库:Spectrum

Spectrum Web Components 具有以下特点:

默认支持无障碍访问:开发时考虑到现有和新兴浏览器规范,以支持辅助技术。 轻量级:使用 Lit Element 实现,开销最小。 基于标准:基于 Web Components 标准,如自定义元素和 Shadow DOM 构建。 框架无关:由于浏览器级别的支持,可以与任何框架一起使用。

Slim.js

Slim.js 是一个开源的轻量级 Web Components 库,它为组件提供数据绑定和扩展能力,使用 es6 原生类继承。

Polymer

Polymer 是 Google 推出的 Web Components 库

hybrids

hybrids 是一个 JavaScript UI 框架,用于创建功能齐全的 Web 应用程序、组件库或具有独特的混合声明性和功能性架构的单个 Web Components。

direflow

direflow 是一个 React组件 + web component +web componen t属性变化重新挂载 React 组件的 web component框架。

LitElement

LitElement 是一个简单的基类,用于使用 lit-html 创建快速、轻量级的 Web Components。

X-Tag

X-Tag 是微软推出的开源库,支持 Web Components 规范,兼容Web Components。

Last Updated:
Contributors: 709992523