首頁 > web前端 > html教學 > 你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

不言
發布: 2018-10-18 13:58:17
轉載
7455 人瀏覽過

這篇文章帶給大家的內容是關於你知道原生HTML元件是什麼嗎?原生HTML元件的介紹,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

嘿!看看這幾年啊,Web 前端的發展可是真快啊!

想想幾年前,HTML 是前端開發者的基本技能,透過各式各樣的標籤就可以搭建一個可用的網站,基本互動也不是問題。如果再來點 CSS,嗯,金黃酥脆,美味可口。這時候再撒上幾把 JavaScript,簡直讓人欲罷不能。

隨著需求的成長,HTML 的結構越來越複雜,大量重複的程式碼使得頁面改動起來異常困難,這也就孵化了一批批模版工具,將公共的部分抽取出來變為公共組件。再後來,隨著 JavaScript 的效能提升,JavaScript 的地位越來越高,不再只是配菜了,前端渲染的出現降低了服務端解析模版的壓力,服務端只要提供靜態檔案和 API 接口就行了嘛。再然後,前端渲染工具又被搬回了服務端,後端渲染出現了(黑人問號???)

總之,組件化使得複雜的前端結構變得清晰,各個部分獨立起來,高內聚低耦合,使得維修成本大幅降低。

那麼,你聽過原生 HTML 元件嗎?

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

四大Web 元件標準

在說原生HTML 元件之前,先簡單介紹一下四大Web 元件標準,四大Web 元件標準分別為:HTML Template、Shadow DOM、Custom Elements 和 HTML Imports。實際上其中一個已經被廢棄了,所以變成「三大」了。

HTML Template 相信很多人都有所耳聞,簡單的講也就是HTML5 中的

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 類型,應該是暫時還沒有實作支援。

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

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></video>
登入後複製

我們來看看瀏覽器渲染的結果:

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

等一下!不是說 Shadow DOM 嗎?這和普通 DOM 有啥差別? ? ?

在Chrome 中,Elements 預設是不顯示內部實作的Shadow DOM 節點的,需要在設定中啟用:

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

注意:瀏覽器預設隱藏自身的Shadow DOM 實現,但如果是使用者透過腳本創造的Shadow DOM,是不會被隱藏的。

然後,我們就可以看到video 標籤的真面目了:

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

#在這裡,你可完全像調試普通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;
}
登入後複製

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

由於Shadow DOM 實際上也是DOM 的一種,所以在Shadow DOM 中還可以繼續嵌套Shadow DOM,就像上面那樣。

瀏覽器中還有很多 Element 都使用了 Shadow DOM 的形式來封裝,例如

由於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 中的

一樣看待,但是它不是,所以你不能使用 上的一些屬性,甚至它不是節點。

你可以透過 ShadowRoot 下面的 appendChild、querySelectorAll 之類的屬性或方法去操作整個 Shadow DOM 樹。

對於一個普通的Element,例如

,你可以透過呼叫它上面的attachShadow 方法來建立一個ShadowRoot(還有一個createShadowRoot 方法,已經過時不建議使用),attachShadow 接受一個物件進行初始化:{ mode: 'open' },這個物件有一個mode 屬性,它有兩個取值:'open' 和'closed',這個屬性是在創造ShadowRoot 的時候需要初始化提供的,並在建立ShadowRoot 之後成為一個只讀屬性。

mode: 'open' 和 mode: 'closed' 有什么区别呢?在调用 attachShadow 创建 ShadowRoot 之后,attachShdow 方法会返回 ShadowRoot 对象实例,你可以通过这个返回值去构造整个 Shadow DOM。当 mode 为 'open' 时,在用于创建 ShadowRoot 的外部普通节点(比如

)上,会有一个 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。

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

浏览器内部实现的 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 中的一个

你知道原生HTML元件是什麼嗎?原生HTML元件的介紹

通过 document-fragment 对象,就可以访问到 template 内部的节点了,通过 document.importNode 方法,可以将 document-fragment 对象创建一份副本,然后可以使用一切 DOM 属性方法替换副本中的模版内容,最终将其插入到 DOM 或是 Shadow DOM 中。

<div></div>
<template>
  <div></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 中定义一大批的

此时,我们就可以用到已经被废弃的 HTML Imports 了。虽然它已经被废弃了,但是未来会通过 ES6 Modules 的形式再进行支持,所以理论上也只是换个加载形式而已。

通过 HTML Imports,我们可以将

已经废弃的 HTML Imports 通过 标签实现,只要指定 rel="import" 就可以了,就像这样:,它可以接受 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 [&#39;awesome&#39;];
  }
}
登入後複製

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(&#39;my-element&#39;, class extends HTMLElement {
  ...
});
登入後複製

注册之后,我们就可以使用了,可以直接在 html 文档中写对应的标签,比如:,也可以通过 document.createElement('my-element') 来创建,用法与普通标签几乎完全一样。但要注意的是,虽然 html 标准中说部分标签可以不关闭或是自关闭(
或是
),但是只有规定的少数几个标签允许自关闭,所以,在 html 中写 Custom Elements 的节点时必须带上关闭标签。

由于 Custom Elements 是通过 JavaScript 来定义的,而一般 js 文件都是通过

作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板