关于es6:ES6-Module-自定义元素-初体验

2次阅读

共计 17777 个字符,预计需要花费 45 分钟才能阅读完成。

可能是最全最新的“初体验”。
欸,一位敌人和我说,作者啊,你写这么多,指不定也没人看,一点意思都没有,还不如省下这点工夫坐下来 喝 杯 奶 茶,对吧。我气不过啊,收回来,就想看看有多少人,看了这篇文章,完事了还能给我点个 ????,气一气我这炫富的敌人。

新一代 ECMAScript 和 Web Components 规范曾经倒退得十分全面,古代浏览器反对也非常宽泛。纯原生代码也能够写出 Vue 单文件组件的相似成果。

<game-character data-name="El Primo" data-image="//example.dev/el-primo.png"></game-character>  
import GameCharacter from '//example.dev/xxx.js';  
GameCharacter.register('game-character');  

Module + Components Example – CodePen(肯定要点开哦!)

元素,是组件的身躯

HTML 虽是构建网页的根底技术,但不可否认地,其 原生标签数和可扩展性相当无限 。开发者 总总 不足一种 主动将 JS 与 HTML 相关联 的形式。

这所有继续到 自定义元素 的呈现。自定义元素(MDN)是 HTML 现代化过程中的里程碑,它将指定 HTML 元素与 ES Class 相绑定,加强了 Web 开发构造与性能的分割,是古代仿生学思维(人们钻研生物体的构造与性能工作的原理,发明先进技术)在前端畛域的重要冲破,使得易于复用的 HTML 元素的创立和扩大分外简略。总总 认同,通过原生自定义元素创立组件 简略间接,合乎直觉,代码量小

标准的命名

为了区别原生元素、帮忙浏览器解析,HTML 标准明确而具体地制订了自定义元素的名称应遵循的规定:

  • 自定义元素的名称必须蕴含连字符(-)。
  • 以字母结尾。
  • 不蕴含大写字母。
  • 不能是以下中的一个:

    • <annotation-xml>
    • <color-profile><font-face><font-face-src><font-face-uri><font-face-format><font-face-name><missing-glyph>

标准以 <math-α><emotion-????> 为例,只有满足上述要求,名称都能够应用

另外,一个合乎命名要求的自定义元素,即便未被注册,也 不会 被视为 HTMLUnknownElement 的实例。

// 'game-character' 合乎命名规范。document.createElement('game-character') instanceof HTMLUnknownElement  
// false  
  
// 'character' 不合乎命名规范。document.createElement('character') instanceof HTMLUnknownElement  
// true  

简略清晰的关系定义

自定义元素的卖点,根本就在自定义类。以本例为例,所有的 <game-character> 都将会是 GameCharacter 的实例。

class GameCharacter extends HTMLElement {// constructor 不继承且为空、或继承但只有一个 super() 时,能够疏忽。constructor() {super();  
  }  
}  

自定义元素与类的关系,能够应用 customElements.define(MDN)搭建,这个 API 同时还会实现该元素在 DOM 中的 注册

customElements.define('game-character', GameCharacter);  

自定义元素实现了注册流程,new 操作符就把握了发明该自定义元素实例的能力。

// 注册后,const ele = document.createElement('game-character');  
// 性能上等价于:const ele = new GameCharacter();  

可复用的构造

Shadow DOM(MDN)是 HTML 和 CSS 特异性的原生解决方案,书写 部分作用域的元素 id 和 CSS 款式 由此解脱了对第三方框架的依赖。它是 Web Components 的要害一环;与自定义元素和 ES6 Module 一起,组成了古代 Web 开发模块化的原生“硬件”根底。

创立 Shadow DOM 非常容易,在选定的元素上 attachShadow(MDN)即可发明该元素的“影子根”。该影子根同时也是此办法的返回值,能够通过元素的 shadowRoot(MDN)属性拜访。每个元素只能有一个影子根,调用屡次 attachShadow 会报一个 NotSupportedError 谬误。影子根领有失常元素的大多数 API。

由文档 API 动静构建

class GameCharacter extends HTMLElement {  
  imgEle;  
  
  nameEle;  
  
  constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
  
