你晓得原生HTML组件有哪些吗?原生HTML组件的介绍
嘿!看看这几年啊,Web 前端的开展可是真快啊!
想想几年前,HTML 是前端开发者的根本技艺,通过林林总总的标签就可以搭建一个可用的网站,根本交互也不是问题。假如再来点 CSS,嗯,金黄酥脆,美味可口。这时候再撒上几把 JavaScript,几乎让人骑虎难下。
随着需求的增长,HTML 的构造越来越复杂,大量反复的代码使得页面改动起来非常艰难,这也就孵化了一批批模版工具,将公共的部分抽取出来变为公共组件。再后来,随着 JavaScript 的机能晋升,JavaScript 的地位越来越高,不再只是配菜了,前端渲染的显现落低了效劳端解析模版的压力,效劳端只要供给静态文件和 API 接口就行了嘛。再然后,前端渲染工具又被搬回了效劳端,后端渲染显现了(黑人问号???)
总之,组件化使得复杂的前端构造变得清楚,各个部分独立起来,高内聚低耦合,使得保护成本大大落低。
那么,你有据说过原生 HTML 组件吗?
四大 Web 组件标准
在说原生 HTML 组件此前,要先简便介绍一下四大 Web 组件标准,四大 Web 组件标准离别为:HTML Template、Shadow DOM、Custom Elements 和 HTML Imports。实际上其中一个已经被废弃了,所以变成“三大”了。
HTML Template 信赖许多人都有所耳闻,简便的讲也就是 HTML5 中的 <template> 标签,正常状况下它无色无味,感知不到它的存在,乃至它下面的 img 都不会被下载,script 都不会被施行。<template> 就如它的名字一样,它只是一个模版,只要到你用到它时,它才会变得成心义。
Shadow DOM 则是原生组件封装的根本工具,它可以实现组件与组件之间的独立性。
Custom Elements 是用来包装原生组件的容器,通过它,你就只需要写一个标签,就能得到一个完全的组件。
HTML Imports 则是 HTML 中相似于 ES6 Module 的一个东西,你可以直接 import 另一个 html 文件,然后使用其中的 DOM 节点。但是,由于 HTML Imports 和 ES6 Module 实在是太像了,并且除了 Chrome 之外没有阅读器情愿实现它,所以它已经被废弃并不引荐使用了。将来会使用 ES6 Module 来代替它,但是此刻貌似还没有代替的方案,在新版的 Chrome 中这个功效已经被删除了,并且在使用的时候会在 Console 中给出警告。警告中说使用 ES Modules 来代替,但是我测试在 Chrome 71 中 ES Module 会强迫检测文件的 MIME 类型必需为 JavaScript 类型,应当是临时还没有实现支撑。
Shadow DOM
要说原生 HTML 组件,就要先聊聊 Shadow DOM 到底是个什么东西。
大家对 DOM 都很熟知了,在 HTML 中作为一个最根基的骨架而存在,它是一个树构造,树上的每一个节点都是 HTML 中的一部分。DOM 作为一棵树,它具有着上下级的层级关系,我们平常使用“父节点”、“子节点”、“兄弟节点”等来停止描写(当然有人觉得这些称呼强调性别,所以也制造了一些性别无关的称呼)。子节点在必然程度上会继承父节点的一些东西,也会因兄弟节点而发生必然的影响,比力明显的是在利用 CSS Style 的时候,子节点会从父节点那里继承一些样式。
而 Shadow DOM,也是 DOM 的一种,所以它也是一颗树,只不外它是长在 DOM 树上的一棵非凡的紫薯,啊不,子树。
什么?DOM 本身不就是由一棵一棵的子树组成的吗?这个 Shadow DOM 有什么特殊的吗?
Shadow DOM 的特殊之处就在于它致力于创立一个相对独立的一个空间,虽然也是长在 DOM 树上的,但是它的环境却是与外界隔离的,当然这个隔离是相对的,在这个隔离空间中,你可以选中性地从 DOM 树上的父节点继承一些属性,乃至是继承一棵 DOM 树进来。
利用 Shadow DOM 的隔离性,我们就可以制造原生的 HTML 组件了。
实际上,阅读器已经通过 Shadow DOM 实现了一些组件了,只是我们使用过却没有发觉罢了,这也是 Shadow DOM 封装的组件的魅力所在:你尽管写一个 HTML 标签,其他的交给我。(是不是有点像 React 的 JSX 啊?)
我们来看一看阅读器利用 Shadow DOM 实现的一个示例吧,那就是 video 标签:
<video controls src="./video.mp4" width="400" height="300"></video>
我们来看一下阅读器渲染的结果:
等一下!不是说 Shadow DOM 吗?这和一般 DOM 有啥不同???
在 Chrome 中,Elements 默许是不显示内部实现的 Shadow DOM 节点的,需要在设定中启用:
注:阅读器默许潜藏本身的 Shadow DOM 实现,但假如是会员通过足本制造的 Shadow DOM,是不会被潜藏的。然后,我们就可以看到 video 标签的真面目了:
在这里,你可完全像调试一般 DOM 一样随便调整 Shadow DOM 中的内容(反正和一般 DOM 一样,刷新一下就复原了)。
我们可以看到上面这些 shadow DOM 中的节点大多都有 pseudo 属性,按照这个属性,你就可以在外面编写 CSS 样式来操纵对应的节点样式了。比方,将上面这个 pseudo="-webkit-media-controls-overlay-play-button" 的 input 按钮的背风光改为橙色:
video::-webkit-media-controls-overlay-play-button { background-color: orange; }
由于 Shadow DOM 实际上也是 DOM 的一种,所以在 Shadow DOM 中还可以连续嵌套 Shadow DOM,就像上面那样。
阅读器中还有许多 Element 都使用了 Shadow DOM 的情势停止封装,比方 <input>、<select>、<audio> 等,这里就不一一展现了。
由于 Shadow DOM 的隔离性,所以即使是你在外面写了个样式:div { background-color: red !important; },Shadow DOM 内部的 div 也不会受到任何影响
也就是说,写样式的时候,该用 id 的时候就用 id,该用 class 的时候就用 class,一个按钮的 class 应当写成 .button 就写成 .button。完全不消思考当前组件中的 id、class 大概会与其他组件冲突,你只要确保一个组件内部不冲突就好——这很容易做到。
这解决了此刻绝大多数的组件化框架都面临的问题:Element 的 class(className) 到底如何写?用前缀命名空间的情势会致使 class 名太长,像这样:.header-nav-list-sublist-button-icon;而使用一些 CSS-in-JS 工具,可以制造一些独一的 class 名称,像这样:.Nav__welcomeWrapper___lKXTg,这样的名称仍然有点长,还带了冗余信息。
ShadowRoot
ShadowRoot 是 Shadow DOM 下面的根,你可以把它当做 DOM 中的 <body> 一样对待,但是它不是 <body>,所以你不克不及使用 <body> 上的一些属性,乃至它不是一个节点。
你可以通过 ShadowRoot 下面的 appendChild、querySelectorAll 之类的属性或办法去操纵整个 Shadow DOM 树。
关于一个一般的 Element,比方 <p>,你可以通过调取它上面的 attachShadow 办法来创立一个 ShadowRoot(还有一个 createShadowRoot 办法,已经过时不引荐使用),attachShadow 接受一个对象停止初始化:{ mode: 'open' },这个对象有一个 mode 属性,它有两个取值:'open' 和 'closed',这个属性是在制造 ShadowRoot 的时候需要初始化供给的,并在创立 ShadowRoot 之后成为一个只读属性。
mode: 'open' 和 mode: 'closed' 有什么不同呢?在调取 attachShadow 创立 ShadowRoot 之后,attachShdow 办法会返回 ShadowRoot 对象实例,你可以通过这个返回值去结构整个 Shadow DOM。当 mode 为 'open' 时,在用于创立 ShadowRoot 的外部一般节点(比方 <p>)上,会有一个 shadowRoot 属性,这个属性也就是制造出来的阿谁 ShadowRoot,也就是说,在创立 ShadowRoot 之后,还是可以在任何地方通过这个属性再得到 ShadowRoot,连续对其停止革新;而当 mode 为 'closed' 时,你将不克不及再得到这个属性,这个属性会被设定为 null,也就是说,你只能在 attachShadow 之后得到 ShadowRoot 对象,用于结构整个 Shadow DOM,一旦你失去对这个对象的援用,你就没法再对 Shadow DOM 停止革新了。
可以从上面 Shadow DOM 的截图中看到 #shadow-root (user-agent) 的字样,这就是 ShadowRoot 对象了,而括号中的 user-agent 表示这是阅读器内部实现的 Shadow DOM,假如使用通过足本本人创立的 ShadowRoot,括号中会显示为 open 或 closed 表示 Shadow DOM 的 mode。
阅读器内部实现的 user-agent 的 mode 为 closed,所以你不克不及通过节点的 ShadowRoot 属性去获得其 ShadowRoot 对象,也就意味着你不克不及通过足本对这些阅读器内部实现的 Shadow DOM 停止革新。
HTML Template
有了 ShadowRoot 对象,我们可以通过代码来创立内部构造了,关于简便的构造,或许我们可以直接通过 document.createElement 来创立,但是轻微复杂一些的构造,假如全部都这样来创立不仅费事,并且代码可读性也很差。当然也可以通过 ES6 供给的反引号字符串(const template = `......`;)配合 innerHTML 来结构构造,利用反引号字符串中可以任意换行,并且 HTML 对缩进并不敏锐的特性来实现模版,但是这样也是不足文雅,究竟代码里大段大段的 HTML 字符串并不美妙,即使是独自抽出一个常量文件也是一样。
这个时候就可以请 HTML Template 登场了。我们可以在 html 文档中编写 DOM 构造,然后在 ShadowRoot 中加载过来即可。
HTML Template 实际上就是在 html 中的一个 <template> 标签,正常状况下,这个标签下的内容是不会被渲染的,包罗标签下的 img、style、script 等都是不会被加载或施行的。你可以在足本中使用 getElementById 之类的办法得到 <template> 标签对应的节点,但是却没法直接拜访到其内部的节点,由于默许他们只是模版,在阅读器中展现为 #document-fragment,字面意思就是“文档片段”,可以通过节点对象的 content 属性来拜访到这个 document-fragment 对象。
通过 document-fragment 对象,就可以拜访到 template 内部的节点了,通过 document.importNode 办法,可以将 document-fragment 对象创立一份副本,然后可以使用一切 DOM 属性办法更换副本中的模版内容,终究将其插入到 DOM 或是 Shadow DOM 中。
<div id="div"></div> <template id="temp"> <div id="title"></div> </template>
const template = document.getElementById('temp'); const copy = document.importNode(template.content, true); copy.getElementById('title').innerHTML = 'Hello World!'; const div = document.getElementById('div'); const shadowRoot = div.attachShadow({ mode: 'closed' }); shadowRoot.appendChild(copy);
HTML Imports
有了 HTML Template,我们已经可以利便地制造封闭的 Web 组件了,但是当前还有一些不完善的地方:我们必需要在 html 中定义一大批的 <template>,每个组件都要定义一个 <template>。
此时,我们就可以用到已经被废弃的 HTML Imports 了。虽然它已经被废弃了,但是将来会通过 ES6 Modules 的情势再停止支撑,所以理论上也只是换个加载情势罢了。
通过 HTML Imports,我们可以将 <template> 定义在其他的 html 文档中,然后再在需要的 html 文档中停止导入(当然也可以通过足本按需导入),导入后,我们就可以直接使用其中定义的模版节点了。
已经废弃的 HTML Imports 通过 <link> 标签实现,只要指定 rel="import" 就可以了,就像这样:<link rel="import" href="./templates.html">,它可以接受 onload 和 onerror 事件以指示它已经加载完成。当然也可以通过足原本创立 link 节点,然后指定 rel 和 href 来按需加载。Import 成功后,在 link 节点上有一个 import 属性,这个属性中储备的就是 import 进来的 DOM 树啦,可以 querySelector 之类的,并通过 cloneNode 或 document.importNode 办法创立副本后使用。
将来新的 HTML Imports 将会以 ES6 Module 的情势供给,可以在 JavaScript 中直接 import * as template from './template.html';,也可以按需 import,像这样:const template = await import('./template.html');。不外当前虽然阅读器都已经支撑 ES6 Modules,但是在 import 其他模块时会检查效劳端返回文件的 MIME 类型必需为 JavaScript 的 MIME 类型,不然不同意加载。
Custom Elements
有了上面的三个组件标准,我们实际上只是对 HTML 停止拆分罢了,将一个大的 DOM 树拆成一个个彼此隔离的小 DOM 树,这还不是真正的组件。
要实现一个真正的组件,我们就需要用到 Custom Elements 了,就如它的名字一样,它是用来定义原生组件的。
Custom Elements 的中心,实际上就是利用 JavaScript 中的对象继承,去继承 HTML 原生的 HTMLElement 类(或是详细的某个原生 Element 类,比方 HTMLButtonElement),然后本人编写相关的生命周期函数,处置成员属性乃至会员交互的事件。
看起来这和此刻的 React 很像,在 React 中,你可以这样制造一个组件:class MyElement extends React.Component { ... },而使用原生 Custom Elements,你需要这样写:class MyElement extends HTMLElement { ... }。
Custom Elements 的生命周期函数并不多,但是足够使用。这里我将 Custom Elements 的生命周期函数与 React 停止一个简便的对照:
constructor(): 结构函数,用于初始化 state、创立 Shadow DOM、监听事件之类。
对应 React 中 Mounting 阶段的大半部分,包罗:constructor(props)、static getDerivedStateFromProps(props, state) 和 render()。
在 Custom Elements 中,constructor() 结构函数就是其本来的含义:初始化,和 React 的初始化相似,但它没有像 React 中那样将其拆分为多个部分。在这个阶段,组件仅仅是被创立出来(比方通过 document.createElement()),但是还没有插入到 DOM 树中。
connectedCallback(): 组件实例已被插入到 DOM 树中,用于停止一些展现相关的初始化操纵。
对应 React 中 Mounting 阶段的最后一个生命周期:componentDidMount()。
在这个阶段,组件已经被插入到 DOM 树中了,或是其本身就在 html 文件中写好在 DOM 树上了,这个阶段一样是停止一些展现相关的初始化,比方加载数据、图片、音频或视频之类并停止展现。
attributeChangedCallback(attrName, oldVal, newVal): 组件属性发生转变,用于更新组件的状态。
对应 React 中的 Updating 阶段:static getDerivedStateFromProps(props, state)、shouldComponentUpdate(nextProps, nextState)、render()、getSnapshotBeforeUpdate(prevProps, prevState) 和 componentDidUpdate(prevProps, prevState, snapshot)。
当组件的属性(React 中的 props)发生转变时触发这个生命周期,但是并不是所有属性转变都会触发,比方组件的 class、style 之类的属性发生转变一样是不会发生非凡交互的,假如所有属性发生转变都触发这个生命周期的话,会使得机能造成较大的影响。所以 Custom Elements 要求开发者供给一个属性列表,只要当属性列表中的属性发生转变时才会触发这个生命周期函数。
这个属性列表通过组件类上的一个静态只读属性来声明,在 ES6 Class 中使用一个 getter 函数来实现,只实现 getter 而不实现 setter,getter 返回一个常量,这样就是只读的了。像这样:
class AwesomeElement extends HTMLElement { static get observedAttributes() { return ['awesome']; } }
disconnectedCallback(): 组件被从 DOM 树中移除,用于停止一些清算操纵。
对应 React 中的 Unmounting 阶段:componentWillUnmount()。
adoptedCallback(): 组件实例从一个文档被移动到另一个文档。
这个生命周期是原生组件独占的,React 中没有相似的生命周期。这个生命周期函数也并不常用到,一样在操纵多个 document 的时候会碰到,调取 document.adoptNode() 函数转移节点所属 document 时会触发这个生命周期。
在定义了自定义组件后,我们需要将它注册到 HTML 标签列表中,通过 window.customElements.define() 函数即可实现,这个函数接受两个必需参数和一个可选参数。第一个参数是注册的标签名,为了不和 HTML 本身的标签冲突,Custom Elements 要求会员自定义的组件名必需至少包括一个短杠 -,并且不克不及以短杠开头,比方 my-element、awesome-button 之类都是可以的。第二个参数是注册的组件的 class,直接将继承的子类类名传入即可,当然也可以直接写一个匿名类:
window.customElements.define('my-element', class extends HTMLElement { ... });
注册之后,我们就可以使用了,可以直接在 html 文档中写对应的标签,比方:<my-element></my-element>,也可以通过 document.createElement('my-element') 来创立,用途与一般标签几乎完全一样。但要留意的是,虽然 html 标准中说部分标签可以不关闭或是自关闭(<br> 或是 <br />),但是只要规定的少数几个标签同意自关闭,所以,在 html 中写 Custom Elements 的节点时必需带上关闭标签。
由于 Custom Elements 是通过 JavaScript 来定义的,而一样 js 文件都是通过 <script> 标签外联的,所以 html 文档中的 Custom Elements 在 JavaScript 未施行时是处于一个默许的状态,阅读器默许会将其内容直接显示出来。为了不这样的状况发生,Custom Elements 在被注册后都会有一个 :defined CSS 伪类而在注册前没有,所以我们可以通过 CSS 选中器在 Custom Elements 注册前将其潜藏起来,比方:
my-element:not(:defined) { display: none; }
或者 Custom Elements 也供给了一个函数来检测指定的组件可否已经被注册:customElements.whenDefined(),这个函数接受一个组件名参数,并返回一个 Promise,当 Promise 被 resolve 时,就表示组件被注册了。
这样,我们就可以安心的在加载 Custom Elements 的 JavaScript 的 <script> 标签上使用 async 属性来延迟加载了(当然,假如是使用 ES6 Modules 情势的话默许的加载行动就会和 defer 相似)。
Custom Elements + Shadow DOM
使用 Custom Elements 来创立组件时,平常会与 Shadow DOM 停止结合,利用 Shadow DOM 的隔离性,就可以制造独立的组件。
平常在 Custom Elements 的 constructor() 结构函数中去创立 Shadow DOM,并对 Shadow DOM 中的节点增加事件监听、对特定事件触发原生 Events 对象。
正常编写 html 文档时,我们大概会给 Custom Elements 增加一些子节点,像这样:<my-element><h1>Title</h1><p>Content</p></my-element>,而我们创立的 Shadow DOM 又具有其本人的构造,怎样将这些子节点放置到 Shadow DOM 中准确的位置上呢?
在 React 中,这些子节点被放置在 props 的 children 中,我们可以在 render() 时选中将它放在哪里。而在 Shadow DOM 中有一个非凡的标签:<slot>,这个标签的用途就犹如其字面意思,在 Shadow DOM 上放置一个“插槽”,然后 Custom Elements 的子节点就会主动放置到这个“插槽”中了。
有时我们需要愈加准确地操纵子节点在 Shadow DOM 中的位置,而默许状况下,所有子节点都会被放置在统一个 <slot> 标签下,即使是你写了多个 <slot>。那怎样更准确地对子节点停止操纵呢?
默许状况下,<slot>Fallback</slot> 这样的是默许的 <slot>,只要第一个默许的 <slot> 会有效,将所有子节点全部放进去,假如没有可用的子节点,将会显示默许的 Fallback 内容(Fallback 可以是一棵子 DOM 树)。
<slot> 标签有一个 name 属性,当你供给 name 后,它将变为一个“有名字的 <slot>”,这样的 <slot> 可以存在多个,只要名字各不雷同。此时他们会主动匹配 Custom Elements 下带 slot 属性并且 slot 属性与本身 name 雷同的子节点,像这样
<template id="list"> <div> <h1>Others</h1> <slot></slot> </div> <div> <h1>Animals</h1> <slot name="animal"></slot> </div> <div> <h1>Fruits</h1> <slot name="fruit"></slot> </div> </template> <my-list> <div slot="animal">Cat</div> <div slot="fruit">Apple</div> <div slot="fruit">Banana</div> <div slot="other">flower</div> <div>pencil</div> <div slot="animal">Dog</div> <div slot="fruit">peach</div> <div>red</div> </my-list>
class MyList extends HTMLElement { constructor() { super(); const root = this.attachShadow({ mode: 'open' }); const template = document.getElementById('list'); root.appendChild(document.importNode(template.content, true)); } } customElements.define('my-list', MyList);
这样就可以得到如图所示的构造,#shadow-root (open) 表示这是一个开放的 Shadow DOM,下面的节点是直接从 template 中 clone 过来的,阅读器主动在三个 <slot> 标签下放置了几个灰色的 <div> 节点,实际上这些灰色的 <div> 节点表示的是到其真实节点的“援用”,鼠标移动到他们上会显示一个 reveal 链接,点击这个链接即可跳转至其真实节点。
这里我们可以看到,虽然 <my-list> 下的子节点是乱序放置的,但是只如果给定了 slot 属性,就会被放置到准确的 <slot> 标签下。留意视察其中有一个 <div slot="other">flower</div>,这个节点由于指定了 slot="other",但是却寻不到匹配的 <slot> 标签,所以它不会被显示在结果中。
在为 Custom Elements 下的 Shadow DOM 设定样式的时候,我们可以直接在 Shadow DOM 下放置 <style> 标签,也可以放置 <link rel="stylesheet">,Shadow DOM 下的样式都是部分的,所以不消担忧会影响到 Shadow DOM 的外部。并且由于这些样式仅影响部分,所以对机能也有很大的晋升。
在 Shadow DOM 内部的样式中,也有一些特定的选中器,比方 :host 选中器,代表着 ShadowRoot,这相似于一般 DOM 中的 :root,并且它可以与其他伪类组合使用,比方当鼠标在组件上时::host(:hover),当组件具有某个 class 时::host(.awesome),当组件具有 disabled 属性时::host([disabled])……但是 :host 是具有继承属性的,所以假如在 Custom Elements 外部定义了某些样式,将会覆盖 :host 中的样式,这样就可以轻松地实现林林总总的“主题风格”了。
为了实现自定义主题,我们还可以使用 Shadow DOM 供给的 :host-context() 选中器,这个选中器同意检查 Shadow DOM 的任何祖先节点可否包括指定选中器。比方假如在最外层 DOM 的 <html> 或 <body> 上有一个 class:.night,则 Shadow DOM 内就可以使用 :host-context(.night) 来指定一个夜晚的主题。这样可以实现主题样式的继承。
还有一种样式的定义方式是利用 CSS 变量。我们在 Shadow DOM 中使用变量来指定样式,比方:background-color: var(--bg-colour, #0F0);,这样就可以在 Shadow DOM 外面指定 --bg-colour 变量来设定样式了,假如没有指定变量,将使用默许的样式色彩 #0F0。
有时我们需要在 Shadow DOM 内部使用完全自定义的样式,比方字体样式、字体大小,假如任由其继承大概致使规划错乱,而每次在组件外面指定样式又略显费事,并且也毁坏了组件的封装性。所以,Shadow DOM 供给了一个 all 属性,只要指定 :host{ all: initial; } 就可以重置所有继承的属性。
Demo
Web Components 的 Demo 在网上已经有许多了,这是我 2 年前初次接触 ES6 与 Web Components 的时候写的一个 Demo:https://github.com/jinliming2/Calendar-js,一个日历,当时还是 v0 的标准,并且在 Firefox 下还存在会致使 Firefox 崩溃的 Bug(感受是 Firefox 在实现 Shadow DOM 时的 Bug)。当前这个 Demo 已经不克不及在 Firefox 下运转了,由于 Firefox 已经删除了 v0 标准,开端执行 v1 标准了,所以近期我大概会重构一下这个 Demo。
以上就是你知道原生HTML组件是啥吗?原生HTML组件的介绍的具体内容,更多请关注百分百源码网其它相关文章!