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 模板)等。

代码示例

<style type="text/css">
  .other {
    position: relative;
    width: 80%;
  }
  .define-css {
    height: 280px;
  }
</style>


<div class="other">
  <cus-scroll-horizontal>
    <div class="com-flex define-css">
      <div style="width: 200px; background-color: #0f0">111</div>
      <div style="width: 480px; background-color: #00f">111</div>
      <div style="width: 500px; background-color: #000">111</div>
      <div style="width: 100px; background-color: #0f0">111</div>
      <div style="width: 800px; background-color: #f00">111</div>
      <div style="width: 100px; background-color: #0f0">111</div>
    </div>
  </cus-scroll-horizontal>
</div>


<script type="text/javascript">
  // 注册组件键名
  var EL_KEY = "cus-scroll-horizontal";

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

        constructor() {
          super();

          console.log(this);

          this.shadow = this.attachShadow({ mode: "open" });

          // ⚠️ 全局变量
          this.initGlobal = this.initGlobal();

          this.renderUI();
        }

        initGlobal(options = {}) {
          // 移动端、非移动端
          this.IS_MOBILE_DEVICE =
            /(AppleWebKit.*Mobile.*)|android|avantgo|blackberry|bada\/|bb|meego|iphone|ipad|ipod|iemobile|kindle|midp|mmp|mobile|nokia|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|smartphone|s(ony)?ericsson|tizen|windows (phone|ce)|webos|xda|xiino/i.test(
              window.navigator.userAgent,
            );

          window.requestAnimationFrame =
            window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
          window.cancelAnimationFrame =
            window.cancelAnimationFrame || window.mozCancelAnimationFrame;

          this.scrollConf = {
            doing: false,
            animateRef: null, // 定时器
            prevTimestamp: 0, // 上一次定时数值
            timestamp: 0, // 当前定时数值
            timestampLimit: 300, // 计时上限数值

            viewLeftStartX: 0,
            viewLeftEndX: 0,

            barLeftX: 0, // 滚动条滑块 滚动距离
          };
          this.pointerConf = {
            prevViewLeftX: 0,
            start: false,
            end: false,

            startX: 0,
            endX: 0,
            moveX: 0,
          };
        }

        /**
         * 渲染样式
         */
        renderStyle() {
          var style = document.createElement("style");
          style.textContent = `
            .com-flex {
              display: -webkit-flex;
              display: flex;
            }
            .own-scroll-layout {
              overflow: hidden;
              position: relative;
              width: 100%;
            }
            .own-scroll-view-layout {
              position: relative;
              width: 100%;
              overflow-y: hidden;
              overflow-x: auto;
            }
            /** 隐藏滚动条 */
            .own-scroll-view-layout::-webkit-scrollbar {
              display: none; /** chrome safari */
            }
            .own-scroll-view-layout {
              scrollbar-width: none; /** firefox */
              -ms-overflow-style: none; /** IE 10+ */
            }
            .own-scroll-view-wrapper {
              position: relative;
              flex-wrap: nowrap;
              -webkit-flex-wrap: nowrap;
            }
            .own-scroll-view-content {
              position: relative;
              flex-wrap: nowrap;
              -webkit-flex-wrap: nowrap;
              flex-shrink: 0;
              flex-grow: 0;
              -webkit-flex-shrink: 0;
              -webkit-flex-grow: 0;
            }
            .own-scroll-view-content > * {
              position: relative;
              flex-shrink: 0;
              flex-grow: 0;
              -webkit-flex-shrink: 0;
              -webkit-flex-grow: 0;
            }

            .own-scroll-layout:not([data-scroll="false"]) {
              padding-bottom: 8px;
            }
            .own-scroll-bar-layout {
              display: none;
              position: absolute;
              right: 0;
              bottom: 0;
              left: 0;
              padding: 0 24px;
              height: 8px;
            }
            .own-scroll-layout:not([data-scroll="false"]) .own-scroll-bar-layout {
              display: block;
            }
            .own-scroll-bar-wrapper {
              position: relative;
              width: 100%;
              height: 100%;
              border-radius: 8px;
              background-color: #e5e5e5;
            }
            .own-scroll-bar-track {
              position: relative;
              width: 100%;
              height: 100%;
              border-radius: 8px;
            }

            .own-scroll-bar-thumb-wrapper {
              position: absolute;
              top: 0;
              left: 0;
              height: 100%;
              border-radius: 8px;
              background-color: #ff791b;
              cursor: pointer;
            }
            .own-scroll-bar-thumb {
              position: relative;
              height: 100%;
            }
          `;

          return style;
        }

        /**
         * 渲染UI
         */
        renderUI() {
          // 将模板插入Shadow DOM
          var templateHTML = `
            <!--滚动布局-->
            <div class="own-scroll-layout">
              <!--滚动区布局-->
              <div class="own-scroll-view-layout">
                <!--滚动区容器-->
                <div class="com-flex own-scroll-view-wrapper">
                  <!--滚动区内容-->
                  <div class="com-flex own-scroll-view-content">
                    <slot></slot>
                  </div>
                </div>
              </div>

              <div class="own-scroll-bar-layout">
                <!--滚动条容器-->
                <div class="own-scroll-bar-wrapper">
                  <!--滚动条轨迹-->
                  <div class="own-scroll-bar-track"></div>
                  <!--滚动条滑块容器-->
                  <div class="own-scroll-bar-thumb-wrapper">
                    <!--滚动条滑块-->
                    <div class="own-scroll-bar-thumb"></div>
                  </div>
                </div>
              </div>
            </div>
          `;

          this.shadow.innerHTML = templateHTML;

          var style = this.renderStyle();
          this.shadow.appendChild(style);
        }

        connectedCallback() {
          // 滚动布局
          this.layoutEl = this.shadow.querySelector(".own-scroll-layout");

          // 滚动区布局层
          this.viewLayout = this.shadow.querySelector(
            ".own-scroll-view-layout",
          );

          // 滚动区内容层
          this.viewContentEl = this.shadow.querySelector(
            ".own-scroll-view-content",
          );

          // 滚动条容器
          this.barWrapperEl = this.shadow.querySelector(
            ".own-scroll-bar-wrapper",
          );

          // 滚动条轨迹
          this.barTrackEl = this.shadow.querySelector(
            ".own-scroll-bar-track",
          );

          // 滚动条滑块容器
          this.barThumbWrapperEl = this.shadow.querySelector(
            ".own-scroll-bar-thumb-wrapper",
          );

          // 滚动条滑块
          this.barThumbEl = this.shadow.querySelector(
            ".own-scroll-bar-thumb",
          );

          this.initLaunch();
          this.initEvents();
        }

        initLaunch() {
          var { width: layoutW } = this.layoutEl.getBoundingClientRect();
          var { width: viewContentW } =
            this.viewContentEl.getBoundingClientRect();
          var { width: barTrackW } = this.barTrackEl.getBoundingClientRect();

          // 内容宽度<=容器宽度,隐藏滚动条
          if (viewContentW <= layoutW) {
            this.layoutEl.setAttribute("data-scroll", "false");
            return;
          }
          this.layoutEl.removeAttribute("data-scroll");

          // 设置 滚动条滑块宽度
          var barThumbW = barTrackW / (viewContentW / layoutW);
          this.barThumbEl.style.width = `${barThumbW}px`;

          // 滚动布局宽度
          this.layoutW = layoutW;
          // 滚动区内容宽度
          this.viewContentW = viewContentW;
          // 滚动区最大可滚动距离
          this.maxViewScrollLectX = this.viewContentW - this.layoutW;
          // 滚动条轨迹宽度
          this.barTrackW = barTrackW;
          // 滚动条滑块宽度
          this.barThumbW = barThumbW;
          // 滚动条最大可滑动距离
          this.barMaxLeftX = this.barTrackW - this.barThumbW;
        }

        initEvents() {
          // 内容滚动,同步滚动条位置
          this.scrollViewEvent = (e) => this.onContentScrollFunc(e);

          // 点击滚动条,缓动滚动
          this.clickBarTrackEvent = (e) => this.onClickBarTrackFunc(e);

          // 桌面端拖拽滚动条
          this.pointerDownEvent = (e) => this.onStartFunc(e.x);
          this.pointerMoveEvent = (e) => this.onMoveFunc(e.x);
          this.pointerUpEvent = (e) => this.onEndFunc(e.x);

          // 移动端触摸拖拽滚动条
          this.touchStartEvent = (e) =>
            this.onStartFunc(e.changedTouches[0].pageX);
          this.touchMoveEvent = (e) =>
            this.onMoveFunc(e.changedTouches[0].pageX);
          this.touchEndEvent = (e) =>
            this.onEndFunc(e.changedTouches[0].pageX);

          // 滚动区 滚动事件
          this.viewLayout.addEventListener(
            "scroll",
            this.scrollViewEvent,
            false,
          );

          // 滚动条滑块 点击事件
          this.barTrackEl.addEventListener(
            "click",
            this.clickBarTrackEvent,
            false,
          );

          if (this.IS_MOBILE_DEVICE) {
            this.barThumbWrapperEl.addEventListener(
              "touchstart",
              this.touchStartEvent,
              false,
            );
            document.body.addEventListener(
              "touchmove",
              this.touchMoveEvent,
              false,
            );
            window.addEventListener("touchend", this.touchEndEvent, false);
          } else {
            this.barThumbWrapperEl.addEventListener(
              "pointerdown",
              this.pointerDownEvent,
              false,
            );
            window.addEventListener(
              "pointermove",
              this.pointerMoveEvent,
              false,
            );
            window.addEventListener("pointerup", this.pointerUpEvent, false);
          }
        }

        // 内容滚动回调:同步滚动条位置
        onContentScrollFunc(e) {
          var { scrollLeft } = e.target;

          var barLeftX =
            (scrollLeft / this.maxViewScrollLectX) * this.barMaxLeftX;
          this.translateXFunc(this.barThumbWrapperEl, barLeftX);
        }

        // 点击滚动条回调:缓动滚动到对应位置
        onClickBarTrackFunc(e) {
          this.clearAnimationFunc(); // 清除之前的动画

          // 记录 当前时间戳
          this.scrollConf.prevTimestamp = performance.now();

          // 读取 最后一次 滚动区滚动距离
          var viewLeftStartX = this.viewLayout.scrollLeft;

          // 将 点击位置 在 滚动条轨迹 上的位置 映射到 滚动区的对应位置
          var contentW = (e.offsetX / this.barTrackW) * this.viewContentW;

          var viewLeftEndX;

          if (contentW > viewLeftStartX) {
            viewLeftEndX = contentW - this.layoutW;
          } else {
            viewLeftEndX = contentW;
          }

          var toLeft;

          // 滚动区向左 👈 移动
          if (viewLeftEndX > viewLeftStartX) {
            toLeft = true;
            this.scrollConf.viewLeftStartX = viewLeftStartX;
            this.scrollConf.viewLeftEndX = viewLeftEndX;
          }
          // 滚动区向右 👉 移动
          else {
            toLeft = false;
            this.scrollConf.viewLeftStartX = viewLeftEndX;
            this.scrollConf.viewLeftEndX = viewLeftStartX;
          }

          // 执行缓动动画
          var _self = this;
          function animateLoopFunc(now) {
            _self.scrollConf.timestamp = now - _self.scrollConf.prevTimestamp;

            if (
              _self.scrollConf.timestamp > _self.scrollConf.timestampLimit
            ) {
              _self.scrollConf.timestamp = _self.scrollConf.timestampLimit;
              _self.clearAnimationFunc();
            } else {
              _self.scrollConf.animateRef =
                window.requestAnimationFrame(animateLoopFunc);
            }

            // 线性缓动计算
            var value = _self.linearEasingUtil(
              _self.scrollConf.timestamp,
              _self.scrollConf.viewLeftStartX,
              _self.scrollConf.viewLeftEndX,
              _self.scrollConf.timestampLimit,
            );

            if (!toLeft) {
              value =
                _self.scrollConf.viewLeftEndX -
                value +
                _self.scrollConf.viewLeftStartX;
            }

            // 同步内容和滚动条位置
            _self.viewLayout.scrollLeft = value;
          }

          this.scrollConf.animateRef =
            window.requestAnimationFrame(animateLoopFunc);
        }

        // 拖拽/触摸开始
        onStartFunc(x) {
          this.pointerConf.prevViewLeftX = this.viewLayout.scrollLeft;
          this.pointerConf.start = true;
          this.pointerConf.startX = x;
        }

        // 拖拽/触摸移动
        onMoveFunc(x) {
          if (!this.pointerConf.start) return;

          this.pointerConf.moveX = x;

          var distanceX = this.pointerConf.moveX - this.pointerConf.startX;
          // 滚动条位移映射到内容位移
          var contentMoveW =
            (distanceX / (this.barTrackW - this.barThumbW)) *
            this.viewContentW;

          var moveLeftX = this.pointerConf.prevViewLeftX + contentMoveW;
          // 边界限制
          moveLeftX = Math.max(
            0,
            Math.min(moveLeftX, this.viewContentW - this.layoutW),
          );

          // 同步内容和滚动条位置
          this.viewLayout.scrollLeft = moveLeftX;
          this.scrollConf.barLeftX =
            (this.viewLayout.scrollLeft / this.viewContentW) * this.barTrackW;
          this.translateXFunc(
            this.barThumbWrapperEl,
            this.scrollConf.barLeftX,
          );
        }

        // 拖拽/触摸结束
        onEndFunc(x) {
          if (!this.pointerConf.start) return;

          this.pointerConf.endX = x;
          this.pointerConf.start = false;
        }

        // 平移变换工具方法
        translateXFunc(el, value) {
          el.style.transform = `translateX(${value}px)`;
          el.style.msTransform = `translateX(${value}px)`;
          el.style.webkitTransform = `translateX(${value}px)`;
          el.style.mozTransform = `translateX(${value}px)`;
        }

        // 线性缓动工具方法
        linearEasingUtil(t, b, c, d) {
          if (t <= 0) return b;
          if (t >= d) return c;
          var x = t / d;
          var delta = c - b;
          var factor = x;
          return b + factor * delta;
        }

        // 生命周期:组件从DOM移除后执行(销毁事件、清除动画,防止内存泄漏)
        disconnectedCallback() {
          this.unbindEvents();
          this.clearAnimationFunc();
        }

        // 解绑所有事件
        unbindEvents() {
          this.viewLayout.removeEventListener(
            "scroll",
            this.scrollViewEvent,
            false,
          );
          this.barTrackEl.removeEventListener(
            "click",
            this.clickBarTrackEvent,
            false,
          );

          // 移动端
          if (this.IS_MOBILE_DEVICE) {
            this.barThumbWrapperEl.removeEventListener(
              "touchstart",
              this.touchStartEvent,
              false,
            );
            document.body.removeEventListener(
              "touchmove",
              this.touchMoveEvent,
              false,
            );
            window.removeEventListener("touchend", this.touchEndEvent, false);
          } else {
            this.barThumbWrapperEl.removeEventListener(
              "pointerdown",
              this.pointerDownEvent,
              false,
            );
            window.removeEventListener(
              "pointermove",
              this.pointerMoveEvent,
              false,
            );
            window.removeEventListener(
              "pointerup",
              this.pointerUpEvent,
              false,
            );
          }
        }

        // 清除动画定时器
        clearAnimationFunc() {
          if (this.scrollConf.animateRef) {
            window.cancelAnimationFrame(this.scrollConf.animateRef);
            this.scrollConf.animateRef = null;
          }
        }

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

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

        // 将滚动区滚动位置 映射到 滚动条滑块 滚动位置
        viewLeftToBarThubmLeftUtil() {
          // 读取 滚动区滚动距离
          var scrollLeft = this.viewLayout.scrollLeft;
        }
      },
    );
  }
</script>

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