Building Components Custom Elements 自定义元素
Opened this issue · 0 comments
2019-09-15 01:42:32
首先来写一个示例吧
Web Components 标准非常重要的一个特性是,它使开发者能够将HTML页面的功能封装为 custom elements(自定义标签),而往常,开发者不得不写一大堆冗长、深层嵌套的标签来实现同样的页面功能。这篇文章将会介绍如何使用HTML的custom elements。 - MDN
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements
具体看MDN文档,(复制完应该就差不多了 然后去这里玩)
https://developers.google.com/web/fundamentals/web-components/
查看demo
<style for-run for-show>
.popup-info {
width: 100px;
height: 100px;
position: relative;
top: 100px;
left: 300px;
}
.popup-info1 {
width: 100px;
height: 100px;
position: relative;
top: 160px;
left: 600px;
}
.popup-info img {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
}
.popup-info .info {
font-size: 20px;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 4px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 110px;
left: 0px;
z-index: 3;
}
.popup-info:hover .info{
opacity: 1;
}
</style>
<template for-run for-show>
<popup-info role="popup" img="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=4038933139,466219061&fm=26&gp=0.jpg" data-text="I Learn Js 💗"></popup-info>
<button id="remove">remove</button>
<button id="move">move</button>
<button id="toggle">toggle attribute</button>
<popup-info1 role="popup" img="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=4038933139,466219061&fm=26&gp=0.jpg" data-text="I Learn Js 💗"></popup-info1>
<button is="popup-button">popup-button!</button>
</template>
<script for-run for-show>
class popupInfo extends HTMLElement {
constructor() {
super()
}
static get observedAttributes() {
return ['role']
}
// 生命周期钩子
connectedCallback() {
// 创建 div
let div = document.createElement('div')
div.setAttribute('role', 'popup-info')
// 创建 img
let img = document.createElement('img')
let src = this.getAttribute('img')
// 创建info信息
let info = document.createElement('span')
let text = this.getAttribute('data-text')
// 设置img内容
img.src = src
img.alt = text
// 设置info内容
info.classList.add('info')
info.innerText = text
// 添加class
div.classList.add('popup-info')
// 添加内容 img info
div.appendChild(img)
div.appendChild(info)
this.appendChild(div)
}
disconnectedCallback() {
console.log('我没有了呀')
}
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('当前的属性:', attrName)
console.log('当前的属性 old:', oldVal)
console.log('当前的属性 new:', newVal)
}
adoptedCallback() {
console.log('adoptedCallback 自定义元素被移入新的 document')
}
}
customElements.define('popup-info', popupInfo)
// 测试 自定义元素被移入新的 document
const testAdoptedCallback = ()=> {
// 创建 iframe
const createWindow = () => {
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe.contentWindow
}
let cw = createWindow()
// 创建 自定义元素
let cw1 = document.querySelector('popup-info')
// 创建的元素插入到 新创建的iframe
cw.document.body.appendChild(cw1)
}
const move = () => {
let move = document.querySelector('#move')
move.onclick = function() {
testAdoptedCallback()
}
}
move()
// 测试移除 popup-info
const remove = () => {
let remove = document.querySelector('#remove')
remove.onclick = function() {
document.querySelector('popup-info').remove()
}
}
remove()
// 测试属性被修改 toggle
const toggle = () => {
let i = 0
let toggle = document.querySelector('#toggle')
toggle.onclick = function() {
i++
document.querySelector('popup-info').setAttribute('role', `popup-info-change, ${i}`)
}
}
toggle()
class popupInfo1 extends popupInfo {
constructor() {
super()
}
connectedCallback() {
this.innerText = 'extends popupInfo'
}
}
customElements.define('popup-info1', popupInfo1)
class popupButton extends HTMLButtonElement {
constructor() {
super()
}
connectedCallback() {
this.innerText += ' extends'
}
}
customElements.define('popup-button', popupButton, {extends: 'button'})
</script>
定义新元素
class popupInfo extends HTMLElement {
constructor() {
super()
}
static get observedAttributes() {
return ['role']
}
// 生命周期钩子
connectedCallback() {
// 创建 div
let div = document.createElement('div')
div.setAttribute('role', 'popup-info')
// 创建 img
let img = document.createElement('img')
let src = this.getAttribute('img')
// 创建info信息
let info = document.createElement('span')
let text = this.getAttribute('data-text')
// 设置img内容
img.src = src
img.alt = text
// 设置info内容
info.classList.add('info')
info.innerText = text
// 添加class
div.classList.add('popup-info')
// 添加内容 img info
div.appendChild(img)
div.appendChild(info)
this.appendChild(div)
}
disconnectedCallback() {
console.log('我没有了呀')
}
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('当前的属性:', attrName)
console.log('当前的属性 old:', oldVal)
console.log('当前的属性 new:', newVal)
}
adoptedCallback() {
console.log('adoptedCallback 自定义元素被移入新的 document')
}
}
customElements.define('popup-info', popupInfo)
<popup-info role="popup" img="https://miro.medium.com/max/1440/1*LjR0UrFB2a__5h1DWqzstA.png" data-text="I Learn Js 💗"></popup-info>
定义一个自定义的元素 然后使用
扩展元素
扩展自定义元素
customElements 全局性用于定义自定义元素
customElements.define(),并使用 JavaScript class 扩展基础 HTMLElement
class popupInfo1 extends popupInfo {
constructor() {
super()
}
connectedCallback() {
this.innerText = 'extends popupInfo'
}
}
customElements.define('popup-info1', popupInfo1)
继承自定义元素好像效果有点不对(我还没找到为啥 希望大佬教教我)
有关创建自定义元素的规则
- 自定义元素的名称必须包含短横线 (-)。因此,、 和 等均为有效名称,而 和 <foo_bar> 则为无效名称....
- 您不能多次注册同一标记...
- 自定义元素不能自我封闭标签
教程里面更详细
和上面很相识, 扩展了自定义的元素
扩展原生 HTML 元素
class popupButton extends HTMLButtonElement {
constructor() {
super()
}
connectedCallback() {
this.innerText += ' extends'
}
}
customElements.define('popup-button', popupButton, {extends: 'button'})
要扩展元素,您需要创建继承自正确 DOM 接口的类定义
扩展原生元素时,对 define() 的调用会稍有不同。所需的第三个参数告知浏览器要扩展的标记。
这很有必要,因为许多 HTML 标记均使用同一 DOM 接口
例如 section address 和 em(以及其他)都使用 HTMLElement;q 和 blockquote 则使用 HTMLQuoteElement;等等。
指定 {extends: 'blockquote'} 可让浏览器知道您创建的是 blockquote 而不是 q。有关 HTML DOM 接口的完整列表,请参阅 HTML 规范。
class popupButton extends HTMLButtonElement {}
customElements.define('popup-button', popupButton)
自定义元素响应
自定义元素可以定义特殊生命周期钩子
名称 | 描述 |
---|---|
constructor | 创建或升级元素的一个实例。用于初始化状态、设置事件侦听器或创建 Shadow DOM... |
connectedCallback | 元素每次插入到 DOM 时都会调用... |
disconnectedCallback() | 元素每次从 DOM 中移除时都会调用... |
attributeChangedCallback(attrName, oldVal, newVal) | 属性添加、移除、更新或替换... |
adoptedCallback(attrName, oldVal, newVal) | 自定义元素被移入新的 document... |
https://developers.google.com/web/fundamentals/web-components/customelements#extendhtml
上面写得很清晰 比我这个容易理解
// 生命周期钩子
connectedCallback() {
// 创建 div
let div = document.createElement('div')
div.setAttribute('role', 'popup-info')
// 创建 img
let img = document.createElement('img')
let src = this.getAttribute('img')
// 创建info信息
let info = document.createElement('span')
let text = this.getAttribute('data-text')
// 设置img内容
img.src = src
img.alt = text
// 设置info内容
info.classList.add('info')
info.innerText = text
// 添加class
div.classList.add('popup-info')
// 添加内容 img info
div.appendChild(img)
div.appendChild(info)
this.appendChild(div)
}
dom创建时候 创建内容
...
disconnectedCallback() {
console.log('我没有了呀')
}
...
// 测试移除 popup-info
const remove = () => {
let remove = document.querySelector('#remove')
remove.onclick = function() {
document.querySelector('popup-info').remove()
}
}
remove()
dom 移除执行的方法 可以移除事件什么的
...
attributeChangedCallback(attrName, oldVal, newVal) {
console.log('当前的属性:', attrName)
console.log('当前的属性 old:', oldVal)
console.log('当前的属性 new:', newVal)
}
...
// 测试属性被修改 toggle
const toggle = () => {
let i = 0
let toggle = document.querySelector('#toggle')
toggle.onclick = function() {
i++
document.querySelector('popup-info').setAttribute('role', `popup-info-change, ${i}`)
}
}
toggle()
需要注意的是,如果需要在元素属性变化后,触发 attributeChangedCallback()回调函数,你必须监听这个属性。这可以通过定义observedAttributes() get函数来实现,observedAttributes()函数体内包含一个 return语句,返回一个数组,包含了需要监听的属性名称:
static get observedAttributes() {
return ['role']
}
监听属性变化 调用的方法 比如 role="popup" 什么的
...
adoptedCallback() {
console.log('adoptedCallback 自定义元素被移入新的 document')
}
...
// 测试 自定义元素被移入新的 document
const testAdoptedCallback = ()=> {
// 创建 iframe
const createWindow = () => {
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
return iframe.contentWindow
}
let cw = createWindow()
// 创建 自定义元素
let cw1 = document.querySelector('popup-info')
// 创建的元素插入到 新创建的iframe
cw.document.body.appendChild(cw1)
}
const move = () => {
let move = document.querySelector('#move')
move.onclick = function() {
testAdoptedCallback()
}
}
move()
自定义元素被移入新的 document
元素定义的内容
class popupInfo extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
this.innerHTML = "xxxxxx";
}
}
以新内容覆盖元素的子项并非一种好的做法,因为这样做会不符合设想。
添加元素定义内容的更好做法是使用 shadow DOM,下一篇文章会写
class popupInfoShadow extends HTMLElement {
constructor() {
super()
let shadowRoot = this.attachShadow({mode: 'open'})
shadowRoot.innerHTML = "<span>hello</span>"
}
}
customElements.define('popup-info-shadow', popupInfoShadow)
通过 template 创建元素
<info-template></info-template>
<template id="info-template">
<style>
p {
color: red;
}
</style>
<p>hello template</p>
</template>
<script>
class infoTemplate extends HTMLElement {
constructor(){
super()
let shadowRoot = this.attachShadow({mode: 'open'})
const t = document.querySelector('#info-template')
const instance = t.content.cloneNode(true)
shadowRoot.appendChild(instance)
}
}
customElements.define('info-template', infoTemplate)
- 我们在 HTML 中定义新的元素:info-template
- 元素的 Shadow DOM 使用 template 创建
- 由于是 Shadow DOM,元素的 DOM 局限于元素本地
- 由于是 Shadow DOM,元素的内部 CSS 作用域限于元素内
设置自定义元素样式
<style>
p {
...
}
</style>
:defined CSS 伪类 表示任何已定义的元素。这包括任何浏览器内置的标准元素以及已成功定义的自定义元素 (例如通过 CustomElementRegistry.define() 方法)。
https://developer.mozilla.org/zh-CN/docs/Web/CSS/:defined
app-drawer:not(:defined) {}
<style>
info-template {
color:red;
opacity: 0;
}
aa-bb:not(:defined) {
color:red;
opacity: 0;
}
</style>
<info-template></info-template>
<info-template>123</info-template>
<aa-bb>123</aa-bb>
定义之前会隐藏起来哦
其他详情
未知元素与未定义的自定义元素
HTML 使用起来非常宽松和灵活。例如,在页面上声明 randomtagthatdoesntexist,浏览器将非常乐意接受它。为什么非标准标记可以生效?答案在于 HTML 规范允许这样。规范没有定义的元素作为 HTMLUnknownElement 进行解析。
自定义元素则并非如此。如果在创建时使用有效的名称(包含“-”),则潜在的自定义元素将解析为 HTMLElement。 您可以在支持自定义元素的浏览器中核实这一点。打开控制台:Ctrl+Shift+J(或者在 Mac 上,Cmd+Opt+J)并粘贴下列代码行:
// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true
// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true
API 参考
全局性 customElements 定义了处理自定义元素的方法。
define(tagName, constructor, options)
在浏览器中定义新的自定义元素。
customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});
抽离出来可以更利于阅读
get(tagName)
let Drawer = customElements.get('app-drawer');
console.log(Drawer)
if (Drawer) {
let drawer = new Drawer();
}
let PopupInfo = customElements.get('popup-info');
console.log(PopupInfo)
if (PopupInfo) {
let popupInfo = new PopupInfo();
}
在给定有效自定义元素标记名称的情况下,返回元素的构造函数。
如果没有注册元素定义,则返回 undefined。
whenDefined(tagName)
customElements.whenDefined('popup-info').then(() => {
console.log('popup-info ready!');
});
customElements.whenDefined('app-drawer').then(() => {
console.log('app-drawer ready!');
}).catch(err => {
console.log('err', err)
})
如果定义了自定义元素,则返回可解析的 Promise。如果元素已定义,则立即得到解析。
如果标记名称并非有效自定义元素名称,则拒绝(好像也不会走catch)
历史记录和浏览器支持
如果您最近几年持续关注网络组件,您应知道 Chrome 36+ 实施的自定义元素 API 版本使用了 document.registerElement() 而不是 customElements.define()。
但前者是标准的弃用版本,称为 v0。customElements.define() 成为现行标准并逐步获得各大浏览器厂商的支持。这称为自定义元素 v1。
https://developers.google.com/web/fundamentals/web-components/customelements
浏览器支持
要检测自定义元素功能,检测是否存在 window.customElements:
const supportsCustomElementsV1 = 'customElements' in window;
Polyfill
https://www.npmjs.com/package/@webcomponents/custom-elements
注:无法对 :defined CSS 伪类执行 polyfill。 (没测试过)
const supportsCustomElementsV1 = 'customElements' in window;
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = src
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}
if (!supportsCustomElementsV1) {
loadScript('https://unpkg.com/@webcomponents/custom-elements')
console.log('use polyfill')
} else {
// Native support.Good to go.
console.log('native')
}
总结:
自定义元素提供了一种新工具,可让我们在浏览器中定义新 HTML 标记并创建可重用的组件。
它们与 Shadow DOM 和 template 等新平台原语结合使用,我们可开始实现更多的可能
- 创建和扩展可重复使用组件的跨浏览器(网络标准)
- 无需库或框架即可使用。原生 JS/HTML 威武!
- 提供熟悉的编程模型。仅需使用 DOM/CSS/HTML。
- 与其他网络平台功能良好匹配(Shadow DOM、template、CSS 自定义属性等)
- 与浏览器的 DevTools 紧密集成。
- 利用现有的无障碍功能。
- (我也是复制的 2333)
https://developers.google.com/web/fundamentals/web-components/customelements
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements