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