    // 构建元素构造  
    const imgEle = document.createElement('img');  
    imgEle.title = 'Avatar Picture Unset.';  
    imgEle.alt = 'Unknown Person.';  
    shadowRoot.appendChild(imgEle);  
    this.imgEle = imgEle;  
  
    const nameEle = document.createElement('span');  
    nameEle.textContent = 'Unknown Person.';  
    shadowRoot.appendChild(nameEle);  
    this.nameEle = nameEle;  
  }  
  
  // 响应 HTML 属性变更  
 attributeChangedCallback(name, oldValue, newValue) {switch (name) {  
      case 'data-image':  
        this.imgEle.src = newValue;  
        break;  
      case 'data-name':  
        this.nameEle.textContent = newValue;  
        this.imgEle.title = newValue;  
        this.imgEle.alt = newValue;  
        break;  
      default:  
        break;  
    }  
  }  
}  

简略的影子树结构能够间接应用示例的办法,一一生成。

由模板字符串动态构建

简单的影子树结构能够创立 <template>(MDN)元素,通过承受换行的 反引号 模板字符串,贮存作为模板。

模板
// 元素构造  
const template = document.createElement('template');  
template.innerHTML = `  
  <img title="Avatar Picture Unset." alt="Unknown Person.">  
  <span>Unknown Person.</span>  
`;  
模板构造的导入

模板元素 <template>content(MDN)属性返回模板元素子元素组成的 DocumentFragment(MDN)。

class GameCharacter extends HTMLElement {  
  // ...  
  
  constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
  
    // 构建元素构造  
    const children = template.content.cloneNode(true);  
    shadowRoot.appendChild(children);  
  
    this.imgEle = shadowRoot.querySelector('img');  
    this.nameEle = shadowRoot.querySelector('span');  
  }  
  
  // ...  
}  

贮存于 <template> 元素中的内容会被浏览器解析,但不会被渲染。

与世隔绝的款式设计

Shadow DOM 提供的 CSS 作用域保障了 影子树中的款式规定不会透露,内部款式也不会渗入 ,这带来了更简略高效的 CSS 选择器,更通用易读的 class 类名称,命名抵触之忧被打入冷宫。给予初涉模块化的开发者 入身世外桃源般的恍然大悟 ,犹如 糊涂少年与 Hello World 的悄悄初见。我的项目越做越大,代码又臭又长,作者今日竟能由此返璞归真,不由得破涕为笑。

:host(MDN)选择器负责抉择影子根的宿主。限定宿主须要满足的条件时,须要将相干选择器放到 :host()(MDN)选择器外部 :host-content()(MDN)选择器用于限定宿主的 父元素 须要满足的条件。

示例

在这个例子中,鼠标悬浮在自定义元素上时会扭转边框色彩和图像大小、父元素蕴含 .black 类时自定义元素会更换为彩色背景:

:host {  
  background: #fff;  
  color: #333;  
  border-width: 2px;  
  border-style: solid;  
  border-color: #2196f3;  
  transition: border-color 240ms ease-in-out;  
}  
  
:host-content(.black) {  
  background: #333;  
  color: #fff;  
}  
  
:host(:hover) {border-color: #ffc107;}  
  
/* 有效规定 */  
:host:hover {/* 轻易什么都有效 */}  
  
img {transition: transform 240ms ease-in-out;}  
  
:host(:hover) img {transform: scale(1.1);  
}  
如何利用

款式设计能够间接写在 JS 文件中,就像 由模板字符串动态 构建 元素 HTML 构造时做的一样,把 CSS 与 HTML <template> 放在一起。也能够抉择独立成为单个文件,在脚本中记录或获取该文件的 URL,在自定义元素的款式类子元素中导入。

动态记录款式
/* 蕴含在 JS 文件中 */  
const styleText = `  
  :host {...}  
  ...  
`;  
  
class GameCharacter extends HTMLElement {  
  //...  
  
  constructor() {  
    // ...  
  
    const styleEle = document.createElement('style');  
    styleEle.textContent = styleText;  
    shadowRoot.appendChild(styleEle);  
  
    // ...  
  }  
  
  // ...  
}  

上面的蕴含在 <template> 元素中的做法是较为举荐的,能够通过后文“import.meta”和“模板字符串”两节取得更多信息。

/* 蕴含在 JS 文件中 2,应用 <template> 元素 */  
const template = document.createElement('template');  
template.innerHTML = `  
  <style>  
  :host {/* ... */}  
  /* ... */  
  </style>  
  <img title="Avatar Picture Unset." alt="Unknown Person.">  
  <span>Unknown Person.</span>  
`;  
  
class GameCharacter extends HTMLElement {  
  // ...  
  
  constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
  
    // 构建元素构造  
    const children = template.content.cloneNode(true);  
    shadowRoot.appendChild(children);  
  
    // ...  
  }  
  
  // ...  
}  
导入内部样式表
/* 内部款式载入 */  
const styleURL = '//example.dev/index.css';  
  
