ss樣式的書寫順序及原理——很重要!很重要!很重要!
為什么重要???
概括講就是,它涉及了瀏覽器的渲染原理:reflow和repaint
還想知道更多為什么可以參考:https://blog.csdn.net/qq_36060786/article/details/79311244
如何優(yōu)雅解決?
開源比較流行的 CSS lint 方案:Csslint、SCSS-Lint、Stylelint
Stylelint 優(yōu)點(diǎn)
npm install stylelint stylelint-config-recess-order stylelint-config-standard stylelint-order stylelint-scss
stylelint-scss
scss 拓展,增加支持 scss 語法
stylelint-order
該插件的作用是強(qiáng)制你按照某個順序編寫 css。例如先寫定位,再寫盒模型,再寫內(nèi)容區(qū)樣式,最后寫 CSS3 相關(guān)屬性。這樣可以極大的保證我們代碼的可讀性。
stylelint-config-standard
作用:配置 Stylelint 規(guī)則。
官方的代碼風(fēng)格 :stylelint-config-standard。該風(fēng)格是 Stylelint 的維護(hù)者汲取了 GitHub、Google、Airbnb 多家之長生成的。
stylelint-config-recess-order
stylelint-order 插件的第三方配置
配置方式:
按順序查找,以上三種方式任何一項(xiàng)有值,就會結(jié)束查找
在項(xiàng)目根目錄新建.stylelintrc.json 文件,復(fù)制以下內(nèi)容
module.exports={
"extends": ["stylelint-config-standard", "stylelint-config-recess-order"],
"rules": {
"at-rule-no-unknown": [true, {"ignoreAtRules" :[
"mixin", "extend", "content", "include"
]}],
"indentation": 4,
"no-descending-specificity": null // 禁止特異性較低的選擇器在特異性較高的選擇器之后重寫
}
}
rules 優(yōu)先級大于 extends,建議采用 extends 方式統(tǒng)一管理
配置文件中單獨(dú)配置 at-rule-no-unknown 是為了讓 Stylelint 支持 SCSS 語法中的 mixin、extend、content 語法。
片段禁用規(guī)則
/* stylelint-disable */
/* (請說明禁止檢測的理由)前端組件限制類名 */
.cropper_topContainer .img-preview {
border: 0 none;
}
/* stylelint-enable */
fix 方式
更多語法規(guī)則請參考:https://stylelint.io/user-guide/usage/cli
如果希望在保存時(shí)自動 fix, 參考這里https://blog.csdn.net/gyz718/article/details/70556188
在應(yīng)用商店搜索擴(kuò)展并安裝:stylelint-plus、Vetur、Beautify
"stylelint.autoFixOnSave": true, // 保存自動格式化
"vetur.format.defaultFormatter.html": "js-beautify-html", // 格式化模板
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
"wrap_attributes": "force-aligned", // 第一個屬性后開始折行,并對齊
},
在 vscode 中安裝插件:stylelint-plus
當(dāng)然也可以選擇普通的 stylelint 插件,不過 plus 版本有保存即 fix 的功能
使用
css 格式化:ctrl + s 自動修復(fù)
OM 元素經(jīng)常會動態(tài)地綁定一些 class 類名或 style 樣式。
v-bind 指令的主要用法是動態(tài)更新 HTML 元素上的屬性,可使用 v-bind 指令綁定 class 和 style 。如下,示例:
<div id="app"> <a v-bind:href="url">我是一個鏈接</a> <img v-bind:src="imgUrl"> <!-- 縮寫為 --> <a :href="url">我是一個鏈接</a> <img :src="imgUrl"> </div> <script> var app=new Vue({ el: "#app", data: { url: 'https://www.github.com', imgUrl: 'http://bingbinlee.com/img.pang' } }) </script>
鏈接的 href 屬性和圖片的 src 屬性都被動態(tài)設(shè)置了,當(dāng)數(shù)據(jù)變化時(shí),就會重新渲染。
在數(shù)據(jù)綁定中,最常見的兩個需求就是元素的樣式名稱 class 和內(nèi)聯(lián)樣式 style 的動態(tài)綁定,它們也是 HTML 屬性,因此可以使用 v-bind 指令。只需要用 v-bind 計(jì)算出表達(dá)式最終的字符串也可以,不過有時(shí)候表達(dá)式的邏輯較為復(fù)雜,使用字符串拼接方法較難閱讀和維護(hù),所以 Vue.js 增強(qiáng)了對 class 和 sytle 的綁定。
2.1 對象語法
給 v-bind:class 設(shè)置一個對象,可以動態(tài)地切換 class ,例如:
<div id="app"> <div :class="{ 'active': isActive }"></div> </div> <script> var app=new Vue({ el: "#app", data: { isActive: true } }) </script>
上面示例中,類名 active 依賴于數(shù)據(jù) isActive, 當(dāng)其為 true 時(shí),div 會擁有類名 Active,為 false 時(shí)則沒有,所以上例最終渲染完的結(jié)果是:
<div class="active"></div>
對象中也可以傳入多個屬性,來動態(tài)切換 class 。另外,:class 可以與普通 class 共存,例如:
<div id="app"> <div class="static" :class="{ 'active': isActive, 'error': isError}"></div> </div> <script> var app=new Vue({ el: '#app', data: { isActive: true, isError: false } }) </script>
:class 內(nèi)的表達(dá)式每項(xiàng)為真時(shí),對應(yīng)的類名就會加載,上面渲染后的結(jié)果為:
<div class="ststic active"></div>
當(dāng)數(shù)據(jù) isActive 或 isError 變化時(shí),對應(yīng)的 class 類名也會更新。比如當(dāng) isError 為 ture 時(shí),渲染后的結(jié)果為:
<div class="static active error"></div>
當(dāng) :class 的表達(dá)式過長或邏輯復(fù)雜時(shí),還可以綁定一個計(jì)算屬性,這是一種很友好和常見的用法,一般當(dāng)條件多于兩個時(shí),都可以使用 data 或 computed ,例如使用計(jì)算屬性:
<div id="app"> <div :class="classes"></div> </div> <script> var app=new Vue({ el: "#app", data: { isActive: true, error: null }, computed: { classes: function () { return { active: this.isActive && !this.error, 'text-fail': this.error && this.error.type==='fail' } } } }) </script>
除了計(jì)算屬性,也可以直接綁定一個 Object 類型得數(shù)據(jù),或者使用類似計(jì)算屬性的 methods 。
2.2 數(shù)組語法
當(dāng)需要應(yīng)用多個 class 時(shí),可以使用數(shù)組語法,給 :class 綁定一個數(shù)組,應(yīng)用一個 class 列表:
<div id="app"> <div :class="[activeCls, errorCls]"></div> </div> <script> var app=new Vue ({ el: "#app", data: { activeCls: 'active', errorCls: 'error' } }) </script>
渲染后的結(jié)果為:
<div class="active error"></div>
也可以使用三元表達(dá)式來根據(jù)條件切換 class ,例如下面的示例:
<div id="app"> <div :class="[isActive ? activeCls : '', errorCls]"></div> </div> <script> var app=new Vue ({ el: '#app', data: { isActive: true, activeCls: 'active', errorCls: 'error' } }) </script>
樣式 error 會始終應(yīng)用,當(dāng)數(shù)據(jù) isActive 為真時(shí),樣式 active 才會被應(yīng)用。class 有多個條件時(shí),這樣寫較為煩瑣,可以在數(shù)組語法中使用對象語法:
<div id="app"> <div :class="[{ 'active': isActive}, errorCls]"></div> </div> <script> var app=new Vue ({ el: "#app", data: { isActive: ture, errorCls: 'error' } }) </script>
當(dāng)然,與對象語法一樣,也可以使用 data、computed 和 methods 三種方法,以計(jì)算屬性為例:
<div id="app"> <button :class="classes"></button> </div> <script> var app=new Vue ({ el: '#app', data: { size: 'large', disabled: true }, computed: { classes: function () { return [ 'btn', { ['btn-' + this.size]: this.size !=='', ['btn-disabled']: this.disabled } ]; } } }) </script>
示例中的樣式 btn 會始終應(yīng)用,當(dāng)數(shù)據(jù) size 不為空時(shí),會應(yīng)用樣式前綴 btn- ,后加 size 的值;當(dāng)數(shù)據(jù) disabled 為真時(shí),會應(yīng)用樣式 btn-disabled,所以該示例最終渲染的結(jié)果為:
<button class="btn btn-large btn-disabled"></button>
使用計(jì)算屬性給元素動態(tài)設(shè)置類名,在業(yè)務(wù)中經(jīng)常用到,尤其是在寫復(fù)用的組件時(shí),所以在開發(fā)過程中,如果表達(dá)式較長或邏輯復(fù)雜,應(yīng)該盡可能地優(yōu)先使用計(jì)算屬性。
如果直接在組件上使用 class 或 :class ,樣式規(guī)則會直接應(yīng)用到這個組件的根元素上,例如聲明一個簡單的組件:
Vue.component('my-component', { template: '<p class="article">我是個文本</p>' });
然后在調(diào)用這個組件時(shí),應(yīng)用上面的對象語法或數(shù)組語法給組件綁定 class ,以對象語法為例:
<div id="app"> <my-component :class="{ 'active': isActive }"></my-component> </div> <script> var app=new Vue ({ el: "#app", data: { isActive: true } }) </script>
最終組件渲染后的結(jié)果為:
<p class="article active">我是個文本</p>
這種用法僅適用于自定義組件的最外層是一個根元素,否則會無效,當(dāng)不滿足這種條件或需要給具體的子元素設(shè)置類名時(shí),應(yīng)當(dāng)使用組件的 props 來傳遞(這些用法同樣適用于綁定內(nèi)聯(lián)樣式 style 的內(nèi)容)。
使用 v-bind:style (即 :style)可以給元素綁定內(nèi)聯(lián)樣式,方法與 :class 類似,也有對象語法和數(shù)組語法,看起來很像直接在元素上寫 CSS :
<div id="app"> <div :style="{ 'color': color, 'fontSize': fontSize + 'px'}">我是個文本</div> </div> <script> var app=new Vue ({ el: "#app", data: { color: 'red', fontSize: 14 } }) </script>
CSS 屬性名稱使用駝峰命名 (camelCase)或短橫分割命名(kebab-case),渲染后的結(jié)果為:
<div style="color: red; font-size: 14px;">紅色文本</div>
大多數(shù)情況下,直接寫一長串的樣式不便于閱讀和維護(hù),所以一般寫在 data 或 computed 里,以 data 為例改寫下面的示例:
<div id="app"> <div :style="styles">我是一個紅色的文本</div> </div> <script> var app=new Vue ({ el: "#app", data: { styles: { color: 'red', fontSize: 14 + 'px' } } }) </script>
應(yīng)用多個樣式對象時(shí),可以使用數(shù)組語法:
<div :style="[styleA, styleB]">文本</div>
在實(shí)際業(yè)務(wù)中,:style 的數(shù)組語法并不常用,因?yàn)橥梢詫懺谝粋€對象里面;而較為常用的應(yīng)當(dāng)時(shí)計(jì)算屬性。
另外,使用 :style 時(shí),Vue.js 會自動給特殊的 CSS 屬性名稱增加前綴,比如 transform 。
在筆記本電腦屏幕上的概念
言
Web Component蠻早就有出現(xiàn)過,有時(shí)候需要時(shí)間。作者:騰訊@林林小輝。
還記得當(dāng)document.querySelector最開始被廣泛的被瀏覽器支持并且結(jié)束了無處不在的JQuery。這最終給我們提供了一個原生的方法,雖然JQuery已經(jīng)提供了很久。我覺得這同樣將會發(fā)生在像Angular和React這的前端框架身上。
這些框架可以幫助我們?nèi)プ鲆恍┳霾坏降氖虑椋热鐒?chuàng)建可以復(fù)用的前端組件,但是這樣需要付出復(fù)雜度、專屬語法、性能消耗的代價(jià)。 但是這些將會得到改變。
現(xiàn)代瀏覽器的API已經(jīng)更新到你不需要使用一個框架就可以去創(chuàng)建一個可復(fù)用的組件。Custom Element和Shadow DOM都可以讓你去創(chuàng)造可復(fù)用的組件。
最早在2011年,Web Components就已經(jīng)是一個只需要使用HTML、CSS、JavaScript就可以創(chuàng)建可復(fù)用的組件被介紹給大家。這也意味著你可以不使用類似React和Angular的框架就可以創(chuàng)造組件。甚至,這些組件可以無縫的接入到這些框架中。
這么久以來第一次,我們可以只使用HTML、CSS、JavaScript來創(chuàng)建可以在任何現(xiàn)代瀏覽器運(yùn)行的可復(fù)用組件。Web Components現(xiàn)在已經(jīng)被主要的瀏覽器的較新版本所支持。
Edge將會在接下來的19版本提供支持。而對于那些舊的版本可以使用 polyfill兼容至IE11. 這意味著你可以在當(dāng)下基本上任何瀏覽器甚至移動端使用Web Components。
創(chuàng)造一個你定制的HTML標(biāo)簽,它將會繼承HTM元素的所有屬性,并且你可在任何支持的瀏覽器中通過簡單的引入一個script。所有的HTML、CSS、JavaScript將會在組件內(nèi)部局部定義。 這個組件在你的瀏覽器開發(fā)工具中顯示為一個單獨(dú)個HTML標(biāo)簽,并且它的樣式和行為都是完全在組件內(nèi)進(jìn)行,不需要工作區(qū),框架和一些前置的轉(zhuǎn)換。
讓我們來看一些Web Components的一些主要功能。
自定義元素
自定義元素是簡單的用戶自定義HTML元素。它們通過使用CustomElementRegistry來定義。要注冊一個新的元素,通過window.customElements中一個叫define的方法來獲取注冊的實(shí)例。
window.customElements.define('my-element', MyElement);
方法中的第一個參數(shù)定義了新創(chuàng)造元素的標(biāo)簽名,我們可以非常簡單的直接使用
<my-element></my-element>
為了避免和native標(biāo)簽沖突,這里強(qiáng)制使用中劃線來連接。 這里的MyElement的構(gòu)造函數(shù)需要使用ES6的class,這讓JavaScript的class不像原來面向?qū)ο骳lass那么讓人疑惑。同樣的,如果一個Object和Proxy可以被使用來給自定義元素進(jìn)行簡單的數(shù)據(jù)綁定。但是,為了保證你的原生HTML元素的拓展性并保證元素繼承了整個DOM API,需要使用這個限制。 讓我們寫一個這個自定義元素class
class MyElement extends HTMLElement { constructor() { super(); } connectedCallback() { // here the element has been inserted into the DOM }}
這個自定義元素的class就好像一個常規(guī)的繼承自nativeHTML元素的class。在它的構(gòu)造函數(shù)中有一個叫connectedCallback額外添加的方法,當(dāng)這個元素被插入DOM樹的時(shí)候?qū)|發(fā)這個方法。你可以把這個方法與React的componentDidMount方法。
通常來說,我們需要在connectedCallback之后進(jìn)行元素的設(shè)置。因?yàn)檫@是唯一可以確定所有的屬性和子元素都已經(jīng)可用的辦法。構(gòu)造函數(shù)一般是用來初始化狀態(tài)和設(shè)置Shadow DOM。
元素的構(gòu)造函數(shù)和connectCallback的區(qū)別是,當(dāng)時(shí)一個元素被創(chuàng)建時(shí)(好比document.createElement)將會調(diào)用構(gòu)造函數(shù),而當(dāng)一個元素已經(jīng)被插入到DOM中時(shí)會調(diào)用connectedCallback,例如在已經(jīng)聲明并被解析的文檔中,或者使用document.body.appendChild添加。
你同樣可以用過調(diào)用customElements.get(‘my-element’)來獲取這個元素構(gòu)造函數(shù)的引用,從而構(gòu)造元素。前提是你已經(jīng)通過customElement.define()去注冊。然后你可以使用new element()來代替document.createElement()去實(shí)例一個元素。
customElements.define('my-element', class extends HTMLElement {...});...const el=customElements.get('my-element');const myElement=new el(); // same as document.createElement('my-element'); document.body.appendChild(myElement);
與connectedCallback相對應(yīng)的則是disconnectCallback,當(dāng)元素從DOM中移除的時(shí)候?qū){(diào)用它。但是要記住,在用戶關(guān)閉瀏覽器或者瀏覽器tab的時(shí)候,不會調(diào)用這個方法。 還有adoptedCallback,當(dāng)元素通過調(diào)用document.adoptNode(element)被采用到文檔時(shí)將會被調(diào)用,雖然到目前為止,我還沒有碰到這個方法被調(diào)用的時(shí)候。
另一個有用的生命周期方法是attributeChangedCallback,每當(dāng)將屬性添加到observedAttributes的數(shù)組中時(shí),就會調(diào)用這個函數(shù)。這個方法調(diào)用時(shí)兩個參數(shù)分別為舊值和新值。
class MyElement extends HTMLElement { static get observedAttributes() { return ['foo', 'bar']; } attributeChangedCallback(attr, oldVal, newVal) { switch(attr) { case 'foo': // do something with 'foo' attribute case 'bar': // do something with 'bar' attribute } }}
這個方法只有當(dāng)被保存在observedAttributes數(shù)組的屬性改變時(shí),就如這個例子中的foo和bar,被改變才會調(diào)用,其他屬性改變則不會。 屬性主要用在聲明元素的初始配置,狀態(tài)。理論上通過序列化可以將復(fù)雜值傳遞給屬性,但是這樣會影響性能,并且你可以直接調(diào)用組件的方法,所以不需要這樣做。但是如果你希望像React和Angular這樣的框架提供屬性的綁定,那你可以看一下。Polymer。
生命周期函數(shù)的順序
順序如下:
constructor -> attributeChangedCallback -> connectedCallback
為什么attributeChangedCallback要在connectedCallback之前執(zhí)行呢?
回想一下,web組件上的屬性主要用來初始化配置。這意味著當(dāng)組件被插入DOM時(shí),這些配置需要可以被訪問了。因此attributeChangedCallback要在connectedCallback之前執(zhí)行。 這意味著你需要根據(jù)某些屬性的值,在Shadow DOM中配置任何節(jié)點(diǎn),那么你需要在構(gòu)造函數(shù)中引用這些節(jié)點(diǎn),而不是在connectedCallback中引用它們。
例如,如果你有一個ID為container的組件,并且你需要在根據(jù)屬性的改變來決定是否給這個元素添加一個灰色的背景,那么你可以在構(gòu)造函數(shù)中引用這個元素,以便它可以在attributeChangedCallback中使用:
constructor() { this.container=this.shadowRoot.querySelector('#container');} attributeChangedCallback(attr, oldVal, newVal) { if(attr==='disabled') { if(this.hasAttribute('disabled') { this.container.style.background='#808080'; } else { this.container.style.background='#ffffff'; } }}
如果你一直等到connectedCallback再去創(chuàng)建this.container。然后在第一時(shí)間調(diào)用attributeChangedCallback,它還是不尅用的。因此盡管你應(yīng)該盡可能的延后你組件的connectedCallback,但在這種情況下是不可能的。
同樣重要的是,你可以在組件使用customElement.define()之前去使用它。當(dāng)改元素出現(xiàn)在DOM或者被插入到DOM,而還沒有被注冊時(shí)。它將會是一個HTMLUnkonwElement的實(shí)例。瀏覽器將會這樣處理未知的元素,你可以像處理其他元素一樣與它交互,除此之前,它將不會有任何方法和默認(rèn)樣式。
然后當(dāng)通過使用customElement.define()去定義它時(shí),并可使用類來定義增加它,這個過程被稱為升級。當(dāng)使用customElement.whenDefined升級元素時(shí),可以調(diào)用回調(diào),并會返回一個promise。當(dāng)這個元素被升級時(shí)。
customElements.whenDefined('my-element').then(()=> { // my-element is now defined})
Web Component的公共API
除了這些生命周期方法,你還可以定義可以從外部調(diào)用的方法,這對于使用React和Angular等框架目前是不可行的。例如你可以定義一個名為doSomething的方法:
class MyElement extends HTMLElement { ... doSomething() { // do something in this method }}
然后你可以在外部使用它
const element=document.querySelector('my-element'); element.doSomething();
在你的元素上定義的任何方法,都會成為其公共JavaScript的一部分。通過這種方式,你可以給元素的屬性提供setter來實(shí)現(xiàn)數(shù)據(jù)綁定。例如在元素的HTML中展示設(shè)置的屬性值。由于本質(zhì)上不可以將給屬性設(shè)置除了字符串以外的值,所以應(yīng)該講像對象這樣的復(fù)雜之作為屬性傳遞給自定義元素。
除了生命組件的初始狀態(tài),屬性還可以用于對應(yīng)屬性的值,以便將元素的Javascript狀態(tài)反應(yīng)到DOM的表現(xiàn)中。input元素的disabled屬性就是一個很好的例子:
<input name="name"> const input=document.querySelector('input'); input.disabled=true;
在將input的disabled的屬性設(shè)置為true后,改變也會相應(yīng)的反映到disabled屬性上。
<input name="name" disabled>
通過setter可以很容易的將property反應(yīng)到attribute上。
class MyElement extends HTMLElement { ... set disabled(isDisabled) { if(isDisabled) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } } get disabled() { return this.hasAttribute('disabled'); }}
當(dāng)attribute改變后需要執(zhí)行某些操作時(shí),將其添加到observedAttributes數(shù)組中。作為一種性能優(yōu)化,只有在這被列舉出的屬性才會監(jiān)測它們的改變。無論這個attribute什么時(shí)候改變了,都會調(diào)用attributeChangedCallback,參數(shù)分別是當(dāng)前值和新的值。
class MyElement extends HTMLElement { static get observedAttributes() { return ['disabled']; } constructor() { const shadowRoot=this.attachShadow({mode: 'open'}); shadowRoot.innerHTML=` <style> .disabled { opacity: 0.4; } </style> <div id="container"></div> `; this.container=this.shadowRoot('#container'); } attributeChangedCallback(attr, oldVal, newVal) { if(attr==='disabled') { if(this.disabled) { this.container.classList.add('disabled'); } else { this.container.classList.remove('disabled') } } }}
現(xiàn)在無論何時(shí)disabled的attribute被改變時(shí),this.container上面的名為disabled的class都會顯示或隱藏,它是ShadowDOM的內(nèi)在元素。 接下來讓我們看一下。
Shadow DOM
使用Shadow DOM,自定義元素的HTML和CSS完全封裝在組件內(nèi)。這意味著元素將以單個的HTML標(biāo)簽出現(xiàn)在文檔的DOM樹種。其內(nèi)部的結(jié)構(gòu)將會放在#shadow-root。
實(shí)際上一些原生的HTML元素也使用了Shadow DOM。例如你再一個網(wǎng)頁中有一個<video>元素,它將會作為一個單獨(dú)的標(biāo)簽展示,但它也將顯示播放和暫停視頻的控件,當(dāng)你在瀏覽器開發(fā)工具中查看video標(biāo)簽,是看不到這些控件。
這些控件實(shí)際上就是video元素的Shadow DOM的一部分,因此默認(rèn)情況下是隱藏的。要在Chrome中顯示Shadow DOM,進(jìn)入開發(fā)者工具中的Preferences中,選中Show user agent Shadow DOM。當(dāng)你在開發(fā)者工具中再次查看video元素時(shí),你就可以看到該元素的Shadow DOM了。
Shadow DOM還提供了局部作用域的CSS。所有的CSS都只應(yīng)用于組件本身。元素將只繼承最小數(shù)量從組件外部定義的CSS,甚至可以不從外部繼承任何CSS。不過你可以暴露這些CSS屬性,以便用戶對組件進(jìn)行樣式設(shè)置。這可以解決許多CSS問題,同時(shí)仍然允許自定義組件樣式。 定義一個Shadow root:
const shadowRoot=this.attachShadow({mode: 'open'}); shadowRoot.innerHTML=`<p>Hello world</p>`;
這定義了一個帶mode: open的Shadow root,這意味著可以再開發(fā)者工具找到它并與之交互,配置暴露出的CSS屬性,監(jiān)聽拋出的事件。同樣也可以定義mode:closed,會得到與之相反的表現(xiàn)。
你可以使用使用HTML字符串添加到innerHtml的property屬性中,或者使用一個<template>去給Shadow root添加HTML。一個HTML的template基本是惰性的HTML片段,你可以定義了延后使用。在實(shí)際插入DOM前,它是不可見也不可解析的。這意味著定義在內(nèi)部的任何資源都無法獲取,任何內(nèi)部定義的CSS和JavaScript只有當(dāng)它被插入DOM中時(shí),才會被執(zhí)行。當(dāng)組件的HTML根據(jù)其狀態(tài)發(fā)生更改時(shí),例如你可以定義多個<template>元素,然后根據(jù)組件的狀態(tài)去插入這些元素,這樣可以輕松的修改組件的HTML部分,并不需要修改單個DOM節(jié)點(diǎn)。
當(dāng)Shadow root被創(chuàng)建之后,你可以使用document對象的所有DOM方法,例如this.shadowRoot.querySelector去查找元素。組件的所有樣式都被定義在style標(biāo)簽內(nèi),如果你想使用一個常規(guī)的<link rel="stylesheet">標(biāo)簽,你也可以獲取外部樣式。除此之外,還可以使用:host選擇器對組件本身進(jìn)行樣式設(shè)置。例如,自定義元素默認(rèn)使用display: inline,所以如果你想要將組件展示為款元素,你可以這樣做:
:host { display: block;}
這還允許你進(jìn)行上下文的樣式化。例如你想要通過disabled的attribute來改變組件的背景是否為灰色:
:host([disabled]) { opacity: 0.5;}
默認(rèn)情況下,自定義元素從周圍的CSS中繼承一些屬性,例如顏色和字體等,如果你想清空組件的初始狀態(tài)并且將組件內(nèi)的所有CSS都設(shè)置為默認(rèn)的初始值,你可以使用:
:host { all: initial;}
非常重要,需要注意的一點(diǎn)是,從外部定義在組件本身的樣式優(yōu)先于使用:host在Shadow DOM中定義的樣式。如果你這樣做
my-element { display: inline-block;}
它將會被覆蓋
:host { display: block;}
不應(yīng)該從外部去改變自定義元素的樣式。如果你希望用戶可以設(shè)置組件的部分樣式,你可以暴露CSS變量去達(dá)到這個效果。例如你想讓用戶可以選擇組件的背景顏色,可以暴露一個叫 —background-color的CSS變量。 假設(shè)現(xiàn)在有一個Shadow DOM的根節(jié)點(diǎn)是 <div id="container">
#container { background-color: var(--background-color);}
現(xiàn)在用戶可以在組件的外部設(shè)置它的背景顏色
my-element { --background-color: #ff0000;}
你還可以在組件內(nèi)設(shè)置一個默認(rèn)值,以防用戶沒有設(shè)置
:host { --background-color: #ffffff;}#container { background-color: var(--background-color);}
當(dāng)然你還可以讓用戶設(shè)置任何的CSS變量,前提是這些變量的命名要以—開頭。
通過提供局部的CSS、HTML,Shadow DOM解決了全部CSS可能帶來的一些問題,這樣問題通常導(dǎo)致不斷地添加樣式表,其中包含了越來越多的選擇器和覆蓋。Shadow DOM似的標(biāo)記和樣式捆綁到自己的組件內(nèi),而不需要任何工具和命名約定。你再也不用擔(dān)心新的class或id會與現(xiàn)有的任何一個沖突。
除此之外,還可以通過CSS變量設(shè)置web組件的內(nèi)部樣式,還可以將HTML注入到Web Components中。
通過slots組成
組合是通過Shadow DOM樹與用戶提供的標(biāo)記組合在一起的過程。這是通過<slot>元素完成的,該元素基本是Shadow DOM的占位符,用來呈現(xiàn)用戶提供的標(biāo)記。用戶提供的標(biāo)記又可以成為 light DOM。合成會將light DOM和Shadow DOM合并成為一個新的DOM樹。
例如,你可以創(chuàng)建一個<iamge-gallery>組件,并提供標(biāo)準(zhǔn)的img標(biāo)簽作為組件要呈現(xiàn)的內(nèi)容:
<image-gallery> <img src="foo.jpg" slot="image"> <img src="bar.jpg" slot="image"></image-gallery>
組件現(xiàn)在將會獲取兩個提供的圖像,并且使用slots將它們渲染到組件的Shadow DOM中。注意到slot=”image”的attribute,這告訴了組件應(yīng)該要在Shadow DOM的什么位置渲染它們。例如這樣
<div id="container"> <div class="images"> <slot name="image"></slot> </div></div>
當(dāng)light DOM中的節(jié)點(diǎn)被分發(fā)到Shadow DOM中時(shí),得到的DOM樹看起來是這樣的:
<div id="container"> <div class="images"> <slot name="image"> <img src="foo.jpg" slot="image"> <img src="bar.jpg" slot="image"> </slot> </div></div>
正如你看到的,任何用戶提供的具有slot屬性的元素,都將在slot元素中呈現(xiàn)。而slot元素具有name屬性,其值與slot屬性的值對應(yīng)。 <select>元素的工作方與此完全相同,你可以在開發(fā)這工具中查看(查看設(shè)置在上方)
它接受用戶提供的option元素,并將它們呈現(xiàn)到下拉菜單中。 帶有name屬性的slot被稱為具名slot,但是這個屬性不是必須的。它僅用于需要將內(nèi)容呈現(xiàn)在特定位置時(shí)使用。當(dāng)一個或多個slot沒有name屬性時(shí),將按照用戶提供內(nèi)容的順序在其中展示。當(dāng)用戶提供的內(nèi)容少于slot時(shí),slot可以提供默認(rèn)的展示。
看一下<image-gallery>的Shadow DOM:
<div id="container"> <div class="images"> <slot></slot> <slot></slot> <slot> <strong>No image here!</strong> <-- fallback content --> </slot> </div></div>
如果你再只給兩個image的話,最后的結(jié)果如下:
<div id="container"> <div class="images"> <slot> <img src="foo.jpg"> </slot> <slot> <img src="bar.jpg"> </slot> <slot> <strong>No image here!</strong> </slot> </div></div>
通過slot在Shadow DOM中展示的元素被稱為分發(fā)節(jié)點(diǎn)。這些組件被插入前的樣式也將會被用于他們插入后。在Shadow DOM中,分發(fā)節(jié)點(diǎn)可以通過::sloted()來獲取額外的樣式
::slotted(img) { float: left;}
::sloted()可以接受任何有效的CSS選擇器,但它只能選擇頂級節(jié)點(diǎn),例如::slotedd(section img)的情況,將不會作用于this content
<image-gallery> <section slot="image"> <img src="foo.jpg"> </section></image-gallery>
在JavaScript中使用slots
你可以通過JavaScript與slots進(jìn)行交互去監(jiān)測哪個節(jié)點(diǎn)被分發(fā)到哪個slot,哪些slot被插入了元素,以及slotchange事件。
要找出哪些元素已經(jīng)被分發(fā)給對應(yīng)的slots可以使用 slot.assignedNodes() 如果你還想查看slot的默認(rèn)內(nèi)容,你可以使用 slot.assignedNodes({flatten: true}) 要找出哪些slot被分發(fā)的元素,可以使用element.assignedSlot 當(dāng)slot內(nèi)的節(jié)點(diǎn)發(fā)生改變,即添加或刪除節(jié)點(diǎn)時(shí),將會出發(fā)slotchange事件。要注意的是,只有當(dāng)slot節(jié)點(diǎn)自身改變才會觸發(fā),而這些slot節(jié)點(diǎn)的子節(jié)點(diǎn)并不會觸發(fā)。
slot.addEventListener('slotchange', e=> { const changedSlot=e.target; console.log(changedSlot.assignedNodes());});
在元素第一次初始化時(shí),Chrome會觸發(fā)slotchange事件,而Safari和Firefox則不會。
Shadow DOM中的事件
默認(rèn)情況下,自定義元素(如鼠標(biāo)和鍵盤事件)的標(biāo)準(zhǔn)事件將會從Shadow DOM中冒泡。每當(dāng)一個事件來此Shadow DOM中的一個節(jié)點(diǎn)時(shí),它會被重定向,因此該事件似乎來自元素本身。如果你想找出事件實(shí)際來自Shadow DOM中的哪個元素,可以調(diào)用event.composedPath()來檢索事件經(jīng)過的節(jié)點(diǎn)數(shù)組。然而,事件的target屬性還是會指向自定義元素本身。
你可以使用CustomEvent從自定義元素中拋出任何你想要的事件。
class MyElement extends HTMLElement { ... connectedCallback() { this.dispatchEvent(new CustomEvent('custom', { detail: {message: 'a custom event'} })); }}// on the outside document.querySelector('my-element').addEventListener('custom', e=> console.log('message from event:', e.detail.message));
但是當(dāng)一個事件從Shadow DOM的節(jié)點(diǎn)拋出而不是自定義元素本身,他不會從ShadowDOM上冒泡,除非它使用了composition: true來創(chuàng)建
class MyElement extends HTMLElement { ... connectedCallback() { this.container=this.shadowRoot.querySelector('#container'); // dispatchEvent is now called on this.container instead of this this.container.dispatchEvent(new CustomEvent('custom', { detail: {message: 'a custom event'}, composed: true // without composed: true this event will not bubble out of Shadow DOM })); }}
模板元素
除了使用this.shadowRoot.innerHTML來向一個元素的shadow root添加HTML,你也可以使用 <template>來做。template保存HTML供以后使用。它不會被渲染,并只有確保內(nèi)容是有效的才會進(jìn)行解析。模板中的JavaScript不會被執(zhí)行,也會獲取任何外部資源,默認(rèn)情況下它是隱藏的。
當(dāng)一個web component需要根據(jù)不同的情況來渲染不同的標(biāo)記時(shí),可以用不同的模板來完成:
class MyElement extends HTMLElement { ... constructor() { const shadowRoot=this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML=` <template id="view1"> <p>This is view 1</p> </template> <template id="view1"> <p>This is view 1</p> </template> <div id="container"> <p>This is the container</p> </div> `; } connectedCallback() { const content=this.shadowRoot.querySelector('#view1').content.clondeNode(true); this.container=this.shadowRoot.querySelector('#container'); this.container.appendChild(content); }}
這里兩個模板都使用了innerHTML放在shadow root內(nèi),最初這兩個模板都是隱藏的,自由container被渲染。在connectedCallback中我們通過this.shadowRoot.querySelector('#view1').content.clondeNode(true)獲取了#view1的內(nèi)容。模板content的屬性以DocumentFragment形式返回模板的內(nèi)容,可以勇士appendChild添加到另一個元素中。因?yàn)閍ppendChild將在元素已經(jīng)存在于DOM中時(shí)移除它,所以我們需要先使用cloneNode(true),否則模板的內(nèi)容將會被移除,這意味著我們只能使用一次。
模板對于快速的更改HTML部分或者重寫標(biāo)記非常有用。它們不僅限于web components并且可以在任何DOM中使用。
擴(kuò)展原生元素
到目前為止,我們一直在擴(kuò)展HTMLElement來創(chuàng)建一個全新的HTML元素。自定義元素還允許使用擴(kuò)展原生內(nèi)置元素,支持增強(qiáng)已經(jīng)存在的HTML元素,例如images和buttons。目前此功能僅在Chrome和Firefox中受支持。
擴(kuò)展現(xiàn)有HTML元素的好處是繼承了元素的所有屬性和方法。這允許對現(xiàn)有元素進(jìn)行逐步的增強(qiáng)。這意味著即使在不支持自定義元素的瀏覽器中,它仍是可用的。它只會降級到默認(rèn)的內(nèi)置行為。而如果它是一個全新的HTML標(biāo)簽,那它將會完全無法使用。
例如,我們想要增強(qiáng)一個HTML<button>標(biāo)簽
class MyButton extends HTMLButtonElement { ... constructor() { super(); // always call super() to run the parent's constructor as well } connectedCallback() { ... } someMethod() { ... }} customElements.define('my-button', MyButton, {extends: 'button'});
我們的web component不在擴(kuò)展更通用的HTMLElement,而是擴(kuò)展HTMLButtonElement。當(dāng)我們使用customElements.define()的時(shí)候還需要添加一個額外的參數(shù) {extends: ‘button’}來表示我們的類擴(kuò)展的是<button>元素。這可能看起來有些多余,因?yàn)槲覀円呀?jīng)表明了我們想要擴(kuò)展的是HTMLElementButton,但是這是必要的,因?yàn)橐恍┰毓蚕硪粋€DOM接口。例如 <q> 和 <blockquote>都共享 HTMLQuoteElement接口。
這個增強(qiáng)后的button可以通過is屬性來被使用
<button is="my-button">
現(xiàn)在它將被我們的MyElement類增加,如果它加載在一個不支持自定義元素的瀏覽器中,它將降級到一個標(biāo)準(zhǔn)的按鈕,真正的漸進(jìn)式增強(qiáng)。
注意,在擴(kuò)展現(xiàn)有元素時(shí),不能使用Shadow DOM。這只是一種擴(kuò)展原生HTML元素的方法,它繼承了所有現(xiàn)有的屬性、方法和事件,并提供了額外的功能。當(dāng)然可以在組件中修改元素的DOM和CSS,但是嘗試創(chuàng)建一個Shadow root將會拋出一個錯誤。
擴(kuò)展內(nèi)置元素的另一個好處就是,這些元素也可以應(yīng)用于子元素被限制的情況。例如thead元素只允許tr作為其子元素,因此<awesome-tr>元素將呈現(xiàn)無效標(biāo)記。這種情況下,我們可以拓展內(nèi)置的tr元素。并像這樣使用它:
<table> <thead> <tr is="awesome-tr"></tr> </thead></table>
這種創(chuàng)建web components的方式帶來了巨大的漸進(jìn)式增強(qiáng),但是正如前面所提到,目前僅有Chrome和Firefox支持。Edge也將會支持,但不幸的是,目前Safari還沒有實(shí)現(xiàn)這一點(diǎn)。
測試web components
與為Angular和React這樣的框架編寫測試相比,測試web components既簡單又直接。不需要轉(zhuǎn)換或者復(fù)雜的設(shè)置,只需要創(chuàng)建元素,并將其添加到DOM中并運(yùn)行測試。 這里有一個使用Mocha的測試
import 'path/to/my-element.js'; describe('my-element', ()=> { let element; beforeEach(()=> { element=document.createElement('my-element'); document.body.appendChild(element); }); afterEach(()=> { document.body.removeChild(element); }); it('should test my-element', ()=> { // run your test here });});
在這里,第一行引入了my-element.js文件,該文件將我們的web component通過es6模塊對外暴露。這意味著我們測試文件也需要作為一個ES6模塊加載到瀏覽器中農(nóng)。這需要以下的index.html能夠在瀏覽器中運(yùn)行測試。除了Mocha,這個設(shè)置還加載了WebcomponentsJS polyfill,Chai用于斷言,以及Sinon用于監(jiān)聽和模擬。
<!doctype html><html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="../node_modules/mocha/mocha.css"> <script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script> <script src="../node_modules/sinon/pkg/sinon.js"></script> <script src="../node_modules/chai/chai.js"></script> <script src="../node_modules/mocha/mocha.js"></script> <script> window.assert=chai.assert; mocha.setup('bdd'); </script> <script type="module" src="path/to/my-element.test.js"></script> <script type="module"> mocha.run(); </script> </head> <body> <div id="mocha"></div> </body></html>
在加載完所需的scripts后,我們暴露chai.assert作為一個全局變量,因此我們可以在測試中簡易的使用assert(),并設(shè)置Mocha來使用BDD接口。然后加載測試文件,并調(diào)用mocha.run()運(yùn)行測試。
請注意,在使用ES6模塊化時(shí),還需要將mocha.run()放在type=”module”的script中。因?yàn)镋S6模塊在默認(rèn)情況下是延遲執(zhí)行的。如果mocha.run()放在一個常規(guī)的script標(biāo)簽中,他將會在加載my-element.test.js之前執(zhí)行。
瀏覽器兼容
目前,Chrome、Firefox、Safari和Opera的最新版本都支持定制元素,即將推出的Edge 19也將支持定制元素。在iOS和Android上,Safari、Chrome和Firefox都支持它們。
對于老版本的瀏覽器,我們可以使用WebcomponentsJS polyfill,這樣下載
npm install --save @webcomponents/webcomponentsjs
你可以將webcomponents-loader.js包含進(jìn)去,這可以用來檢測特性只加載必要的polyfills。使用這個polyfill,你可以使用自定義的元素不需要向源碼中添加任何東西。但是它沒有真正的提供局部CSS。這意味著在不同web components中如果有同樣的class和id,在同一個document中,它們將會發(fā)生沖突。此外Shadow DOM的css選擇器 :host() :sloted()可能無法正常工作。
為了使其正確中座,你需要使用Shady CSS ployfill,這也意味著你需要稍微修改源代碼才能使用它。我個人認(rèn)為這是不可取的,所以我創(chuàng)建了一個webpack loader。它將為你處理這個問題。這也意味著你不得不對你的代碼進(jìn)行編譯。但是你可以保持代碼的不變。
webpack loader做了三件事,他在web components的Shadow DOM中為所有的CSS加上前綴,這些css不能以::host或者::slotted開頭,而是與元素的標(biāo)記名開頭,以提供適當(dāng)?shù)木植孔饔糜冢缓笏馕鏊械?:host和::slotted規(guī)則,以它們也能正確工作。
示例 #1 :lazy-img
這是一個圖片懶加載的組件 lazy-img自定義組件主要以元素img標(biāo)簽進(jìn)行實(shí)現(xiàn)
<lazy-img src="path/to/image.jpg" width="480" height="320" delay="500" margin="0px"></lazy-img>
如果繼承與img標(biāo)簽,通過is使用
<img is="lazy-img" src="path/to/img.jpg" width="480" height="320" delay="500" margin="0px">
這是一個很好的例子,說明了原生web components的強(qiáng)大,只需要導(dǎo)入JavaScript,添加HTML標(biāo)記,或者使用js拓展原生web組件,就可以使用了。
示例 #2 material-webcomponents
通過使用自定義元素實(shí)現(xiàn)Google的Material Design Github
So,我應(yīng)該拋棄我的框架嗎?
當(dāng)然,這要視情況而定。 目前的前端框架具有數(shù)據(jù)綁定、狀態(tài)管理和相當(dāng)標(biāo)準(zhǔn)化的代碼庫等功能所帶來的額外價(jià)值。問題是你的應(yīng)用是否真的需要它。
如果你需要問自己,你的應(yīng)用是否需要類似Redux這樣的狀態(tài)管理,那么你可能并不需要它。
你或許可以從數(shù)據(jù)綁定中獲益,但是對于數(shù)組和對象等非基本類型的值已經(jīng)允許直接用來設(shè)置web component的屬性。基本類型的值也可以直接用來設(shè)置,并且可以通過attributeChangedCallback來監(jiān)聽這些屬性的改變。
原生的web components并不提供類似允許其使用數(shù)據(jù)實(shí)例化并更新的功能,盡管有人建議這樣拓展<template>
<template id="example"> <h1>{{title}}</h1> <p>{{text}}</p></template> const template=document.querySelector('#example'); const instance=template.createInstance({title: 'The title', text: 'Hello world'}); shadowRoot.appendChild(instance.content); //update instance.update({title: 'A new title', text: 'Hi there'});
當(dāng)前最新提供DOM有效更新的庫是lit-html 另一個經(jīng)常提到的前端框架的好處是,它們提供了一個標(biāo)準(zhǔn)的代碼基準(zhǔn),可以使團(tuán)隊(duì)中的每一個新人從一開始就熟悉這些代碼基準(zhǔn)。雖然我想這是真的,但是我認(rèn)為這種好處是相當(dāng)有限的。
我曾今用過Angular、React和Polymer做過很多項(xiàng)目。雖然大家都對它們很熟悉,但是盡管使用了相同的框架,這些代碼庫還是有很大的不同。清晰定義的工作方式和樣式指南比簡單的使用框架更有助于代碼庫的一致性。框架也帶來了額外的復(fù)雜性,問問自己這是否真的值得。
現(xiàn)在web component已經(jīng)得到了廣泛的支持,你可以會得出這樣的結(jié)論:原生代碼可以提供與框架相同的功能,但是性能更好,代碼更少,復(fù)雜度更低。
使用原生web components的好處非常的清晰:
JQuery及其極其出色的遺產(chǎn)將會存在一段時(shí)間,但是你可能會發(fā)現(xiàn)不會有太多新項(xiàng)目用它來構(gòu)建,因?yàn)楝F(xiàn)在有了更好的選擇,并且正在迅速獲得關(guān)注。我期望這些前端框架的角色會發(fā)生巨大的變化,以至于它們僅僅圍繞原生 web component提供一個薄薄的層。
官網(wǎng)文檔:https://developers.google.cn/web/fundamentals/web-components/
關(guān)于本文
譯者:@林林小輝
譯文:https://zhuanlan.zhihu.com/p/64619005
作者:@dannymoerkerke
原文:https://www.dannymoerkerke.com/blog/web-components-will-replace-your-frontend-framework
*請認(rèn)真填寫需求信息,我們會在24小時(shí)內(nèi)與您取得聯(lián)系。