HTML & DOM 拾遗

google developer

web components

自定义元素(custom Elements):可重用的网页组件

TL;DR
自定义元素可以使开发者创建新的HTML 标记。现在有很多网页组件,我们可以使用或者拓展这些组件,使得我们的页面更加模块化。

自定义新元素

可以使用 customElements 来定义全局元素

// 定义一个移动抽屉面板 <app-drawer>
class AppDrawer extends HTMLElement {...}
window.customElments.define('app-drawer', AppDrawer);

// or use an anonymous class if you don't want a named constructor in current scope
window.customElements.define('app-drawer', class extends HTMLElement {...});

<app-drawer> </app-drawer>

自定义的元素与原生的元素没有区别,因为其扩展了 HTMLElement, 可以确保继承了完整的DOM API, 可以为其添加事件监听等。还可以自定义 JavaScript API

class AppDrawer extends HTMLElement {
	// A getter/setter for an open property
	get open() {
		return this.hasAttribute('open');
	}
	set open(val) {
		// Reflect the value of the open property as an HTML attribute
		if (val) {
			this.setAttribute('open', '');
		} else {
			this.removeAttribute('open')
		}
		this.toggleDrawer();
	}

	toggleDrawer() {
		...	
	}
}

这个例子中,我们定义了一个有 open 属性,toggleDrawer() 的 drawer. 这些属性将会成为 HTML 的属性。
自定义元素有一个非常牛的地方,那就是在定义内部的 this 指向这个 DOM 元素本身。这样的话就可以使用 this 来访问元素属性,查询节点(this.querySelectorAll(’.items’))等。
创建元素的规则:

  • 自定义元素的名称必须包含 - ,这样的话HTML解析器就可以分辨出哪些是自定义元素,这也确保了往HTML加入新元素时,能够向前兼容
  • 一个标签名只能注册一次。
  • 自定义元素不能是自闭合的,因为HTML仅仅允许特定的元素自闭合。
扩展元素

既可以拓展自定义元素,也可以扩展内置 HTML 元素。
扩展自定义元素

class FancyDrawer extends AppDrawer {
	constructor() {
		super(); // always call super() first in the constructor
	}
	toggleDrawer() {
		// super.toggleDrawer() or other implemention
	}

	antherMethod() {
		...
	}
}

扩展原生 HTML 元素

// <button> 自定义元素需要从 HTMLButtonElement 继承,<img> 需要从扩展 HTMLImageElement
class FancyButton extends HTMLButtonElement {
	constructor() {
		super();
		this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));	
	}
	// ripple animation
	drawRipple(x, y) {
		let div = document.creatElement('div');
		div.classList.add('ripple');
		this.appendChild(div);
		div.style.top = `${y - div.clientHeight/2}px`;
		div.style.left = `${x - div.clientWidth/2}px`;
		div.style.backgroudColor = 'currentColor';
		div.classList.add('run');
		div.adEventListener('transitionend', e => div.remove());
	}
}

customElment.define('fancy-button', FancyButton, {extends: 'button'});

扩展原生元素时,define() 的第三个参数要告诉浏览器所扩展的元素。因为存在多个原生元素使用同一DOM接口的情况,要告诉浏览器具体拓展的元素,浏览器才能确定。
扩展原生元素的用法:

<button is="fancy-button" disabled>Fancy button!</button>

// 使用js创建实例
let button = document.creatElement('button', {is: 'fancy-button'});
button.textContext = 'Fancy button';
button.disabled = true;
document.body.appendChild(button);

// use new
let button = new FancyButton();
button.textContext = 'Fancy button';
button.disabled = true;
自定义元素响应

自定义元素可以为不同的生命周期定义钩子函数,称为自定义元素响应。

名称调用时机
constructor创建或升级元素的一个实例。用于初始化状态,设置时间监听器或者创建shadow DOM
connectedCallback元素每次插入到DOM时都会调用。用于运行setup code,例如获取资源或者进行渲染
disconnectedCallback元素每次从DOM中移除时都会调用。用于运行清理代码(移除事件监听)
attributeChangedCallback(attrName, oldVal, newVal)属性添加,移除,更新或替换时调用
adoptedCallback()兹定于元素被移入新的document时

浏览器对加入 observedAttributes 白名单队列的属性都会执行 attributeChangedCallback(). 事实上,这里需要进行性能优化,因为对于一些通用属性(style,class)的更改,不应该出现大量的回调。

回调的响应是同步的,当触发事件执行时,将会立即触发回调函数。

属性(properties)和特性(attributes)

将属性映射为特性:
HTML属性通常会将其值与HTML特性的形式映射回DOM。

div.id = 'my-id';
div.hidden = true;
// ----
<div id="my-id" hidden>