class GameCharacter extends HTMLElement {  
  //...  
  
  constructor() {  
    // ...  
  
    const styleEle = document.createElement('style');  
    styleEle.textContent = `@import url(${styleURL})`;  
    shadowRoot.appendChild(styleEle);  
  
    // ...  
  }  
  
  // ...  
}  
/* 内部款式载入 2,应用 <link> 元素 */  
  constructor() {  
    // ...  
  
    const styleEle = document.createElement('link');  
    styleEle.setAttribute('rel', 'stylesheet');  
    styleEle.setAttribute('href', styleURL);  
    shadowRoot.appendChild(styleEle);  
  
    // ...  
  }  

回调函数

自定义元素波及的 回调函数名 、对应的 解释 补充点 如下:

名称 响应
constructor 元素实例被创立或降级。
connectedCallback 元素被注入 DOM 中。
disconnectedCallback 元素被移出 DOM。
attributeChangedCallback 元素指定的 HTML 属性被更改。
adoptedCallback 元素被移到新文档。

constructor

HTML Standard 对自定义元素所对应类的 constructor 函数的要求,如下:

  1. 每一个自定义元素对应类的 constructor 函数外部必须首先 无参数 调用一次 super
  2. 除了间接 returnreturn this,不能应用 return
  3. 不能调用 document.writedocument.open 办法。
  4. 不能获取或变更 任何 子元素和 任何 HTML 属性。
  5. 一般来说,尽可能把工作交给 connectedCallback(注入 DOM 回调)。
  6. 一般来说,constructor 函数的作用是设置默认值、事件侦听器和可能须要的 shadow root。

connectedCallback & disconnectedCallback

尽管字面上,元素注入和移除回调的触发意味着元素在或不在 DOM 状态的切换,但值得注意的是,这两个回调都是 不阻塞以后队列的同步函数 。这意味着,如果你将一个自定义元素增加入 DOM 后又疾速移除,那么在 connectedCallback 触发时该元素 可能曾经不在 DOM 里了

attributeChangedCallback

该回调会承受三个字符串模式的参数,第一个是 被扭转的 HTML 属性名 ,第二个是 被扭转前的旧值 ,第三个是 扭转后的新值 。该回调 只会 侦听类的 observedAttributes 动态 属性返回的数组中蕴含的 HTML 属性名。例如,

class GameCharacter extends HTMLElement {  
  // ...  
  static observedAttributes = ['data-image', 'data-name'];  
  
  attributeChangedCallback(name) {  
    // 永远为 true。this.constructor.observedAttributes.includes(name);  
  }  
  // ...  
}  

目前,网上大多数教程,包含 HTML 标准、MDN 文档中的示例,都将 observedAttributes 用一个动态 getter 返回,

  // ...  
  static get observedAttributes() {return ['data-image', 'data-name'];  
  }  
  // ...  

这是 ES6 晚期 class 外部不容许申明属性、只能蕴含函数时的做法,当初没有理由再这么做。

adoptedCallback

当元素被移入新 document 对象时调用。例如,存在一个自定义元素 ele,调用 document.adoptNode(ele) 将触发 ele 元素的 adoptedCallback 回调。

注册系列回调

注册流程实现后,每个 曾经存在 <game-character> 会触发一系列实例中的回调函数,该过程也称为“降级(upgrade)”,按程序 次要 蕴含:

  1. 跑一次 constructor
  2. 以元素每个存在的 HTML 属性为相干参数,各跑一次 attributeChangedCallback 属性变更回调。
  3. 若曾经存在于 DOM,跑一次 connectedCallback 注入 DOM 回调。

模块,是组件的灵魂

