整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          CSS里的整潔架構

          CSS里的整潔架構

          歷數技術進步的代價時,弗洛伊德遵循的路線使人感到壓抑。他同意塔姆斯的評論:我們的發明只不過是手段的改進,目的卻未見改善。

          ——尼爾波斯曼《技術壟斷》

          雖然開發工具早已經從 preprocessor 進化到了 styled component 甚至是 functional css,但在我看來新的工具并沒有讓我們的樣式代碼寫的更好,只是更快——也可能會讓代碼壞的更快。工具的繁榮并沒有讓那些導致代碼難以維護的根本問題煙消云散,而是更易讓我們對其視而不見。這篇文章旨在回答一個問題:為什么樣式代碼難以寫對,它的陷阱究竟在哪里?

          如果一本正經的聊架構,套路多半是按照某些重要的特征依次展開講解。但這些所謂的重要特征其實在編程領域中是放之四海而皆準的,例如“擴展性”、“可復用”、“可維護性”等等,按這種思路聊,空談大于應用。所以我們不如通過解決某個具體的樣式問題,來審視樣式代碼應該如何編寫和組織

          下圖是一個非常簡單的 popup 組件,我們會以它的樣式開發過程串起整篇的內容。

          我們首先以一種簡單粗暴的方式來實現它,直覺上看,實現這個 popup 只需要三個元素即可:div 是最外面的容器,h1 用于包裹 "Success" 文案,button 用來實現按鈕

          <div class="popup">
           <div>Success</div>
           <button>OK</button>
          </div>

          我不會完整的寫出它的完整樣式,只大概列出其中一些關鍵屬性

          .popup {
           display: flex;
           justify-content: space-around;
          
           padding: 20px;
           width: 200px;
           height: 200px;
          
           div {
             margin: 10px;
             font-size: 24px;
          }
          
           button {
             background: orange;
             font-size: 16px;
             margin: 10px;
          }
          }

          第一版實現即完成了。目前看來并沒有什么不妥。

          問題不在于實現而是在于維護。接下來我就以一些常見的實際需求變更來看看上面的代碼存在怎樣的問題。


          對 DOM 元素的依賴

          假設現在需要在“Success”下方新增一個元素用于展示成功的具體信息

          想當然的我們需要新增一個 div 標簽。但如果這樣的話上面樣式中的 .popup div 樣式就會同時對這兩個 div 產生同樣的效果,這并不是我們希望的,很明顯這兩個元素的樣式是不同的。OK,如果你堅持使用標簽作為選擇器的話,你可以使用偽類選擇器 nth-child 來區分樣式:

          .popup {
           div:nth-child(1) {
             margin: 10px;
             font-size: 24px;
          }
          
           div:nth-child(2) {
             margin: 5px;
             font-size: 16px;
          }

          但如果某一天你認為"Success"應該使用 h1 而非 div 封裝更為恰當的話,那么修改的成本則是:

          • 將 div 改為 h1,
          • 將 div:nth-child(1) 樣式改為 h1 所屬,
          • 將 div:nth-child(2) 還原為 div 樣式

          但如果你一開始就能給 button 和 div 一個確切的 class 名稱,那么當你修改 DOM 元素時也僅僅需要修改 DOM 元素,而無需修改樣式文件了

          上面舉得這個例子是水平拓展的情況,也就是說我在某一元素的同一級新增一個元素。縱向拓展也會出現同樣的問題,你可以完全想象的出類似于這樣的選擇器:

          .popup div > div > h1 > span {
          
          }
          
          .popup {
           div {
             div {
               span {}
            }
          }
          }

          無論是上面代碼中的哪一種情況,樣式是否生效都極度依賴于 DOM 結構。在一連串的 DOM 標簽的層級關系中,哪怕只有一個元素出現了問題(可能是元素標簽類型發生了修改,還有可能是在它之上新增了一個元素)都會導致樣式大面積失效。同時這樣的做法也會讓你復用樣式難上加難,如果你希望復用 .popup div > div > h1 > 的樣式,你不得不將 DOM 結構也拷貝到想要復用的地方。

          所以這里我們至少能得出一個結論:CSS 不應該過分的依賴 HTML 結構

          而之所以加上“過分”二字,是因為樣式完全無法脫離結構獨立存在,例如 .popup .title .icon 這樣的的依賴關系背后就暗示了 HTML 結構的大致輪廓。

          所以我們可以繼續將上面的原則稍作更正:CSS 應該擁有對 HTML 的最小知識。理想情況下一個 .button 樣式無論應用在任何元素上看上去都應該像同一個立體的可點擊按鈕。


          父元素依賴

          上一節中我們開發完畢的組件通常會在頁面上被多處引用,但總存在個別場景需要你對組件稍作修改才得以適配。假設有一個需求是希望把這個 popup 應用在他們的移動端網站上,但為了適配動設備,某些元素的有關尺寸例如長寬內外邊距等都要縮小,你會怎么實現?

          我見過的 90% 的解決方案都是以添加父元素的依賴進行實現,也就是判斷該組件是否在某個特定的 class 下,如果是的話則修改樣式:

          body.mobile {
           .popup {
             padding: 10px;
             width: 100px;
             height: 100px;
          }
          }

          但如果此時你需要給平板設備添加一個新的樣式,我猜你可能會再添加一個 body.tablet { .popup {} } 代碼。又如果移動端網站有兩處需要使用 popup ,那么你的代碼很最終會變成這樣:

          body.mobile {
           .sidebar {
             .popup
          }
          
           .content {
             .popup
          }
          }

          這樣的代碼依然是難以復用的。如果某位開發者看到了移動端網站 popup 打開的樣式很喜歡,然后想移植到另一處,那么單純引入 popup 組件是不夠的,他還需要找到真正的生效的代碼,將樣式和 DOM 層級都復制粘貼過去。

          在一個組件自身已經擁有樣式的情況下,過分的依賴父組件間接的調整樣式,是一種 case by case 的編碼行為,本質上這架空了 popup 自帶樣式。假設 popup 自帶 box-shadow 的樣式屬性,但在有的用例里,box-shadow 可能會被加重,而在有的用例里,box-shadow 又可能會消失,那么它自帶的 box-shadow 根部本就沒有意義了,因為它永遠不會生效。

          架空違背了“最小驚訝原則”,給后續的維護者帶來了“驚喜”。如果此時 popup 的設計稿發生了修改,陰影需要減少,則修改它自身的樣式是不會生效的,或者說無法在每一處生效。而至于還有哪些地方無法生效,為什么它們無法生效,維護者并不知道,他同樣需要 case by case 的去查看代碼。這么做無疑增加了修改代碼的成本.

          解決這個問題并不像解決 DOM 依賴問題那么簡單,需要我們多管齊下。


          樣式角色的分離

          想提高代碼的可維護性,分離關注點永遠是屢試不爽的手段??v觀現有的各類組織樣式的方法論,比如 SMASS 或者是 ITCSS,對樣式進行適當的角色劃分是它們的核心思想之一。

          我們以一個完整的 popup 樣式為例:

          .popup {
           width: 100px;
           height: 30px;
          
           background: blue;
           color: white;
           border: 1px solid gary;
          
           display: flex;
           justify-content: center;
          }

          在這一組樣式中,我們看到

          • 有與布局相關的 width, height
          • 與視覺樣式相關的 background, color
          • 自身的布局樣式 flex
          • 其他樣式比如 border

          根據這些特點和常見的規范,可以考慮從下面幾個維度對樣式進行分離:

          • 布局(Layout)和尺寸(size): 一個組件在不同的父組件下擁有不同的尺寸是再正常不過的事情。與其定義一個被架空隨時會被覆蓋的尺寸,不如將布局的工作交由專職的組件處理。反過來說,該組件自生并不擁有尺寸,例如它可以選擇總是以 100% 的寬和高充滿包裹它的容器。

          從表面上看,這種行為只是將樣式(尺寸)從一個組件轉移到另一個組件(容器)上,但卻從根本上解決了我們上面提到的父元素依賴的困惱。任何想使用 popup 的其他組件,不用再設法關心 popup 組件的尺寸是如何實現的,它只需要關自己。

          進一步從深層次上說,它消滅了依賴。你可能沒有注意到,flex 布局的樣式配置遵循的就是這種模式:當你想讓你孩子元素按照某種規則布局的話,你只需要修改父元素和 flex 布局樣式屬性即可,完全不用再在孩子元素的樣式上做出修改。

          我個人認為另一個反模式的例子是 text-overflow: ellipsis 屬性,單一的該樣式屬性是不足以自動省略容器內的文字,容器還需要滿足 1) 寬度必須是 px 像素為單位 2) 元素必須擁有 overflow:hidden 和 --tt-darkmode-color: #A3A3A3;">而至于布局功能元素是與父元素為同一元素,還是獨立元素,我傾向于后者,畢竟幾個 markup 代碼并不會給我們添加多少負擔,但清晰的職責劃分卻能給我們將來的維護帶來不少便利。

          在這個前提下任何給 popup 添加的布局樣式實際上都意味這你新增了隱性依賴,因為你實際上是在暗示:它在這個父容器下的這個 margin 值看上去剛好。

          • 修飾類(Modifier): SOLID 原則中的 open-closed 告訴我們要對修改關閉,對拓展開發,這對樣式代碼也同樣成立。

          通常我們不會只需要單一樣式的按鈕,可能還需要帶有紅底白字的錯誤樣式的按鈕,還需要黃底白字的警告樣式按鈕。這種用例常見的解決方案不是新建 N 個不同的按鈕樣式,比如 primary-button, error-button(這樣務必會出現很多公共的 button 代碼),而是在一個 button 樣式的基礎上,通過提供樣式的“修飾”類來達到最終的目的。例如基礎款的按鈕 class 名稱為 button, 如果你想讓它變得帶有警告樣式的話,只需要同時使用 error 的 class 名稱即可。

          <div className="button error"></div>

          從本質上說這也是一種關注點的分離,只不過從這個角度上看它關心的是“變”與“不變”。我們將“變量”統統轉移到“修飾”類中。

          但這種方案在實現時會遇到不少問題,首先是修飾類的設計,例如當我在定義例如 error, primary, warning 的修飾類時,究竟哪些樣式屬性是我可以覆蓋的哪些是不可以,這必須有事前約定。否則某人在寫 error 樣式時,可能會無腦的覆蓋原 button 上的樣式直到看上去滿意為止。它依賴于抽象能力,但糟糕的抽象比不抽象還要難以維護。

          • 模塊化:借著組件模塊化這股東風,樣式模塊化似乎是水到渠成的事情。但如果眼光放長遠一些,模塊化并不僅限于將樣式趕到某個角落封裝起來集中管理。從上面的例子也不難看到,借用樣式中父元素依賴的特性可以輕松打破這種封裝。

          組件并非是封裝樣式的唯一單位,在一個網站中,還可能存在諸如 base、reset 這種全局或者說切面性質的樣式屬性。我理想的模塊化樣式應該能夠輕松達到以下的目的:

          • 控制樣式影響的方向性:例如全局樣式能夠影響組件,但組件不能夠影響全局;
          • 樣式模塊間的隔離和污染:雖然 A 組件是 B 組件的子元素,但 B 組件的樣式不會影響 A 的樣式。

          詮釋這兩點最好的例子是在進行響應式開發時,業內通用的對字體大小適配的解決方案。例如下面這個組件的 html 結構:

          <div class="ancestor">
           <div class="parent">
            parent
             <div class="child">
              hello
             </div>    
           </div>
          </div>

          在樣式中我們會設定:

          • ancestor 組件字體相對于根元素 html 變化,所以使用 rem 單位;
          • parent 和 child 的字體單位需要相對于該組件(也就是ancestor)的基準字體進行變化,所以使用 em 單位。
          .ancestor {
           font-size: 1rem;
          }
          .parent {
           font-size: 1.5em;
          }
          .child {
           font-size: 2em;
          }

          這樣當我們需要根據設備調整字體大小時,只需要調整根元素 html 字體大小,那么頁面上其他元素就會自我調節了。而如果我們只想調整局部樣式時,我們只需要調整 .ancestor 的字體大小即可,不會影響到其他元素。

          你閱讀到這里不難看出來,樣式難寫對的問題在于它太容易影響別的組件,也太容易受別的組件所影響了。絕大部分人遇到的問題是:

          • 我以為我修改的是 A 組件的樣式,但無形中卻影響到了 B 組件;
          • 組件 A 同時受好幾組樣式的影響,無論單獨修改誰都無法達到最終的效果。

          解決這個問題的辦法早就有了,那就是樣式的隔離。比如在 Angular 中,它是靠給元素添加隨機屬性并且給樣式附帶上屬性選擇器來實現的,例如你同時創建了 page-title 組件和 section-title 組件,它們都擁有 h1 元素的樣式,但是在編譯之后你看到的 css 分別是:

          h1[_ngcontent-kkb-c18] {
             background: yellow;
          }
          
          h1[_ngcontent-kkb-c19] {
             background: blue;
          }

          這樣所有的 h1 元素樣式都不會被互相影響。


          實現里的問題

          Pre-Processer

          無論你主觀上多么想避免以上的所有問題,給樣式一個好的整潔架構。在實現的過程中,我們依然會不小心掉入工具的陷阱中。

          再一次回到我們上面提到的 popup 樣式:

          .popup {
           width: 100px;
           height: 30px;
          
           background: blue;
           color: white;
          }

          假如你發現 { background: blue; color: white; } 作為常見樣式出現頻繁,希望對它進行復用,在使用 Sass 編程的前提下很明顯此時你有兩個選擇:@mixin 或者 @extend。

          如果采用 mixin,代碼如下:

          @mixin common {  
           background: blue;
           color: white;
          }
          
          .popup {  
           @include common;  
          }
          而如果采用 extend:
          .common {  
           background: blue;
           color: white;
          }
          
          .popup {  
           @extend .common;  
          }

          第一個問題是,無論你選擇哪種模式,你都很難說開發者是有意在依賴抽象還是在依賴實現。我們可以把 @mixin common 和 .common 解讀為對一種抽象的封裝,但很有可能后續的消費者只是想復用 background 和 color 而已。一旦如此,common 模塊就變得難以修改,因為對任意一個屬性的修改都會影響到未知的若干個模塊。

          在 SASS 中雖然我們可以給類名添加參數,把它當作參數相互傳遞,但它與我們實際編程中的變量和函數并不相同:JavaScript 中的函數我們往往只關心它的輸入與輸出,只是定義函數并不會對程序的結果造成影響。而當你在定義樣式類的那個時刻就已經可能對頁面產生了影響,并且其中的每一條屬性都會產生影響。

          如果你聽說過“組合優于繼承”,我相信會對這一點有更深刻的體驗。你可以回想繼承體系中存在的副作用,例如繼承打破了對超類的封裝,子類不能減少超類的接口等等,在 SASS 的這類復用關系中都能找到相似的影子。
          extend 相比 mixin 更危險的地方在于,它破壞了我們一如既往組織模塊的方式。

          例如目前已有一個 page 頁面,其中擁有一組 page-title 的樣式:

          .page {
           .page-title {
               .icon {
                   width: 10px;
              }
          
               .label {
                   width: 100px;
              }
          }    
          }

          現在 card-title 想通過 extend 來復用它:

          .card-title {
             @extend .page-title;
          }

          那么編譯之后的結果看上去會非常奇怪:

          .page .page-title .icon, .page .card-title .icon {
           width: 10px;
          }
          .page .page-title .label, .page .card-title .label {
           width: 100px;
          }

          哪怕你沒有聽說過 BEM,你的編程經驗也應該會告訴你 page 和 card 的樣式應屬于不同的模塊。但事實上編譯后的結果更像是優先考慮復用,從橫切面強行把二者耦合在一起。

          而如果你嘗試將公共的 title 樣式抽象為 mixin,再在 page-title 和 card-title 中進行復用:

          @mixin title {
             .icon {
                 width: 10px;
            }
          
             .label {
                 width: 100px;
            }
          }
          
          .page {
             .page-title {
                 @include title        
            }
          }
          
          .card-title {
             @include title
          }

          編譯的結果如下:

          .page .page-title .icon {
           width: 10px;
          }
          .page .page-title .label {
           width: 100px;
          }
          
          .card-title .icon {
           width: 10px;
          }
          .card-title .label {
           width: 100px;
          }

          很明顯 page 和 card 的樣式更涇渭分明。

          An Necessary Evil

          如果你問我我是否會遵守上面自己寫的每一條原則,我的答案是否定的。在實際開發中我傾向用便捷性換取可維護性。

          在編程領域里面唯一不變的就是變化本身,無論在敲鍵盤之前你面向對象設計的多么準確,對組件拆分的多么恰當,任何業務上的變化都有可能讓你所有的設計推倒重來。所以為了保證代碼能夠精確反饋業務知識的合理性,我們需要時常對代碼設計重新設計。

          你可以想象整個過程需要重新審視架構,從頭閱讀理解代碼,修改完畢后驗證。執行這一系列步驟需要不小的成本,還不包括其中的試錯,以及因為重構而浪費的添加新功能的機會。更重要的是成本擺在那里,但收益卻并不明顯。

          如果你的樣式代碼是基于 design system 之上的,那么你的改動成本會更高。因為你更不可能以個人的視角隨心所欲的改動代碼了,而是要自上而下的用整個產品的設計語言來衡量修改的合理性。

          另一個更實際的問題是,代碼從來不是依靠個人來維護。當這一套理論在團隊內并沒有達成共識,或者是大家只在理論層面了解過而實操時并不在意時,少數人的精心付出終究會化為泡影。代碼在理想狀態下應該最大成度上摒棄“人”這個因素成為流水線上工業化的產品。所以當我發現某個框架只有要求人們閱讀完數十頁最佳實踐有關的文檔才能寫出符合官方標準的好代碼時,那么現實工作中好代碼出現的概率基本為0——在規范輸出代碼上,一則有效的 eslint 規則比十頁文檔都要強。而在本篇中敘述的各種原則屬于后者。

          然而 css 代碼被寫的亂七八糟又會怎樣呢?產品壞了是肯定的,但相比其他 bug 有意思的事情是:

          • 相比腳本而言發現樣式問題的概率高,所見即所得;
          • 帶來的破壞相比腳本功能小,問題下產品依然可用;
          • 修復問題成本低,甚至不需要完整閱讀源碼即可有針對性的快速修復。

          基于上面的三點,同時考慮到當下技術棧繁雜學習成本高,腳本開發工作量大,交付壓力重,樣式架構的正確性想當然是被犧牲掉的那一個。

          最后重申我不鼓勵這樣的行為,這只是屈服于現實壓力下其中的一種可能性而已。如果你所在的項目資源充足,以及大家有決心把事情做對,那也未嘗不可。

          Functional CSS

          在我看來還有一類實踐是游離于以上體系之外的,比如 tailwind 和 tachyons 。之所以將它們稱之為“函數式”樣式,是因為在這些框架不提供組件化、語義化的樣式,比如 .card, .btn,而提供的是“工具類(utility class)”,比如 .overflow-auto,.box-content,它們 類似于函數式編程中沒有副作用的純函數。當你需要給你元素添加樣式時,只需要給這個元素添加對應的 class 名稱即可:

          <div class="overflow-auto box-content float-left"></div>

          之所以說這種實踐游離于以上體系之外,是因為它打破了我上面所說的前提:樣式和 DOM 結構之間存在依賴關系。在這種編程模式下,因為不再存在“級聯”關系,所以每個元素的樣式都是獨立的,互不影響。

          如此看來這種模式簡直就是天堂,本文里提及的所有問題都可以避免了:父元素依賴、角色耦合、預處理器里糾結的復用。

          但仔細想想,這種方式是不是很 inline style 類似?用 inline style 也能解決我們所說的上述所有問題。我們是不是又回到了起點?

          除了上面的問題外,我不再給出進一步推薦或者反對意見的原因在于,一方面這種實踐存在很大的爭議。另一方面我缺乏使用這類框架的經驗。這里經驗的判斷標準不是“是否用過”,而是“是否長期投入到多人協作的大型項目中”——“長期”、“多人”、“大型”這幾個關鍵詞很重要。因為我們在做技術選型的時候,更多要考慮和現有項目的契合度、團隊的適應成本,以及評估長遠來看它能給我們帶來巨大的好處是否能抵消替換它的成本。這些經驗是我缺乏的。



          文/Thoughtworks 李光毅

          更多精彩洞見,請關注公眾號Thoughtworks洞見

          家好,我是皮皮。

          前言

          對于前端來說,HTML 都是最基礎的內容。

          今天,我們來了解一下 HTML 和網頁有什么關系,以及與 DOM 有什么不同。通過本講內容,你將掌握瀏覽器是怎么處理 HTML 內容的,以及在這個過程中我們可以進行怎樣的處理來提升網頁的性能,從而提升用戶的體驗。


          一、瀏覽器頁面加載過程

          不知你是否有過這樣的體驗:當打開某個瀏覽器的時候,發現一直在轉圈,或者等了好長時間才打開頁面……

          此時的你,會選擇關掉頁面還是耐心等待呢?

          這一現象,除了網絡不穩定、網速過慢等原因,大多數都是由于頁面設計不合理導致加載時間過長導致的。

          我們都知道,頁面是用 HTML/CSS/JavaScript 來編寫的。

          • HTML 的職責在于告知瀏覽器如何組織頁面,以及搭建頁面的基本結構;
          • CSS 用來裝飾 HTML,讓我們的頁面更好看;
          • JavaScript 則可以豐富頁面功能,使靜態頁面動起來。

          HTML由一系列的元素組成,通常稱為HTML元素。HTML 元素通常被用來定義一個網頁結構,基本上所有網頁都是這樣的 HTML 結構:

          <html>
              <head></head>
              <body></body>
          </html>

          其中:

          • html元素是頁面的根元素,它描述完整的網頁;
          • head元素包含了我們想包含在 HTML 頁面中,但不希望顯示在網頁里的內容;
          • body元素包含了我們訪問頁面時所有顯示在頁面上的內容,是用戶最終能看到的內容;


          HTML 中的元素特別多,其中還包括可用于 Web Components 的自定義元素。

          前面我們提到頁面 HTML 結構不合理可能會導致頁面響應慢,這個過程很多時候體現在<script><style>元素的設計上,它們會影響頁面加載過程中對 Javascript 和 CSS 代碼的處理。

          因此,如果想要提升頁面的加載速度,就需要了解瀏覽器頁面的加載過程是怎樣的,從根本上來解決問題。

          瀏覽器在加載頁面的時候會用到 GUI 渲染線程和 JavaScript 引擎線程(更詳細的瀏覽器加載和渲染機制將在第 7 講中介紹)。其中,GUI 渲染線程負責渲染瀏覽器界面 HTML 元素,JavaScript 引擎線程主要負責處理 JavaScript 腳本程序。

          由于 JavaScript 在執行過程中還可能會改動界面結構和樣式,因此它們之間被設計為互斥的關系。也就是說,當 JavaScript 引擎執行時,GUI 線程會被掛起。

          以網易云課堂官網為例,我們來看看網頁加載流程。

          (1)當我們打開官網的時候,瀏覽器會從服務器中獲取到 HTML 內容。

          (2)瀏覽器獲取到 HTML 內容后,就開始從上到下解析 HTML 的元素。

          (3)<head>元素內容會先被解析,此時瀏覽器還沒開始渲染頁面。

          我們看到<head>元素里有用于描述頁面元數據的<meta>元素,還有一些<link>元素涉及外部資源(如圖片、CSS 樣式等),此時瀏覽器會去獲取這些外部資源。除此之外,我們還能看到<head>元素中還包含著不少的<script>元素,這些<script>元素通過src屬性指向外部資源。

          (4)當瀏覽器解析到這里時(步驟 3),會暫停解析并下載 JavaScript 腳本。

          (5)當 JavaScript 腳本下載完成后,瀏覽器的控制權轉交給 JavaScript 引擎。當腳本執行完成后,控制權會交回給渲染引擎,渲染引擎繼續往下解析 HTML 頁面。

          (6)此時<body>元素內容開始被解析,瀏覽器開始渲染頁面。

          在這個過程中,我們看到<head>中放置的<script>元素會阻塞頁面的渲染過程:把 JavaScript 放在<head>里,意味著必須把所有 JavaScript 代碼都下載、解析和解釋完成后,才能開始渲染頁面。

          到這里,我們就明白了:如果外部腳本加載時間很長(比如一直無法完成下載),就會造成網頁長時間失去響應,瀏覽器就會呈現“假死”狀態,用戶體驗會變得很糟糕。

          因此,對于對性能要求較高、需要快速將內容呈現給用戶的網頁,常常會將 JavaScript 腳本放在<body>的最后面。這樣可以避免資源阻塞,頁面得以迅速展示。我們還可以使用defer/async/preload等屬性來標記<script>標簽,來控制 JavaScript 的加載順序。

          百度首頁

          三、DOM 解析

          對于百度這樣的搜索引擎來說,必須要在最短的時間內提供到可用的服務給用戶,其中就包括搜索框的顯示及可交互,除此之外的內容優先級會相對較低。

          瀏覽器在渲染頁面的過程需要解析 HTML、CSS 以得到 DOM 樹和 CSS 規則樹,它們結合后才生成最終的渲染樹并渲染。因此,我們還常常將 CSS 放在<head>里,可用來避免瀏覽器渲染的重復計算。


          二、HTML 與 DOM 有什么不同

          我們知道<p>是 HTML 元素,但又常常將<p>這樣一個元素稱為 DOM 節點,那么 HTML 和 DOM 到底有什么不一樣呢?

          根據 MDN 官方描述:文檔對象模型(DOM)是 HTML 和 XML 文檔的編程接口。

          也就是說,DOM 是用來操作和描述 HTML 文檔的接口。如果說瀏覽器用 HTML 來描述網頁的結構并渲染,那么使用 DOM 則可以獲取網頁的結構并進行操作。一般來說,我們使用 JavaScript 來操作 DOM 接口,從而實現頁面的動態變化,以及用戶的交互操作。

          在開發過程中,常常用對象的方式來描述某一類事物,用特定的結構集合來描述某些事物的集合。DOM 也一樣,它將 HTML 文檔解析成一個由 DOM 節點以及包含屬性和方法的相關對象組成的結構集合。


          三、DOM 解析

          我們常見的 HTML 元素,在瀏覽器中會被解析成節點。比如下面這樣的 HTML 內容:

          <html>
              <head>
                  <title>標題</title>
              </head>
              <body>
                  <a href='xx.com'>我的超鏈接</a>
                  <h1>頁面第一標題</h1>
              </body>
          </html>

          打開控制臺 Elements 面板,可以看到這樣的 HTML 結構,如下圖所示:

          在瀏覽器中,上面的 HTML 會被解析成這樣的 DOM 樹,如下圖所示:


          我們都知道,對于樹狀結構來說,常常使用parent/child/sibling等方式來描述各個節點之間的關系,對于 DOM 樹也不例外。

          舉個例子,我們常常會對頁面功能進行抽象,并封裝成組件。但不管怎么進行整理,頁面最終依然是基于 DOM 的樹狀結構,因此組件也是呈樹狀結構,組件間的關系也同樣可以使用parent/child/sibling這樣的方式來描述。同時,現在大多數應用程序同樣以root為根節點展開,我們進行狀態管理、數據管理也常常會呈現出樹狀結構。


          四、事件委托

          我們知道,瀏覽器中各個元素從頁面中接收事件的順序包括事件捕獲階段、目標階段、事件冒泡階段。其中,基于事件冒泡機制,我們可以實現將子元素的事件委托給父級元素來進行處理,這便是事件委托。

          如果我們在每個元素上都進行監聽的話,則需要綁定三個事件;(假設頁面上有a,b,c三個兄弟節點)

          function clickEventFunction(e) {
            console.log(e.target===this); // logs `true`
            // 這里可以用 this 獲取當前元素
          }
          // 元素a,b,c綁定
          element2.addEventListener("click", clickEventFunction, false);
          element5.addEventListener("click", clickEventFunction, false);
          element8.addEventListener("click", clickEventFunction, false);

          使用事件委托,可以通過將事件添加到它們的父節點,而將事件委托給父節點來觸發處理函數:

          function clickEventFunction(event) {
            console.log(e.target===this); // logs `false`
            // 獲取被點擊的元素
            const eventTarget=event.target;
            // 檢查源元素`event.target`是否符合預期
            // 此處控制廣告面板的展示內容
          }
          // 元素1綁定
          element1.addEventListener("click", clickEventFunction, false);

          這樣能解決什么問題呢?

          • 綁定子元素會綁定很多次的事件,而綁定父元素只需要一次綁定。
          • 將事件委托給父節點,這樣我們對子元素的增加和刪除、移動等,都不需要重新進行事件綁定。

          常見的使用方式主要是上述這種列表結構,每個選項都可以進行編輯、刪除、添加標簽等功能,而把事件委托給父元素,不管我們新增、刪除、更新選項,都不需要手動去綁定和移除事件。

          如果在列表數量內容較大的時候,對成千上萬節點進行事件監聽,也是不小的性能消耗。使用事件委托的方式,我們可以大量減少瀏覽器對元素的監聽,也是在前端性能優化中比較簡單和基礎的一個做法。

          注意:

          1. 如果我們直接在document.body上進行事件委托,可能會帶來額外的問題;
          2. 由于瀏覽器在進行頁面渲染的時候會有合成的步驟,合成的過程會先將頁面分成不同的合成層,而用戶與瀏覽器進行交互的時候需要接收事件。此時,瀏覽器會將頁面上具有事件處理程序的區域進行標記,被標記的區域會與主線程進行通信。
          3. 如果我們document.body上被綁定了事件,這時候整個頁面都會被標記;
          4. 即使我們的頁面不關心某些部分的用戶交互,合成器線程也必須與主線程進行通信,并在每次事件發生時進行等待。這種情況,我們可以使用passive: true選項來解決


          五、總結

          我們了解了 HTML 的作用,以及它是如何影響瀏覽器中頁面的加載過程的,同時還介紹了使用 DOM 接口來控制 HTML 的展示和功能邏輯。我們了解了DOM解析事件委托等相關概念。

          用 CSS 最困難的部分之一是處理CSS的權重值,它可以決定到底哪條規則會最終被應用,尤其是如果你想在 Bootstrap 這樣的框架中覆蓋其已有樣式,更加顯得麻煩。不過隨著 CSS 層的引入,這一切都發生了變化。 這個新功能允許您創建自己的自定義 CSS 層,這是有史以來第一次確定所有 CSS 代碼權重的層次結構。 在本文中,我將剖析這對您意味著什么,它是如何工作的,以及您今天如何開始使用它。

          什么是層(Layers)

          創建您自己的自定義圖層是 CSS 的新功能,但圖層從一開始就存在于 CSS 中。 CSS 中有 3 個不同的層來管理所有樣式的工作方式。

          瀏覽器(也稱為用戶代理)樣式 - user agent style
          用戶樣式 - User Styles
          作者樣式 - Author Styles

          瀏覽器樣式是應用于瀏覽器的默認樣式。這就是為什么 Chrome 和 Safari 中的按鈕看起來不同的原因。在瀏覽器層中找到的樣式在瀏覽器之間是不同的,并且給每個瀏覽器一個獨特的外觀。

          下一層是用戶樣式,這并不是您真正需要擔心的事情。這些通常是用戶可以編寫并注入瀏覽器的自定義樣式,但瀏覽器不再真正支持這些樣式。用戶可能會更改一些瀏覽器設置,這些設置會向該圖層添加樣式,但在大多數情況下,可以完全忽略該層。

          最后,我們來到作者層。這是您最熟悉的層,因為您編寫的每一段 CSS 代碼都屬于這一層。

          這些層分開的原因是因為它可以很容易地覆蓋瀏覽器樣式和用戶樣式中定義的代碼,因為層定義了自己的層次結構,完全忽略了權重的影響。

          這 3 個 CSS 層是有序的(瀏覽器樣式、用戶樣式、然后是作者樣式),后面層中的每個樣式都將覆蓋前一層的任何樣式。這意味著即使瀏覽器樣式定義了一個超級特定的選擇器,例如#button.btn.super-specific,并且您的作者樣式定義了一個超級通用的選擇器,例如按鈕,您的作者樣式仍然會覆蓋瀏覽器樣式。

          這實際上已經是您可能一直在使用而沒有意識到的東西。

          * {
            box-sizing: border-box;
          }

          上面的選擇器沒有權重,因為 * 符號對權重沒有貢獻。 這意味著例如使用 p 作為選擇器的 p 標簽的瀏覽器樣式在技術上比 * 選擇器更具體,權重更高。 但是,這一切并不重要,因為作者樣式位于比瀏覽器樣式層晚的層中,因此您的代碼將始終覆蓋瀏覽器樣式。

          理解這一點至關重要,因為使用這個新的圖層 API,您可以在作者圖層中創建自己的圖層,從而更輕松地處理特定性。

          如何創建你自己的層

          下面來看個例子:

          很明顯,這是我們正常理解的CSS, ID設置的顏色權重更高,所以按鈕顯示為紅色。讓我們使用@layer給它們加上兩個層,看看是什么效果:

          按鈕變成藍色。為什么會這樣?

          我們給兩條CSS分別建立了base和utilities層,很明顯,后面創建的層的樣式覆蓋了前面層的樣式,盡管前面層的樣式有更高的權重。這就是層的默認工作原理。當然層的順序是可以指定的,

          @layer utilities, base;

          @layer utilities, base;

          您需要做的就是編寫@layer 關鍵字,后跟以逗號分隔的層列表。 這將按從左到右的順序定義所有層,其中列出的第一層到最后一層的權重是依次增加的。 然后,您可以稍后使用普通的@layer 語法向每個層添加代碼,而不必擔心定義層的順序,因為它們都在這一行中定義。 需要注意的是,這行代碼必須在定義任何層之前出現,所以我通常將它作為我的 CSS 文件中的第一行。如上圖,通過指定層的順序,我們讓base層應用在utilities層之后,所以按鈕又顯示為紅色。

          導入層

          上面這兩種方式都是導入bootstrap框架的CSS,并且把他們放在framework層中,這樣你如果想要覆蓋它已有的樣式,只需要新建一個自己的層,放置在framework層后面就行。像下面這樣。

          匿名層

          匿名層不常用,但它寫在后面可以覆蓋其他層的樣式,像下面可以把按鈕設為橙色。

          不在層里的樣式

          不在層里的樣式會有更高的權重,下面這個列表會讓你看得更清楚覆蓋是怎么發生的

          層還可以重疊設置,不過很少用。具體的用法可以查閱相關文檔。

          瀏覽器支持

          自從IE死了以后,所有主流瀏覽器都已支持這一特性。大家請放心使用。


          主站蜘蛛池模板: 国产av熟女一区二区三区| 精品国产日韩一区三区| 国产视频一区在线观看| 久久精品岛国av一区二区无码| 日韩一区二区久久久久久| 日韩精品一区二区三区中文精品| 亚洲乱码av中文一区二区| 久久久久一区二区三区| 亚洲片一区二区三区| 精品少妇人妻AV一区二区三区| 黄桃AV无码免费一区二区三区| 日韩精品一区二区三区毛片| 国产精品va无码一区二区| 亚洲国产精品一区二区三区久久| 理论亚洲区美一区二区三区| 麻豆一区二区99久久久久| 久久久久99人妻一区二区三区| 国产天堂一区二区综合| 一区免费在线观看| 色国产在线视频一区| 无码av免费毛片一区二区| 日本免费一区二区三区四区五六区| 日本一区二区三区高清| 国产精久久一区二区三区 | 日韩精品一区二区三区中文 | 国产aⅴ精品一区二区三区久久 | 无码国产精品一区二区免费3p | 久久久久人妻精品一区二区三区| 亚洲日本中文字幕一区二区三区 | 亚洲国产av一区二区三区| 视频精品一区二区三区| 日韩精品电影一区亚洲| 无码精品前田一区二区| 成人国内精品久久久久一区 | 久99精品视频在线观看婷亚洲片国产一区一级在线 | 国产凸凹视频一区二区| 精品人妻无码一区二区三区蜜桃一| 久久99国产一区二区三区| 日韩社区一区二区三区| 久久99国产精品一区二区| 国产一区二区三区美女|