這篇文章帶給大家的內容是關於你知道原生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 中的 標籤,正常情況下它無色無味,感知不到它的存在,甚至它下面的img都不會被下載,script 都不會被執行。 就如它的名字一樣,它只是一個模版,只有到你用到它時,它才會變得有意義。
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></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 的形式來封裝,例如 、
由於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。
浏览器内部实现的 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 中的一个 标签,正常情况下,这个标签下的内容是不会被渲染的,包括标签下的 img、style、script 等都是不会被加载或执行的。你可以在脚本中使用 getElementById 之类的方法得到 标签对应的节点,但是却无法直接访问到其内部的节点,因为默认他们只是模版,在浏览器中表现为 #document-fragment,字面意思就是“文档片段”,可以通过节点对象的 content 属性来访问到这个 document-fragment 对象。
通过 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 文档中,然后再在需要的 html 文档中进行导入(当然也可以通过脚本按需导入),导入后,我们就可以直接使用其中定义的模版节点了。
已经废弃的 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 ['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 文档中写对应的标签,比如:
或是
),但是只有规定的少数几个标签允许自关闭,所以,在 html 中写 Custom Elements 的节点时必须带上关闭标签。
由于 Custom Elements 是通过 JavaScript 来定义的,而一般 js 文件都是通过