在 ES6 呈现之前,服务器和浏览器端的模块化次要由第三方框架 CommonJS 和 AMD 实现。ES6 提供了更为 简略间接 的解决方案,闭合了原生 HTML、CSS、JS 模块化开发的 最初一环

将自定义元素与 ES6 Module 联合,首先能够解决组件脚本拆散问题,毕竟 ES6 Module 就是为了将模块代码独立成文件而设计的。在脚本拆散后,???? 如何利用 Module 的个性,解决组件相干的 文件和变量依赖,是本节探讨的主题。

import.meta

ES6 中,被 import 导入的我的项目能够拜访一个 import.meta(MDN)对象,在以后 HTML 标准中其含且仅含一个 url 属性,以 字符串 的模式传递模块 脚本文件本身的门路。能够简略封装这个门路,发明一个获得同模块下其它文件门路或内容的模块。

获得文件门路
/* get-sibling-url/main.js */  
const getSiblingURL = (fileName, referencePath) => {const folderPath = referencePath.substr(0, referencePath.lastIndexOf('/'));  
  return `${folderPath}/${fileName}`;  
};  
  
export default getSiblingURL;  
获得文件内容(Promise)
/* get-sibling-file/main.js */  
const getSiblingFile = (fileName, referencePath) => {const folderPath = referencePath.substr(0, referencePath.lastIndexOf('/'));  
  const filePath = `${folderPath}/${fileName}`;  
  return fetch(filePath);  
};  
  
export default getSiblingFile;  

CSS 样式表要不要从模块 JS 中独立进去呢?

作者的倡议是,不须要 。古代代码编辑器曾经能够做到多窗口编辑同一个文件的不同局部,而自定义元素模块的 HTML、CSS、JS 之间的分割非常严密。这里摘录 Vue 文档“ 单文件组件”一章中的局部内容:

怎么对待关注点拆散?
一个重要的事件值得注意,关注点拆散不等于文件类型拆散。在古代 UI 开发中,咱们曾经发现相比于把代码库拆散成三个大的档次并将其相互交织起来,把它们划分为涣散耦合的组件再将其组合起来更正当一些。在一个组件里,其模板、逻辑和款式是外部耦合的,并且把他们搭配在一起实际上使得组件更加内聚且更可保护。

以字符串模式在 JS 中保留 CSS 和 HTML 具备肯定优化空间,倡议浏览后文“模板字符串”一节。

示例

自定义元素 <hello-world>,用于展现 settings.json 文件内 greeting 属性的字符,则模块配置参数 settings.json、模块脚本 main.js 和所在的目录构造能够为:

模块配置参数
{"greeting": "Are you OK?"}  
模块脚本
/* hello-world/main.js */  
import getSiblingFile from '../get-sibling-file';  
  
class HelloWorld extends HTMLElement {constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
  
    const selfPath = import.meta.url;  
    getSiblingFile('settings.json', selfPath)  
    .then((r) => r.json())  
    .then((settings) => {shadowRoot.textContent = settings.greeting;})  
    .catch(console.log);  
  }  
}  
  
export default HelloWorld;  
目录构造
├── ????src  
│   ├── ????modules  
│   │   ├── ????hello-world  
│   │   │   ├── ????settings.json  
│   │   │   ├── ????main.js  
│   │   │   ├── ????README.md  
│   │   │   └── ????package.json  
│   │   └── ????get-sibling-file  
│   │       ├── ????main.js  
│   │       ├── ????README.md  
│   │       └── ????package.json  
│   ├── ????images  
│   │   ├── ????banner.png  
│   │   └── ????icon.png  
│   ├── ????scripts  
│   │   └── ????index.js  
│   └── ????robots.txt  
├── ????CHANGELOG.md  
├── ✍LICENSE  
├── ????README.md  
├── ????package.json  

主脚本 index.js 将可能在 import 导入 HelloWorld 类之后,通过 customElements.define('hello-world', HelloWorld) 建设 <hello-world> 与该类的分割、实现元素在 DOM 中的注册。

注册回调 vs 注册函数