映射属性可以让元素的 DOM 状态与其JavaScript状态保持同步,另外还可以将用户自定义的样式在js状态变更时应用。

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}
// ---
...

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}
// ---
<app-drawer open disabled></app-drawer>

保留对属性的更改:
通过 attributeChangedCallback 来对属性的更改做出响应。对于 observedAttributes 数组中的列出的每一个属性更改,浏览器都将调用此方法。

class AppDrawer extends HTMLElement {
  ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {...}

  set disabled(val) {...}

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}
元素升级

自定义元素可以在定义注册 之前使用。因为浏览器会对未知标记的元素采用不同的处理方式。调用 define() 并将类定义赋予现有元素的过程称为“元素升级”。
可以使用 window.cutomElements.whenDefined() 来了解元素合适被定义。他提供可在元素获得定义时进行解析的 Promise

customElements.whenDefined('app-drawer').then(() => {
	consoel.log('app-frawer defined');
});
元素定义的内容
customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  ...
});

// ----声明以上自定义标签,会生成以下内容
<x-foo-with-markup>
 <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

创建使用影子DOM 的元素:
在自定义元素中使用 shadow DOM,可以在 constrctor 中调用 this.attachShadow

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <style>:host { ... }</style> <!-- look ma, scoped styles -->
      <b>I'm in shadow dom!</b>
      <slot></slot>
    `;
  }
  ...
});
<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  <b>I'm in shadow dom!</b>
  <slot></slot>
</x-foo-shadowdom>

通过 创建元素

<template id="x-foo-from-template">
  <style>
    p { color: orange; }
  </style>
  <p>I'm in Shadow DOM.My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      const t = document.querySelector('#x-foo-from-template');
      const instance = t.content.cloneNode(true);
      shadowRoot.appendChild(instance);
    }
    ...
  });
</script>

以上代码实现的功能:

  • 在HTML中定义新的元素:<x-foo-from-template>
  • 元素的shadow DOM 使用 templete 创建
  • 由于是 shadow DOM,元素的DOM局限于本地元素
  • 由于是 shadow DOM,元素的内部CSS作用域限于元素内。

shadow DOM:独立的网络组件

TL;DR
shadow DOM 解决了构建网络应用的脆弱性问题。脆弱性是由HTML、CSS和 JS 的全局性引起的。近年来,已经有很多工具去解决这个问题,比如现在使用HTML 的 id / class 的时候,如果页面中已经存在同名的id,就不会再报错。
Shadow DOM 修复了 CSS 和DOM。它在网络平台中引入作用域样式。无需工具或命名约定,即可使用JavaScript捆绑 CSS,隐藏实现的细节以及编写独立的组件。
四大网页组件:HTML 模板、shadow DOM、自定义元素、HTML导入。
shadow DOM 旨在构建基于组件的应用,因此适用于以下情况:

  • 隔离DOM:组件的DOM是独立的(querySelector() 不会反悔组件中 shadow DOM中的节点)
  • 作用域CSS:shadow DOM内部定义的CSS在其作用域内。样式规则不会泄露影响整体页面,页面样式也不会渗入。
  • 组合:委组建设计一个声明性、基于标记的API
  • 简化CSS:作用域DOM意味着可以使用简单的CSS选择器,更通用的 id/class 名称,而不用担心命名冲突。
  • 效率:将应用看成多个DOM块,而不是一个大的(全局)页面。
什么是shadow DOM?

shadow DOM 与普通DOM的两点区别:

  • 创建、使用的方式
  • 与页面其他部分相关的行为方式。

借助于shadow DOM ,可以创建作用域DOM书,DOM树附加在某个元素上,但其又与这个节点的子节点不同。这一作用域子树成为影子树。被附着的元素称为影子宿主

创建shadow DOM
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'// 也可以使用 appendChild()

并不是所有元素都能托管shadow DOM的,比如 <input> 就不能。

为自定义元素创建 shadow DOM:
使用 shadow DOM来分隔元素的HTML、CSS和JS,从而生成一个 web component

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
      <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! --> 
      <div id="tabs">...</div>
      <div id="panels">...</div>
    `;// sytle 所定义的样式,将仅限于这个元素内
  }
  ...
});

在使用时,<fancy-tabs></fancy-tabs> 中的子元素将不会进行渲染,因为元素的shadow DOM 代替其子项被渲染。如果要渲染子项,我们要告诉浏览器在哪里进行渲染,也就是在shadow DOM中添加<slot> 元素。

组件(composition)和slot

