Web Components 上手指南
Element(Vue)
Ant Design(React)
這些組件庫的出現(xiàn),讓我們可以直接使用已經(jīng)封裝好的組件,而且在開源社區(qū)的幫助下,出現(xiàn)了很多的模板項(xiàng)目( vue-element-admin、Ant Design Pro ),能讓我們快速的開始一個(gè)項(xiàng)目。
雖然 React、Vue 為我們的組件開發(fā)提供了便利,但是這兩者在組件的開發(fā)思路上,一個(gè)是自創(chuàng)的 JSX 語法,一個(gè)是特有的單文件模板的語法,兩者的目標(biāo)都是想提供一種組件的封裝方法。畢竟都有其原創(chuàng)的東西在里面,和我們剛開始接觸的 Web 基礎(chǔ)的 HTML、CSS、JS 的方式還是有些出入的。今天介紹的就是,通過 HTML、CSS、JS 的方式來實(shí)現(xiàn)自定義的組件,也是目前瀏覽器原生提供的方案:Web Components。
什么是 Web Components?
Web Components 本身不是一個(gè)單獨(dú)的規(guī)范,而是由一組DOM API 和 HTML 規(guī)范所組成,用于創(chuàng)建可復(fù)用的自定義名字的 HTML 標(biāo)簽,并且可以直接在你的 Web 應(yīng)用中使用。
代碼的復(fù)用一直都是我們追求的目標(biāo),在 JS 中可復(fù)用的代碼我們可以封裝成一個(gè)函數(shù),但是對(duì)于復(fù)雜的HTML(包括相關(guān)的樣式及交互邏輯),我們一直都沒有比較好的辦法來進(jìn)行復(fù)用。要么借助后端的模板引擎,要么借助已有框架對(duì) DOM API 的二次封裝,而 Web Components 的出現(xiàn)就是為了補(bǔ)足瀏覽器在這方面的能力。
如何使用 Web Components?
Web Components 中包含的幾個(gè)規(guī)范,都已在 W3C 和 HTML 標(biāo)準(zhǔn)中進(jìn)行了規(guī)范化,主要由三部分組成:
Custom elements(自定義元素):一組 JavaScript API,用來創(chuàng)建自定義的 HTML標(biāo)簽,并允許標(biāo)簽創(chuàng)建或銷毀時(shí)進(jìn)行一些操作;
Shadow DOM(影子DOM):一組 JavaScript API,用于將創(chuàng)建的 DOM Tree 插入到現(xiàn)有的元素中,且 DOM Tree 不能被外部修改,不用擔(dān)心元素被其他地方影響;
HTML templates(HTML模板):通過 <template>、<slot> 直接在 HTML 文件中編寫模板,然后通過 DOM API 獲取。
Custom elements(自定義元素)
瀏覽器提供了一個(gè)方法:customElements.define() , 來進(jìn)行自定義標(biāo)簽的定義。該方法接受三個(gè)參數(shù):
自定義元素的名稱,一個(gè) DOMString 標(biāo)準(zhǔn)的字符串,為了防止自定義元素的沖突,必須是一個(gè)帶短橫線連接的名稱(e.g. custom-tag)。
定義自定義元素的一些行為,類似于 React、Vue 中的生命周期。
擴(kuò)展參數(shù)(可選),該參數(shù)類型為一個(gè)對(duì)象,且需要包含 extends 屬性,用于指定創(chuàng)建的元素繼承自哪一個(gè)內(nèi)置元素(e.g. { extends: 'p' })。
下面通過一些例子,演示其用法,完整代碼放到了 JS Bin 上。
創(chuàng)建一個(gè)新的 HTML 標(biāo)簽
先看看如何創(chuàng)建一個(gè)全新的自定義元素。
class?HelloUser?extends?HTMLElement?{constructor()?{//?必須調(diào)用?super?方法super();//?創(chuàng)建一個(gè)?div?標(biāo)簽const?$box?=?document.createElement("p");let?userName?=?"User?Name";if?(this.hasAttribute("name"))?{//?如果存在?name?屬性,讀取?name?屬性的值userName?=?this.getAttribute("name");}//?設(shè)置?div?標(biāo)簽的文本內(nèi)容$box.innerText?=?`Hello?${userName}`;//?創(chuàng)建一個(gè)?shadow?節(jié)點(diǎn),創(chuàng)建的其他元素應(yīng)附著在該節(jié)點(diǎn)上const?shadow?=?this.attachShadow({?mode:?"open"?});shadow.appendChild($box);} }//?定義一個(gè)名為?<hello-user?/>?的元素 customElements.define("hello-user",?HelloUser); <hello-user?name="Shenfq"></hello-user>這時(shí)候頁面上就會(huì)生成一個(gè) <p> 標(biāo)簽,其文本內(nèi)容為:Hello Shenfq。這種形式的自定義元素被稱為:Autonomous custom elements,是一個(gè)獨(dú)立的元素,可以在 HTML 中直接使用。
擴(kuò)展已有的 HTML 標(biāo)簽
我們除了可以定義一個(gè)全新的 HTML 標(biāo)簽,還可以對(duì)已有的 HTML 標(biāo)簽進(jìn)行擴(kuò)展,例如,我們需要封裝一個(gè)與 <ul> 標(biāo)簽?zāi)芰︻愃频慕M件,就可以使用如下方式:
class?SkillList?extends?HTMLUListElement?{constructor()?{//?必須調(diào)用?super?方法super();if?(this.hasAttribute("skills")?&&this.getAttribute("skills").includes(','))?{//?讀取?skills?屬性的值const?skills?=?this.getAttribute("skills").split(',');skills.forEach(skill?=>?{const?item?=?document.createElement("li");item.innerText?=?skill;this.appendChild(item);})}} }//?對(duì)?<ul>?標(biāo)簽進(jìn)行擴(kuò)展 customElements.define("skill-list",?SkillList,?{?extends:?"ul"?}); <ul?is="skill-list"?skills="js,css,html"></ul>對(duì)已有的標(biāo)簽進(jìn)行擴(kuò)展,需要用到 customElements.define 方法的第三個(gè)參數(shù),且第二參數(shù)的類,也需要繼承需要擴(kuò)展標(biāo)簽的對(duì)應(yīng)的類。使用的時(shí)候,只需要在標(biāo)簽加上 is 屬性,屬性值為第一個(gè)參數(shù)定義的名稱。
生命周期
自定義元素的生命周期比較簡單,一共只提供了四個(gè)回調(diào)方法:
connectedCallback:當(dāng)自定義元素被插入到頁面的 DOM 文檔時(shí)調(diào)用。
disconnectedCallback:當(dāng)自定義元素從 DOM 文檔中被刪除時(shí)調(diào)用。
adoptedCallback:當(dāng)自定義元素被移動(dòng)時(shí)調(diào)用。
attributeChangedCallback: 當(dāng)自定義元素增加、刪除、修改自身屬性時(shí)調(diào)用。
下面演示一下使用方法:
class?HelloUser?extends?HTMLElement?{constructor()?{//?必須調(diào)用?super?方法super();//?創(chuàng)建一個(gè)?div?標(biāo)簽const?$box?=?document.createElement("p");let?userName?=?"User?Name";if?(this.hasAttribute("name"))?{//?如果存在?name?屬性,讀取?name?屬性的值userName?=?this.getAttribute("name");}//?設(shè)置?div?標(biāo)簽的文本內(nèi)容$box.innerText?=?`Hello?${userName}`;//?創(chuàng)建一個(gè)?shadow?節(jié)點(diǎn),創(chuàng)建的其他元素應(yīng)附著在該節(jié)點(diǎn)上const?shadow?=?this.attachShadow({?mode:?"open"?});shadow.appendChild($box);}connectedCallback()?{console.log('創(chuàng)建元素')//?5s?后移動(dòng)元素到?iframesetTimeout(()?=>?{const?iframe?=?document.getElementsByTagName("iframe")[0]iframe.contentWindow.document.adoptNode(this)},?5e3)}disconnectedCallback()?{console.log('刪除元素')}adoptedCallback()?{console.log('移動(dòng)元素')} } <!--?頁面插入一個(gè)?iframe,將自定義元素移入其中?--> <iframe?width="0"?height="0"></iframe> <hello-user?name="Shenfq"></hello-user>在元素被創(chuàng)建后,等待 5s,然后將自定義元素移動(dòng)到 iframe 文檔中,這時(shí)候能看到控制臺(tái)會(huì)同時(shí)出現(xiàn) 刪除元素、移動(dòng)元素 的 log。
ConsoleShadow DOM(影子DOM)
在前面介紹自定義元素的時(shí)候,已經(jīng)用到了 Shadow DOM。Shadow DOM 的作用是讓內(nèi)部的元素與外部隔離,讓自定義元素的結(jié)構(gòu)、樣式、行為不受到外部的影響。
我們可以看到前面定義的 <hello-user> 標(biāo)簽,在控制臺(tái)的 Elements 內(nèi),會(huì)顯示一個(gè) shadow-root ,表明內(nèi)部是一個(gè) Shadow DOM。
Shadow DOM其實(shí) Web Components 沒有提出之前,瀏覽器內(nèi)部就有使用 Shadow DOM 進(jìn)行一些內(nèi)部元素的封裝,例如 <video> 標(biāo)簽。我們需要現(xiàn)在控制臺(tái)的配置中,打開 Show user agent ashdow DOM 開關(guān)。
設(shè)置然后在控制臺(tái)的 Elements 內(nèi),就能看到 <video> 標(biāo)簽內(nèi)其實(shí)也有一個(gè) shadow-root。
video 標(biāo)簽創(chuàng)建 Shadow DOM
我們可以在任意一個(gè)節(jié)點(diǎn)內(nèi)部創(chuàng)建一個(gè) Shadow DOM,在獲取元素實(shí)例后,調(diào)用 Element.attachShadow() 方法,就能將一個(gè)新的 shadow-root 附加到該元素上。
該方法接受一個(gè)對(duì)象,且只有一個(gè) mode 屬性,值為 open 或 closed,表示 Shadow DOM 內(nèi)的節(jié)點(diǎn)是否能被外部獲取。
<div?id="root"></div> <script>//?獲取頁面的const?$root?=?document.getElementById('root');const?$p?=?document.createElement('p');$p.innerText?=?'創(chuàng)建一個(gè)?shadow?節(jié)點(diǎn)';const?shadow?=?$root.attachShadow({mode:?'open'});shadow.appendChild($p); </script> Shadow DOMmode 的差異
前面提到了 mode 值為 open 或 closed,主要差異就是是否可以使用 Element.shadowRoot 獲取到 shadow-root 進(jìn)行一些操作。
<div?id="root"></div> <script>//?獲取頁面的const?$root?=?document.getElementById('root');const?$p?=?document.createElement('p');$p.innerText?=?'創(chuàng)建一個(gè)?shadow?節(jié)點(diǎn)';const?shadow?=?$root.attachShadow({mode:?'open'});shadow.appendChild($p);console.log('is?open',?$div.shadowRoot); </script> open mode<div?id="root"></div> <script>//?獲取頁面的const?$root?=?document.getElementById('root');const?$p?=?document.createElement('p');$p.innerText?=?'創(chuàng)建一個(gè)?shadow?節(jié)點(diǎn)';const?shadow?=?$root.attachShadow({mode:?'closed'});shadow.appendChild($p);console.log('is?closed',?$div.shadowRoot); </script> closed modeHTML templates(HTML模板)
前面的案例中,有個(gè)很明顯的缺陷,那就是操作 DOM 還是得使用 DOM API,相比起 Vue 得模板和 React 的 JSX 效率明顯更低,為了解決這個(gè)問題,在 HTML 規(guī)范中引入了 <tempate> 和 <slot> 標(biāo)簽。
使用模板
模板簡單來說就是一個(gè)普通的 HTML 標(biāo)簽,可以理解成一個(gè) div,只是這個(gè)元素內(nèi)的所以內(nèi)容不會(huì)展示到界面上。
<template?id="helloUserTpl"><p?class="name">Name</p><a?target="blank"?class="blog">##</a> </template>在 JS 中,我們可以直接通過 DOM API 獲取到該模板的實(shí)例,獲取到實(shí)例后,一般不能直接對(duì)模板內(nèi)的元素進(jìn)行修改,要調(diào)用 tpl.content.cloneNode 進(jìn)行一次拷貝,因?yàn)轫撁嫔系哪0宀⒉皇且淮涡缘?#xff0c;可能其他的組件也要引用。
//?通過?ID?獲取標(biāo)簽 const?tplElem?=?document.getElementById('helloUserTpl'); const?content?=?tplElem.content.cloneNode(true);我們?cè)讷@取到拷貝的模板后,就能對(duì)模板進(jìn)行一些操作,然后再插入到 Shadow DOM 中。
<hello-user?name="Shenfq"?blog="http://blog.shenfq.com"?/><script>class?HelloUser?extends?HTMLElement?{constructor()?{//?必須調(diào)用?super?方法super();//?通過?ID?獲取標(biāo)簽const?tplElem?=?document.getElementById('helloUserTpl');const?content?=?tplElem.content.cloneNode(true);if?(this.hasAttribute('name'))?{const?$name?=?content.querySelector('.name');$name.innerText?=?this.getAttribute('name');}if?(this.hasAttribute('blog'))?{const?$blog?=?content.querySelector('.blog');$blog.innerText?=?this.getAttribute('blog');$blog.setAttribute('href',?this.getAttribute('blog'));}//?創(chuàng)建一個(gè)?shadow?節(jié)點(diǎn),創(chuàng)建的其他元素應(yīng)附著在該節(jié)點(diǎn)上const?shadow?=?this.attachShadow({?mode:?"closed"?});shadow.appendChild(content);}}//?定義一個(gè)名為?<hello-user?/>?的元素customElements.define("hello-user",?HelloUser); </script>添加樣式
<template> 標(biāo)簽中可以直接插入 <style> 標(biāo)簽在,模板內(nèi)部定義樣式。
<template?id="helloUserTpl"><style>:host?{display:?flex;flex-direction:?column;width:?200px;padding:?20px;background-color:?#D4D4D4;border-radius:?3px;}.name?{font-size:?20px;font-weight:?600;line-height:?1;margin:?0;margin-bottom:?5px;}.email?{font-size:?12px;line-height:?1;margin:?0;margin-bottom:?15px;}</style><p?class="name">User?Name</p><a?target="blank"?class="blog">##</a> </template>其中 :host 偽類用來定義 shadow-root的樣式,也就是包裹這個(gè)模板的標(biāo)簽的樣式。
占位元素
占位元素就是在模板中的某個(gè)位置先占據(jù)一個(gè)位置,然后在元素插入到界面上的時(shí)候,在指定這個(gè)位置應(yīng)該顯示什么。
<template?id="helloUserTpl"><p?class="name">User?Name</p><a?target="blank"?class="blog">##</a><!--占位符--><slot?name="desc"></slot>? </template><hello-user?name="Shenfq"?blog="http://blog.shenfq.com"><p?slot="desc">歡迎關(guān)注公眾號(hào):更了不起的前端</p> </hello-user>這里用的用法與 Vue 的 slot 用法一致,不做過多的介紹。
總結(jié)
到這里 Web Components 的基本用法就介紹得差不多了,相比于其他的支持組件化方案的框架,使用 Web Components 有如下的優(yōu)點(diǎn):
瀏覽器原生支持,不需要引入額外的第三方庫;
真正的內(nèi)部私有化的 CSS,不會(huì)產(chǎn)生樣式的沖突;
無需經(jīng)過編譯操作,即可實(shí)現(xiàn)的組件化方案,且與外部 DOM 隔離;
Web Components 的主要缺點(diǎn)就是標(biāo)準(zhǔn)可能還不太穩(wěn)定,例如文章中沒有提到的模板的模塊化方案,就已經(jīng)被廢除,現(xiàn)在還沒有正式的方案引入模板文件。而且原生的 API 雖然能用,但是就是不好用,要不然也不會(huì)出現(xiàn) jQuery 這樣的庫來操作 DOM。好在現(xiàn)在也有很多基于 Web Components 實(shí)現(xiàn)的框架,后面還會(huì)開篇文章專門講一講使用 Web Components 的框架 lit-html、lit-element。
好啦,今天的文章就到這里了,希望大家能有所收獲。
最近組建了一個(gè)江西人的前端交流群,如果你也是江西人可以加我微信ruochuan12 拉你進(jìn)群。
常駐推薦閱讀
若川知乎高贊:有哪些必看的 JS庫?
我在阿里招前端,我該怎么幫你?(現(xiàn)在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準(zhǔn)備阿里P6/P7前端面試--項(xiàng)目經(jīng)歷準(zhǔn)備篇
大廠面試官常問的亮點(diǎn),該如何做出?
如何從初級(jí)到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經(jīng)驗(yàn),做的項(xiàng)目沒什么技術(shù)含量,怎么辦?
常駐末尾
你好,我是若川,江西人~(點(diǎn)擊藍(lán)字了解我)歷時(shí)一年只寫了一個(gè)學(xué)習(xí)源碼整體架構(gòu)系列?有哪些必看的JS庫:jQuery、underscore、lodash、sentry、vuex、axios、koa、redux
關(guān)注若川視野,回復(fù)"pdf" 領(lǐng)取優(yōu)質(zhì)前端書籍pdf,回復(fù)"1",可加群長期交流學(xué)習(xí)
我的博客地址:https://lxchuan12.gitee.io?歡迎收藏
覺得文章不錯(cuò),可以?分享、點(diǎn)贊、在看?呀^_^另外歡迎留言交流~
小提醒:若川視野公眾號(hào)面試、源碼等文章合集在菜單欄中間【源碼精選】按鈕,歡迎點(diǎn)擊閱讀,也可以星標(biāo)我的公眾號(hào),便于查找
總結(jié)
以上是生活随笔為你收集整理的Web Components 上手指南的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原生js实现一个tab栏的标签操作
- 下一篇: air文件打包成exe