如果须要在注册时执行函数,<hello-world> 能够通过 customElements.whenDefined(MDN)侦听本人在 DOM 中的注册事件。该函数承受一个参数作为所侦听的 指标元素名称,返回一个 Promise,依据 whenDefined 被调用时指标元素名称的注册状况,立刻或稍后在注册时以 undefined 为值进行 resolve。然而,

  1. 在自定义元素模块外部动态贮存本身元素名称没有任何意义,还升高了模块的灵活性、可能 产生主脚本无奈解决的命名抵触,不合乎模块化的主旨;
  2. 如果该自定义元素外部存在其它的自定义元素,依赖 whenDefined 会使自定义元素的注册 从外元素到内元素 进行,违反直觉,逻辑上更加导致性能损耗;
  3. customElements 系列函数 无奈传参,限度了自定义元素模块与主脚本的信息传递。

哎呀这可难死作者了。怎么办呢?想来想去终于发现通过 创立 模块内 定义的具备肯定标准的“注册函数”替换注册回调 ,以上 所有 问题就都有理解法:

  1. 主脚本能够向注册函数 传参 ,能够管制该自定义元素及其子自定义元素的名称, 避免命名抵触
  2. 蕴含其它自定义元素的自定义元素组件,能够在本身的注册函数被调用时 首先调用子自定义元素的注册函数 ,发明 从内元素到外元素 的注册链。

示例

在上面的示例中,父自定义元素模块会承受指定的元素名称,在 注册本身前先调用子自定义元素的注册函数

父自定义元素模块
/* modules/parent-ele/main.js */  
import ChildEle from '../child-ele';  
  
// 默认名称。const elementNames = {  
  ParentEle: 'parent-ele',  
  ChildEle: 'child-ele',  
};  
  
class ParentEle {constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
    const child = new ChildEle();  
    shadowRoot.appendChild(child);  
  }  
  
  static register(nameSettings = {}) {Object.assign(elementNames, nameSettings);  
    // 先调用子自定义元素的注册函数,ChildEle.register(elementNames.ChildEle);  
    // 再在 DOM 中注册本身。customElements.define(elementNames.ParentEle, ParentEle);  
  }  
}  
  
export default ParentEle;  
子自定义元素模块

子自定义元素模块承受指定的元素名称,以指定名称注册本身。

/* modules/child-ele/main.js */  
  
// 默认名称。const elementName = 'spider-man';  
  
class ChildEle {static register(name) {if (name) elementName = name;  
    customElements.define(elementName, this);  
  }  
}  
  
export default ChildEle;  
主脚本

主脚本 理论管制着 导入的自定义元素模块的各元素的名称。

/* scripts/main.js */  
import ParentEle from '../modules/parent-ele';  
ParentEle.register({  
  ParentEle: 'ben-ele',  
  ChildEle: 'peter-ele',  
});  

ParentEle 会以 <ben-ele> 身份,ChildEle 会以 <peter-ele> 身份实现 DOM 注册流程。

如果,

  • 主脚本无参数调用注册函数,ParentEle 将会以 <parent-ele> 身份、ChildEle<child-ele> 身份实现 DOM 注册流程。
  • 主脚本无参数调用注册函数且导入的是 ChildEle,则 ChildEle 将会以 <spider-man> 身份实现 DOM 注册流程。

能够 同理嵌套多层 自定义元素模块。

法术,这是法术

不看到这你就输了。

FOUC 全解

不论是将 CSS 款式蕴含在 JS 文件中,还是设定 <style>@import url()</style><link> 从内部载入款式,自定义元素都有可能呈现 FOUC(Flash Of Unstyled Content,浏览器款式闪动)景象,极大地影响用户体验 。开发者仿佛只能抉择放弃 Shadow DOM、原生 CSS 作用域带来的微小便当,或是冀望用户承受 FOUC 造成的视觉进犯。 开发实际中的苦楚和悲伤 ,带走了前端码农对新个性的 好奇与神往 ,反噬着 Web Components 的倒退,自定义元素的前程在 FOUC 频频闪电的笼罩下,若 昏黑黯淡,土崩瓦解。

针对自定义元素,FOUC 分为两种状况。

既有元素定义闪动

“回调函数”一节中提到,DOM 中已有的自定义元素所关联的类,其相干函数会在调用 customElements.define 之后 ,触发。Web Components 规范在设计之初就没有筹备像 ES Module 一样反对动态剖析。自定义元素关联的类是 动静抉择 的,因而浏览器不可能做到在解析 DOM 时就执行自定义元素对应的类的相干函数。

