前言:這周完成了兩場(chǎng)技術(shù)分享會(huì)霎褐,下周還有一場(chǎng)眼虱,就完成了這階段的一個(gè)重大任務(wù)勒虾。分享會(huì)是關(guān)于 TS 的停撞,我這兩場(chǎng)分享會(huì)的主題分別是:
- TS 初級(jí)入門(mén)
- TS 高級(jí)語(yǔ)法
下周的主題是县踢,如何在 React 中優(yōu)雅的書(shū)寫(xiě) TS转绷。
我對(duì)技術(shù)也是有那么點(diǎn)喜歡,所以我平時(shí)喜歡學(xué)習(xí)些新技術(shù)硼啤,但是同時(shí)我認(rèn)為最好的學(xué)習(xí)议经,應(yīng)該是來(lái)自于實(shí)踐,所以除了大量做項(xiàng)目谴返,對(duì)剛學(xué)的技術(shù)最好的幫助就是分享煞肾,把別人給教會(huì),而這也是一種能力的體現(xiàn)嗓袱。所以我對(duì)分享并不是很排斥籍救,反而有種強(qiáng)烈的喜歡。
而且分享還能打破封閉索抓,對(duì)個(gè)人能力有很大的加成作用钧忽,不過(guò)難就難在跨出第一步毯炮,我剛開(kāi)始分享也是有點(diǎn)慌逼肯,但等到第二場(chǎng)就開(kāi)始駕輕就熟了,真的鼓勵(lì)大家要不斷的去嘗試桃煎,不要重復(fù)自己篮幢,要敢于突破自己。
Web Components 這個(gè)技術(shù)是我在 「TS 高級(jí)語(yǔ)法」主題分享前給團(tuán)隊(duì)小伙伴的一個(gè)開(kāi)胃小菜为迈。
以下正文:
前端組件化
無(wú)論你用什么流行框架去寫(xiě)前端三椿,本質(zhì)上你都是在使用前端三劍客即: HTML、CSS 和 JavaScript葫辐。那這三劍客在自己的領(lǐng)域組件化/模塊化
做的怎么樣了呢搜锰?
- 對(duì)于 CSS,我們有
@impot
耿战。 - 對(duì)于 JS 現(xiàn)在也有模塊化方案蛋叼。
那么對(duì)于 HTML 呢?我們知道樣式和腳本都是集成到 HTML 中剂陡,所以所以單獨(dú)的去做 HTML 模塊化狈涮,沒(méi)有任何意義。
既然如此鸭栖,我們看看 HTML 在編程過(guò)程中遇到了什么問(wèn)題歌馍。
- 因?yàn)?CSS 樣式作用在全局,就會(huì)造成樣式覆蓋晕鹊。
- 因?yàn)樵陧?yè)面中只有一個(gè) DOM松却,任何地方都可以直接讀取和修改 DOM暴浦。
可以看到我們的痛點(diǎn)就是解決 CSS 和 DOM 這兩個(gè)阻礙組件化的因素,于是 Web Components 孕育而生玻褪。
Web Components
Web Components 由三項(xiàng)主要技術(shù)組成:
Web Components 整體知識(shí)點(diǎn)不多同规,內(nèi)容也不復(fù)雜,我認(rèn)為核心就是 Shadow DOM(影子 DOM)窟社,為什么我這么認(rèn)為呢券勺?看下 Shadow DOM 的作用你就明白了:
- 影子 DOM 中的元素對(duì)于整個(gè)網(wǎng)頁(yè)是不可見(jiàn)的;
- 影子 DOM 的 CSS 不會(huì)影響到整個(gè)網(wǎng)頁(yè)的 CSSOM灿里,影子 DOM 內(nèi)部的 CSS 只對(duì)內(nèi)部的元素起作用关炼。
看完,你發(fā)沒(méi)發(fā)現(xiàn)它剛好解決了匣吊,我們開(kāi)頭前端組件遇到的問(wèn)題儒拂,所以 Shadow DOM 才是 Web Components 的核心。
自定義元素(Custom elements)
如何自定義元素或叫如何自定義標(biāo)簽
自定義元素就像 Vue 和 React 中的類(lèi)組件色鸳,首先我們需要使用 ES2015 語(yǔ)法來(lái)定義一個(gè)類(lèi)社痛,接著,使用瀏覽器原生的 customElements.define() 方法命雀,告訴瀏覽器我要注冊(cè)一個(gè)元素/標(biāo)簽 user-text
蒜哀,(自定義元素的名稱(chēng)必須包含連詞線,用與區(qū)別原生的 HTML 元素吏砂,就像 React 的自定義組件名使用時(shí)必須大寫(xiě)一樣)撵儿。
class UserText extends HTMLElement {
constructor() {
super();
}
}
上面代碼中,UserText 是自定義元素的類(lèi)狐血,這個(gè)類(lèi)繼承了 HTMLElement 父類(lèi)淀歇。
我們現(xiàn)在把 user-text
作為標(biāo)簽使用,放到頁(yè)面上去:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<user-text></user-text>
<script>
class UserText extends HTMLElement {
constructor () {
super();
this.innerHTML = "我是內(nèi)容";
}
}
globalThis.customElements.define("user-text", UserText);
</script>
</body>
</html>
我們看到頁(yè)面成功渲染:
組件會(huì)有生命周期匈织,所以這個(gè)類(lèi)還有些方法:
- connectedCallback:當(dāng) custom element 首次被插入文檔 DOM 時(shí)浪默,被調(diào)用,俗稱(chēng)組件上樹(shù)报亩。
- disconnectedCallback:當(dāng) custom element 從文檔 DOM 中刪除時(shí)浴鸿,被調(diào)用,俗稱(chēng)組件下樹(shù)或組件消亡弦追。
- adoptedCallback:當(dāng) custom element 被移動(dòng)到新的文檔時(shí)岳链,被調(diào)用,這個(gè) API 常和 document.adoptNode 配合使用约急。
- attributeChangedCallback: 當(dāng) custom element 增加、刪除苗分、修改自身屬性時(shí)厌蔽,被調(diào)用,俗稱(chēng)組件更新摔癣。
模板 (Templates)
頁(yè)面上的元素最終是要給用戶(hù)呈現(xiàn)內(nèi)容奴饮,在自定義組件里,我們通過(guò)字符串的方式來(lái)接受要展現(xiàn)給用戶(hù)的內(nèi)容择浊,這種方式非常不利于組織我們的 HTML戴卜,我們需要一個(gè)寫(xiě) HTML 的地方,這個(gè)技術(shù)就是模板 (Templates)琢岩,非常像 Vue 的模版渲染投剥,如果你熟悉 Vue ,完全可以無(wú)障礙切換担孔。
我們隨便來(lái)弄點(diǎn)數(shù)據(jù)組織下代碼江锨,在瀏覽器展示給用戶(hù):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<template id="user-text-template">
你好,我是模版糕篇!
</template>
<user-text></user-text>
<script>
class UserText extends HTMLElement {
constructor () {
super();
}
connectedCallback () {
const oldNode = document.getElementById("user-text-template").content;
const newNode = oldNode.cloneNode(true);
this.appendChild(newNode);
}
}
globalThis.customElements.define("user-text", UserText);
</script>
</body>
</html>
我們看到頁(yè)面成功渲染:
如果啄育,自定義元素需要?jiǎng)討B(tài)傳值給我們的自定義組件,可以使用插槽 slot娩缰,語(yǔ)法基本同 Vue灸撰,但是此時(shí)還無(wú)法演示谒府,因?yàn)?slot 標(biāo)簽對(duì)標(biāo)準(zhǔn)的 DOM(更專(zhuān)業(yè)點(diǎn)叫 light DOM)無(wú)效拼坎,只對(duì) shadow DOM 是有效的,看下使用示例完疫。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<template id="user-text-template">
<style>
p {
color: red;
}
</style>
<p id="templateDOM">你好泰鸡,我是模版!</p>
<p><slot>因?yàn)槲沂菬o(wú)效的壳鹤,我也會(huì)默認(rèn)展示</slot></p>
</template>
<user-text>
<p>light DOM 環(huán)境下盛龄,slot 標(biāo)簽沒(méi)用</p>
</user-text>
<script>
class UserText extends HTMLElement {
constructor () {
super();
}
connectedCallback () {
const oldNode = document.getElementById("user-text-template").content;
const newNode = oldNode.cloneNode(true);
this.appendChild(newNode);
}
}
globalThis.customElements.define("user-text", UserText);
console.log(document.getElementById("templateDOM"));
</script>
</body>
</html>
看下頁(yè)面加載顯示:
除了,slot 無(wú)法使用芳誓,我們還觀察到 template 元素及其內(nèi)容不會(huì)在 DOM 中呈現(xiàn)余舶,必須通過(guò) JS 的方式去訪問(wèn)、style 標(biāo)簽內(nèi)的樣式是作用到全局的锹淌、template 里面的 DOM 也可以被全局訪問(wèn)匿值。
影子 DOM(shadow DOM)
影子 DOM 是 Web Components 核心中的核心,可以一舉解決我們前面提到的赂摆,CSS 和 DOM 作用全局的問(wèn)題挟憔。
看下使用示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<template id="user-text-template">
<style>
p {
color: red;
}
</style>
<p id="templateDOM">你好钟些,我是模版!</p>
<p><slot>因?yàn)槲沂菬o(wú)效的绊谭,我也會(huì)默認(rèn)展示</slot></p>
</template>
<user-text>
<p>light DOM 環(huán)境下政恍,slot 標(biāo)簽沒(méi)用</p>
</user-text>
<p>測(cè)試 shadow DOM 樣式不作用全局</p>
<script>
class UserText extends HTMLElement {
constructor () {
super();
}
connectedCallback () {
this.attachShadow({ mode: "open" });
const oldNode = document.getElementById("user-text-template").content;
const newNode = oldNode.cloneNode(true);
this.shadowRoot.appendChild(newNode);
}
}
globalThis.customElements.define("user-text", UserText);
console.log(document.getElementById("templateDOM"));
</script>
</body>
</html>
現(xiàn)在完成了,組件的樣式應(yīng)該與代碼封裝在一起达传,只對(duì)自定義元素生效篙耗,不影響外部的全局樣式、DOM 默認(rèn)與外部 DOM 隔離宪赶,內(nèi)部任何代碼都無(wú)法影響外部鹤树,同時(shí) slot 也生效了,看下頁(yè)面加載顯示:
影子 DOM 的 mode 參數(shù)除了有 open逊朽,之外還有 closed罕伯,兩者的區(qū)別在于此影子 DOM 是否能被訪問(wèn)外界訪問(wèn),即是否能通過(guò) JS 獲取影子 DOM 讀取 影子 DOM 里面的內(nèi)容叽讳。
style 穿越 影子 DOM
任何項(xiàng)目為了統(tǒng)一風(fēng)格追他,肯定需要有公共樣式,而且為了方面是統(tǒng)一引入的岛蚤,這就涉及到外部樣式影響到內(nèi)部樣式邑狸,那怎么突破影子 DOM 呢?
CSS 變量
可以使用 CSS 變量來(lái)穿透 DOM:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS 變量樣式穿透</title>
<style>
[type="primary"] {
--ui-button-border: 1px solid transparent;
--ui-button-background: deepskyblue;
--ui-button-color: #fff;
}
</style>
</head>
<body>
<template id="ui-button-template">
<style>
button {
cursor: pointer;
padding: 9px 1em;
border: var(--ui-button-border, 1px solid #ccc);
border-radius: var(--ui-button-radius, 4px);
background-color: var(--ui-button-background, #fff);
color: var(--ui-button-color, #333);
}
</style>
<button ><slot></slot></button>
</template>
<ui-button type="primary">按鈕</ui-button>
<script>
class UiButton extends HTMLElement {
constructor () {
super();
}
connectedCallback () {
this.attachShadow( { mode: "open" });
const oldNode = document.getElementById("ui-button-template").content;
const newNode = oldNode.cloneNode(true);
this.shadowRoot.appendChild(newNode);
}
}
globalThis.customElements.define("ui-button", UiButton);
</script>
</body>
</html>
頁(yè)面展示效果圖:
::part 偽元素
::part 偽元素的用法有點(diǎn)像具名插槽 slot涤妒。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>::part 樣式穿透</title>
<style>
[type="primary"]::part(button) {
cursor: pointer;
padding: 9px 1em;
border: 1px solid #ccc;
border-radius: 4px;
background-color: skyblue;;
color: #987;
}
</style>
</head>
<body>
<template id="ui-button-template">
<button part="button"><slot></slot></button>
</template>
<ui-button type="primary">按鈕</ui-button>
<script>
class UiButton extends HTMLElement {
constructor () {
super();
}
connectedCallback () {
this.attachShadow( { mode: "open" });
const oldNode = document.getElementById("ui-button-template").content;
const newNode = oldNode.cloneNode(true);
this.shadowRoot.appendChild(newNode);
}
}
globalThis.customElements.define("ui-button", UiButton);
</script>
</body>
</html>
HTML 原生組件支持 Web Components
我們知道 HTML5 有很多的原生組件单雾,例如:input,video她紫,textarea硅堆,select,audio
等贿讹。
如果你審查元素會(huì)發(fā)現(xiàn)渐逃,這個(gè)組件并不是純正的原生組件,而是基于 Web Components 來(lái)封裝的民褂。
如果你審查元素沒(méi)有顯示影子 DOM茄菊,請(qǐng)打開(kāi)控制臺(tái),同時(shí)檢查瀏覽器設(shè)置 Settings -> Preferences -> Elements
中把 Show user agent shadow DOM
打上勾赊堪。
落地應(yīng)用有哪些面殖?
首先,github 網(wǎng)址是完全基于 Web Components 來(lái)開(kāi)發(fā)的哭廉,其次 Vue 和 小程序 也是基于 Web Components 來(lái)做組件化的脊僚,而且 Web Components 作為最底層的技術(shù)完全可配合 Vue 和 React 等框架,直接使用的群叶。
光學(xué)不練那不是假把式嗎吃挑,我來(lái)給大家整個(gè) demo钝荡,自定義一個(gè)對(duì)話框,這個(gè)對(duì)話框只滿(mǎn)足最基本的使用需求舶衬,先看下最終的成品埠通。
源代碼,可能比較難得兩個(gè)思路:
- 數(shù)據(jù)更新逛犹,采用的是類(lèi)的 get 和 set
- 關(guān)閉的回調(diào)事件端辱,用的是自定義事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定義彈框</title>
</head>
<body>
<style>
.open-button {
cursor: pointer;
padding: 9px 1em;
border: 1px solid transparent;
border-radius: 4px;
background-color: deepskyblue;
color: #fff;
}
ul > li {
margin: 20px;
}
</style>
<section>
<ul>
<li><button id="launch-dialog-one" class="open-button">open-one</button>
<li><button id="launch-dialog-two" class="open-button">open-two</button></li>
<li><button id="launch-dialog-three" class="open-button">open-three</button></li></li>
</ul>
</section>
<shanshu-dialog title="title-one" id="shanshu-dialog-one">
<span slot="my-text">Let's have some different text!</span>
<p>Some contents Some contents......</p>
<p>Some contents Some contents......</p>
<p>Some contents Some contents......</p>
</shanshu-dialog>
<shanshu-dialog title="title-two" id="shanshu-dialog-two">
<span slot="my-text">Let's have some different text!</span>
<p>Some contents Some contents......</p>
<p>Some contents Some contents......</p>
<p>Some contents Some contents......</p>
</shanshu-dialog>
<shanshu-dialog title="title-three" id="shanshu-dialog-three">
<span slot="my-text">Let's have some different text!</span>
<p>Some contents Some contents......</p>
<p>Some contents Some contents......</p>
<p>Some contents Some contents......</p>
</shanshu-dialog>
<template id="shanshu-dialog-template">
<style>
.wrapper {
opacity: 0;
transition: visibility 0s, opacity 0.25s ease-in;
}
.wrapper:not(.open) {
visibility: hidden;
}
.wrapper.open {
align-items: center;
display: flex;
justify-content: center;
height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 1;
visibility: visible;
}
.overlay {
background: rgba(0, 0, 0, 0.3);
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
}
.dialog {
background: #ffffff;
max-width: 600px;
min-width: 400px;
text-align: center;
padding: 1rem;
position: fixed;
border-radius: 4px;
}
button {
all: unset;
cursor: pointer;
font-size: 1.25rem;
position: absolute;
top: 1rem;
right: 1rem;
}
button:focus {
border: 1px solid skyblue;
}
h1 {
color: #4c5161;
}
.content {
color: #34495e;
position: relative;
}
.btn {
background: none;
outline: 0;
border: 0;
position: absolute;
right: 1em;
top: 1em;
width: 20px;
height: 20px;
padding: 0;
user-select: none;
cursor: unset;
}
.btn::before {
content: "";
display: block;
border: 1px solid green;
height: 20px;
width: 0;
border-radius: 2px;
/*transition: .1s;*/
transform: translate(9px) rotate(45deg);
background: #fff;
}
.btn::after {
content: "";
display: block;
border: 1px solid green;
height: 20px;
border-radius: 2px;
width: 0;
/*transition: .1s;*/
transform: translate(9px, -100%) rotate(-45deg);
background: #fff;
}
</style>
<div class="wrapper">
<div class="overlay"></div>
<div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="content">
<button aria-label="Close" class="btn"></button>
<h1 id="title">Hello world</h1>
<div id="content" class="content">
<slot></slot>
<slot name="my-text"></slot>
</div>
</div>
</div>
</template>
<script type="text/javascript">
"use strict";
class ShanshuDialog extends HTMLElement {
static get observedAttributes() {
return ["open"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.close = this.close.bind(this);
}
connectedCallback() {
const { shadowRoot } = this;
const templateElem = document.getElementById("shanshu-dialog-template");
const oldNode = templateElem.content;
// const newNode = oldNode.cloneNode(true);
const newNode = document.importNode(oldNode, true);
shadowRoot.appendChild(newNode);
shadowRoot.getElementById("title").innerHTML = this.title;
shadowRoot.querySelector("button").addEventListener("click", this.close);
shadowRoot.querySelector(".overlay").addEventListener("click", this.close);
}
disconnectedCallback() {
this.shadowRoot.querySelector("button").removeEventListener("click", this.close);
this.shadowRoot.querySelector(".overlay").removeEventListener("click", this.close);
}
get open() {
return this.hasAttribute("open");
}
set open(isOpen) {
console.log("isOpen", isOpen);
const { shadowRoot } = this;
shadowRoot.querySelector(".wrapper").classList.toggle("open", isOpen);
shadowRoot.querySelector(".wrapper").setAttribute("aria-hidden", !isOpen);
if (isOpen) {
this._wasFocused = document.activeElement;
this.setAttribute("open", false);
this.focus();
shadowRoot.querySelector("button").focus();
} else {
this._wasFocused && this._wasFocused.focus && this._wasFocused.focus();
this.removeAttribute("open");
this.close();
}
}
close() {
this.open !== false && (this.open = false);
const closeEvent = new CustomEvent("dialog-closed");
this.dispatchEvent(closeEvent);
}
}
customElements.define("shanshu-dialog", ShanshuDialog);
const buttonOneDOM = document.getElementById("launch-dialog-one");
const buttonTwoDOM = document.getElementById("launch-dialog-two");
const buttonThreeDOM = document.getElementById("launch-dialog-three");
const shanshuDialogOne = document.querySelector("#shanshu-dialog-one");
buttonOneDOM.addEventListener("click", () => {
document.querySelector("#shanshu-dialog-one").open = true;
});
shanshuDialogOne.addEventListener("dialog-closed", () => {
alert("對(duì)話框關(guān)閉回調(diào)函數(shù)");
});
buttonTwoDOM.addEventListener("click", () => {
document.querySelector("#shanshu-dialog-two").open = true;
});
buttonThreeDOM.addEventListener("click", () => {
document.querySelector("#shanshu-dialog-three").open = true;
});
</script>
</body>
</html>
組件庫(kù)
當(dāng)我們談到在項(xiàng)目中如何應(yīng)用,我們首先需要兩個(gè)東西虽画,選個(gè) UI 組件庫(kù)舞蔽,同時(shí)有比較好的工具來(lái)操作這個(gè) UI 庫(kù),我提供兩個(gè)給你參考码撰。