composition是shadow DOM 最难理解的内容。
一些术语:

  • Light DOM:组件的用户编写的标记。该DOM不在组件 shadow DOM内,它是元素实际的子项。
  • shadow DOM:该DOM由组件的作者编写,它定义内部结构、作用域CSS并封装实现。
  • 扁平的DOM树:浏览器将用户的 light DOM 分布到您的 shadow DOM 的结果,并进行最终渲染。扁平树就是最终看到的树以及在页面上渲染的对象。
<button is="better-button">
  <!-- the image and span are better-button's light DOM -->
  <img src="gear.svg" slot="icon">
  <span>Settings</span>
</button>

#shadow-root
  <style>...</style>
  <slot name="icon"></slot>
  <span id="wrapper">
    <slot>Button</slot>
  </span>
# 扁平树
<button is="better-button">
  #shadow-root
    <style>...</style>
    <slot name="icon">
      <img src="gear.svg" slot="icon">
    </slot>
    <slot>
      <span>Settings</span>
    </slot>
</button>

<slot> 元素
shadow DOM 使用 <slot> 元素将不同的 DOM 树组合在一起。Slot是组件内部的占位符,用户可以使用自己的标记来填充。
通过定义 slot,就可以将外部标记引入到组件的shadow DOM 中进行渲染。就相当于再说“在这里渲染用户的标记”。
可以为 slot 命名,这样用户就可以通过特定名称来引用插槽

#shadow-root
  <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
  </div>
  <div id="panels">
    <slot id="panelsSlot"></slot>
  </div>
# light DOM
<fancy-tabs>
  <button slot="title">Title</button>
  <button slot="title" selected>Title 2</button>
  <button slot="title">Title 3</button>
  <section>content panel 1</section>
  <section>content panel 2</section>
  <section>content panel 3</section>
</fancy-tabs>
# 扁平DOM树  用户自定义内容被渲染进相应的 slot 内。
<fancy-tabs>
  #shadow-root
    <div id="tabs">
      <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
      </slot>
    </div>
    <div id="panels">
      <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
      </slot>
    </div>
</fancy-tabs>
设定样式

shadow DOM 最有用的功能就是 作用域CSS

  • 外部页面的CSS选择器不会应用于组件内部
  • 内部定义的样式也不会渗出,仅限于宿主元素。
高级主题

创建闭合影子节点(不推荐)
创建闭合影子树后,在 JavaScript 外部无法访问组件的内部 DOM。这与 <video> 等原生元素工作方式类似。

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

不推荐使用{mode: ‘closed’} 的原因:

  • 人为的安全功能。没有什么能够阻止攻击者入侵 Element.prototype.attachShadow
  • 闭合模式阻止自定义元素代码访问其自己的 shadow DOM。 这根本没用。相反,如果您想要使用如 querySelector() 等元素,您必须存放影子根以备之后参考。 这就与闭合模式的最初目的完全背道而驰!
customElements.define('x-element', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this._shadowRoot = this.attachShadow({mode: 'closed'});
    this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
  }
  connectedCallback() {
    // When creating closed shadow trees, you'll need to stash the shadow root
    // for later if you want to use it again. Kinda pointless.
    const wrapper = this._shadowRoot.querySelector('.wrapper');
  }
  ...
});
  • 闭合模式使组件对最终用户的灵活性大为降低。

在JS中使用slot
slotchange 事件:当slot的分布式节点发生变化时,slotchange 事件会触发。

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
  console.log('light dom children changed!');
});

当组件的实例首次初始化时,slotchange 不触发。

如要监控 light DOM 其他类型的变化,您可以在元素的构造函数中设置 MutationObserver
调用slot.assignedNodes()可查看 slot 正在渲染哪些元素, {flatten: true}选项将返回 slot 的备用内容

<slot><b>fallback content</b></slot>
用法调用结果
<button is=“better-button”>My button</button>slot.assignedNodes();[text]
<button is=“better-button”></button>slot.assignedNodes();[]
<button is=“better-button”></button>slot.assignedNodes({flatten: true});[<b>fallback content</b>]

element.assignedSlot将告诉得到元素分配给哪个组件 slot。

shadow DOM 事件模型
当事件从shadow DOM中触发时,将会被调整为看起来像是来自整个组件,而不是来自shadow DOM中的内部元素。
有些事件甚至不能从shadow DOM中传播出去。
能够跨越sshadow DOM 边界的事件有:

  • 聚焦事件:blur、focus、focusin、focusout
  • 鼠标事件:click、dblclick、mousedown、mouseenter、mousemove,等等

使用自定义事件
通过影子树中内部节点触发的自定义 DOM 事件不会超出影子边界,除非事件是使用 composed: true标记创建的:

// Inside <fancy-tab> custom element class definition:
selectTab() {
  const tabs = this.shadowRoot.querySelector('#tabs');
  tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}
// 如果是 composed: false(默认值),用户无法侦听到影子根之外的事件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值