那么在 customElements.define 指定的类的 constructor 执行时生成 Shadow DOM,势必导致一次元素重绘。

针对这种元素闪动,常见的解法是应用 :not(:defined) 选择器暗藏未定义的元素,比方将以下代码加到 主文档款式 中。

game-character:not(:defined) {visibility: hidden;}  

但对于主文档流中(positionstaticrelative)的自定义元素(MDN),往往还须要在 DOM 树 加载之前设定好定义后的宽高 ,免得定义后影响主文档流中的其它元素,造成肯定闪动。然而,在 DOM 树加载时计算宽高不合乎直觉,对于宽高不定的自定义元素简直 不事实 。在 2019 年 9 月 已经 有过一个提案,致力于解决这个“不事实”的问题,但起初因故废除[注],目前还未有新提案被提出。

可行的方法之一,是应用 过渡动画 加重视觉上的不适感。

/* 暂不须要,本例中默认宽高为 0 */  
game-character:not(:defined) {/* visibility: hidden; */}  
class GameCharacter extends HTMLElement {  
  //...  
  
  connectedCallback() {  
    // 默认宽高为 0。Object.assign(this.style, {  
      width: '0',  
      height: '0',  
      opacity: '0',  
      overflow: 'hidden',  
      transition: 'all 240ms ease-in-out',  
    });  
    // 过渡到内容宽高、重置透明度。Object.assign(this.style, {  
      // 计算内容宽高时会导致元素重绘,因而此处不会造成前一处设置宽高为零的语句生效。width: `${this.scrollWidth}px`,  
      height: `${this.scrollHeight}px`,  
      opacity: '',  
    });  
    // 过渡后重置变更。this.addEventListener('transitionend', () => {  
      Object.assign(this.style, {  
        width: '',  
        height: '',  
        overflow: '',  
        transition: '',  
      });  
    }, {once: true});  
  }  
  
  // ...  
}  

[注] 原提案参阅 Mitigating flash of unstyled content with custom elements
无关提案废除起因请自行在 2020 Spring Virtual F2F · Issue #855 · w3c/webcomponents 中检索关键词“FOUC”。

内部样式表载入闪动

与存在于主文档流时的行为不同,款式类元素 <style><link> 在 Shadow DOM 中 不会阻塞渲染,自定义元素即便在外联款式已被缓存的状况下,也可能会呈现一次闪动。动静增加自定义元素时,增加自身造成一次闪动,外联款式载入又造成一次闪动。

因而,即便从这个角度,在 Shadow DOM 中 也不倡议 导入内部样式表。

如果肯定要从内部导入,同时不产生额定的 FOUC 景象,那么能够先获取内部 CSS 文件,动静记录其文本内容为 JS 变量。在款式文本 载入之前 ,防止元素显示、影响其它元素定位;在 载入之后 ,将该变量作为 外部款式 利用到已有、后续创立的元素。

解决示例
import getSiblingFile from '../get-sibling-file';  
const selfPath = import.meta.url;  
const styleInfo = {  
  // 内部款式文件名。fileName: 'main.css',  
  
  // 初始款式,absolute 不影响主文档流元素定位,hidden 暗藏元素。// 不应用 display: none 以防止外部图片等资源不被加载。defaultText: `  
    :host {  
      position: absolute;  
      visibility: hidden;  
    }  
  `,  
  
  // 贮存内部款式的变量。text: '',  
  
  // 期待内部款式加载的实例。applyQueue: [],  
  
  // 加载内部款式。load() {getSiblingFile(this.fileName, selfPath)  
    .then((r) => r.text())  
    .then((text) => {  
      this.text = text;  
      // 批改须要载入款式的实例。this.applyQueue.forEach((ele) => {const { styleEle} = ele;  
        styleEle.textContent = text;  
      });  
    }).catch((error) => {throw error;});  
  },  
};  
  
// 加载。styleInfo.load();  
  
class OutsideLover extends HTMLElement {  
  styleEle = null;  
  
  constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
  
    const styleEle = document.createElement('style');  
    this.styleEle = styleEle;  
    // 依据加载实现与否,if (!styleInfo.text) {  
      // 应用默认款式,置入期待队列。styleEle.textContent = styleInfo.defaultText;  
      styleInfo.applyQueue.push(this);  
    } else {  
      // 应用加载实现的款式。styleEle.textContent = styleInfo.text;  
    }  
    shadowRoot.appendChild(styleEle);  
  
    // Something else.  
  }  
}  
  

作者曾经将其做成了一个简略的插件。在导入之后,将继承的 HTMLElement 改为 StylingAdvancedHTMLElement,省去 attachShadow,把要导入的内部款式文件名记为动态属性 styleFileUrl 即可。

示例中的代码就能够简化成这样:

import StylingAdvancedHTMLElement from '//raw.githubusercontent.com/PaperFlu/StylingAdvancedHTMLElement/master/export.js';  
// 取得 Promise 改为取得 URL。import getSiblingURL from '../get-sibling-url';  
const selfPath = import.meta.url;  
  
class OutsideLover extends HTMLElement {styleFileUrl = getSiblingURL('main.css', selfPath);  
  
  constructor() {super();  
    // 不再调用 attachShadow,间接援用 shadowRoot。const {shadowRoot} = this;  
  
    // Something else.  
  }  
}  
  

模板字符串

Vue 设计者在文档中明确地指出应用字符串在 JS 中保留组件的 HTML 构造的毛病,也是字符串保留其它所有语言文本的独特毛病:一是失去了编辑器语法高亮,二是换行须要应用 “。不同于 Vue 中的“字符串模板”,ES6 的模板字符串(MDN)承受换行 ,在大多数编辑器,以及 Github、NPM 等处的 Markdown 代码块内, 能够通过增加标签失去语法高亮

// ES6 模板字符串能够通过这种形式在大多数编辑器中取得正确的语法高亮  
// 并被 Prettier 一类的格式化工具正确处理。const html = String.raw;  
const someHTML = html`  
  <img title="Avatar Picture Unset." alt="Unknown Person.">  
  <span>Unknown Person.</span>  
`;  

在模板字符串前增加标签是存在于 ES6 规范中的非法操作,标签将被视为函数,以肯定规定解析该模板字符串(MDN)。相似 Prettier 的格式化工具可能在标签的辅助下正确格式化模板字符串。

NPM 上有一个包,common-tags,设计了 ES6+ 罕用的标签函数。装置后,能够这样:

import {html} from 'common-tags';  
const template = document.createElement('template');  
template.innerHTML = html`  
  <style>  
    :host {  
      background: #fff;  
      color: #333;  
      border-width: 2px;  
      border-style: solid;  
      border-color: #2196f3;  
      transition: border-color 240ms ease-in-out;  
    }  
  </style>  
  <img title="Avatar Picture Unset." alt="Unknown Person.">  
  <span>Unknown Person.</span>  
`;  
  
class GameCharacter extends HTMLElement {constructor() {super();  
    const shadowRoot = this.attachShadow({mode: 'open'});  
  
    // 构建元素构造  
    const children = template.content.cloneNode(true);  
    shadowRoot.appendChild(children);  
  
  }  
}  

common-tags 的设计不仅仅是为开启编辑器语法高亮,它还能够实现多余空格打消、换行调整、HTML 平安本义等。(官网文档)

语法高亮示例

扩大原生元素

如果你不想设计一个全新的自定义元素,只是对现有元素 不称心 ,怎么办?用于分割自定义元素名与类之间的关系的 customElements.define 还承受 第三个参数 ,用于在须要时标记要扩大的原生元素名。对于这样的自定义元素,咱们叫它: 自定义原生元素

同时,自定义原生元素的对应类继承的可能就不是 HTMLElement 了,它须要 继承被扩大的原生元素的 DOM 接口。比方咱们须要扩大 <button>,咱们就须要继承 HTMLButtonElement,如果咱们须要扩大 <img>,咱们的类就须要继承 HTMLImageElement

到了这里,我的右手就不快乐了,它认为既然都继承了接口,注册的时候,间接疏忽被扩大的原生元素的名字,不是很好看、很简洁吗?因而不违心帮我持续码字。哎呀我好说歹说,它批准帮我去搜寻这个问题,最初发现,不同的原生元素对应的 DOM 接口可能会雷同。就像 <blockquote><q>,接口都是 HTMLQuoteElement,浏览器怎么晓得你想扩大的是哪一个。HTML 标准中有具体的元素接口对应表。

这下我的右手称心了,它批准帮我持续实现上面的文章,做一个可能轻松输入本人 DataURL<img>。这样咱们在用纯文本传递图片的时候,就不便多了。

示例

在本地文件系统测试本例可能会呈现 CORS 相干谬误。(file://

// 定义一个扩大 <img> 的自定义元素  
class TextImg extends HTMLImageElement {get dataURL() {const canvas = document.createElement('canvas');  
    canvas.width = this.width;  
    canvas.height = this.height;  
  
    const ctx = canvas.getContext('2d');  
    ctx.drawImage(this, 0, 0);  
  
    return canvas.toDataURL('image/x-icon');  
  }  
}  
customElements.define('text-img', TextImg, { extends: 'img'});  

动态和动态创建

自定义原生元素的标签名不变,但存在一个 is 属性,批示其对应的 自定义元素名,从而由此绑定对应的类。

在 HTML 中创立
<img is="text-img" src="example.png" alt="example">  
在 JS 中创立
// 能够这样  
const ele = document.createElement('img', { is: 'text-img'});  
// 也能够这样  
const ele = new TextImg();  
  
ele.src = 'example.png';  
ele.alt = 'example';  

矛盾?谬误?

可是看到这里,我的读者又不称心了,他们通过宽泛的学习,发现我的扩大办法和谷歌 Web Fundamentals 上的 Custom Elements v1: Reusable Web Components 教程 不 一 样!教程里的示例二 明明写着:

Example – extending <img>:

customElements.define('bigger-img', class extends Image {  
  // Give img default size if users don't specify.  
  constructor(width=50, height=50) {super(width * 10, height * 10);  
  }  
}, {extends: 'img'});  

Users declare this component as:

<!-- This <img> is a bigger img. -->  
<img is="bigger-img" width="15" height="20">  

or create an instance in JavaScript:

const BiggerImage = customElements.get('bigger-img');  
const image = new BiggerImage(15, 20); // pass constructor values like so.  
console.assert(image.width === 150);  
console.assert(image.height === 200);  

继承的是 Image 呢!

所以也是没有方法,又仔细阅读了这篇文章。发现这篇文章前后仿佛是存在矛盾的,示例之前明明也点明了 <img> 的扩大要继承 HTMLImageElement

To extend an element, you’ll need to create a class definition that inherits from the correct DOM interface. For example, a custom element that extends <button> needs to inherit from HTMLButtonElement instead of HTMLElement. Similarly, an element that extends <img> needs to extend HTMLImageElement.

我的右手因为之前的误会,决定要弥补我,使出了九牛二虎之力,搜到这个示例至多从 2016 年 12 月 9 日开始,就再没有更新过了,而那个时候自定义原生元素还没有被浏览器反对[注]。即便到了当初,自定义原生元素的需要还很小,因而右手狐疑文章年久失更,在 GitHub 上开了个 issue 提出这个问题,只是目前还没有人理。

[注] 参阅 javascript – How to extend default HTML elements as a “customized built-in element” using Web Components? – Stack Overflow。探讨点不同,但援用了同一文段。

当初你们都称心了,那咱们就持续吧。

留神点

  • 应用 JS 编程式创立的元素的 is 属性 在序列化时显示,但 不会 在 DOM 中显示。
  • 所有原生 <img> 元素的个性 都可 用于自定义 <img> 元素。比方 src 显示图片,onload 事件等。
  • 自定义原生元素 只能扩大标准中蕴含的 HTML 元素,元素接口为 HTMLUnknownElement 的旧有元素,如 <bgsound><blink><isindex><keygen><multicol><nextid><spacer>,不能被扩大。

参阅

自定义原生元素在 HTML 标准中的示例:Creating a customized built-in element – HTML Standard

结语

真心感激有人有急躁能看到最初,我太打动了 ???? !!点个????、收个藏再走吧!

材料起源

  • HTML Standard
  • DOM Standard
  • CSS Working Group Editor Drafts
  • TC39 – Specifying JavaScript.
  • webcomponents.org
  • MDN Web Docs
  • Web Fundamentals | Google Developers
  • Web Components 入门实例教程 – 阮一峰的网络日志
  • 单文件组件 — Vue.js
  • GitHub
正文完
 0