瀏覽器工作原理:淺析WebComponent - 像搭積木一樣構建Web應用


  在上篇文章中我們從技術演變的角度介紹了 PWA,這是一套集合了多種技術的理念,讓瀏覽器漸進式適應設備端。今天我們要站在開發者和項目角度來聊聊 WebComponent,同樣它也是一套技術的組合,能提供給開發者組件化開發的能力。

  那什么是組件化呢?

  其實組件化並沒有一個明確的定義,不過這里我們可以使用 10 個字來形容什么是組件化,那就是:對內高內聚,對外低耦合。對內各個元素彼此緊密結合、相互依賴,對外和其他組件的聯系最少且接口簡單。

  可以說,程序員對組件化開發有着天生的需求,因為一個稍微復雜點的項目,就涉及到多人協作開發的問題,每個人負責的組件需要盡可能獨立完成自己的功能,其組件的內部狀態不能影響到別人的組件,在需要和其他組件交互的地方得提前協商好接口。通過組件化可以降低整個系統的耦合度,同時也降低程序員之間溝通復雜度,讓系統變得更加易於維護。

  使用組件化能帶來很多優勢,所以很多語言天生就對組件化提供了很好的支持,比如 C/C++ 就可以很好地將功能封裝成模塊,無論是業務邏輯,還是基礎功能,抑或是 UI,都能很好地將其組合在一起,實現組件內部的高度內聚、組件之間的低耦合。

  大部分語言都能實現組件化,歸根結底在於編程語言特性,大多數語言都有自己的函數級作用域、塊級作用域和類,可以將內部的狀態數據隱藏在作用域之下或者對象的內部,這樣外部就無法訪問了,然后通過約定好的接口和外部進行通信。

  JS 雖然有不少缺點,但是作為一門編程語言,它也能很好地實現組件化,畢竟有自己的函數級作用域和塊級作用域,所以封裝內部狀態數據並提供接口給外部都是沒問題的。

  既然 JS 可以很好地實現組件化,那么我們所談及的 WebComponent 到底又是什么呢?

一、阻礙前端組件化的因素

  在前端雖然 HTML 、CSS 和 JS 是強大的開發語言,但是在大型項目中維護起來會比較困難,如果在頁面中嵌入第三方內容時,還需要確保第三方的內容樣式不會影響到當前內容,同樣也要確保當前的 DOM 不會影響到第三方的內容。

  所以要聊 WebComponent,得先看看 HTML 和 CSS 是如何阻礙前端組件化的,這里我們就通過下面這樣一個簡單的例子來分析下:

<style> p { background-color: brown; color: cornsilk } </style> <p>time.geekbang.org</p>
<style> p { background-color: red; color: blue } </style> <p>time.geekbang</p>

  上面這段代碼分別實現了自己 p 標簽的屬性,如果兩個人分別負責開發這兩段代碼的話,那么在測試階段可能沒有什么問題,不過當最終項目整合的時候,其中內部的 CSS 屬性會影響到其他外部的 p 標簽的,之所以會這樣,是因為 CSS 是影響全局的。

  我們在《瀏覽器工作原理:淺析瀏覽器中的頁面 - 渲染流水線 - CSS如何影響首次加載時的白屏時間》這篇文章中分析過,渲染引擎會將所有的 CSS 內容解析為 CSSOM,在生成布局樹的時候,會在 CSSOM 中為布局樹中的元素查找樣式,所以有兩個相同標簽最終所顯示出來的效果是一樣的,渲染引擎是不能為它們分擔單獨設置樣式的

  除了 CSS 的全局屬性會阻礙組件化,DOM 也是阻礙組件化的一個因素,因為在頁面中只有一個 DOM,任何地方都可以直接讀取和修改 DOM。所以使用 JS 來實現組件化是沒有問題的,但是 JS 一旦遇上 CSS 和 DOM ,那么就相當難辦了。

二、WebComponent 組件化開發

  現在我們了解了 CSS 和 DOM 是阻礙組件化的兩個因素,那要怎么解決呢?

  WebComponent 給出了解決思路,它提供了對局部視圖封裝能力,可以讓 DOM 、CSSOM 和 JS 運行在局部環境中,這樣就使得局部的 CSS 和 DOM 不會影響全局。

  了解了這些,下面我們就結合具體代碼來看看 WebComponent 是怎么實現組件化的。

  前面我們說了,WebComponent 是一套技術的組合,具體涉及到了 Custom elements(自定義元素)、Shadow DOM(影子 DOM )和 HTML templates( HTML 模板),詳細內容你可以參考 MDN 上的相關鏈接

  下面我們就來演示下這三個技術是怎么實現數據封裝的,如下面代碼所示:

<!DOCTYPE html> <html> <body> <!-- 一:定義模板 二:定義內部 CSS 樣式 三:定義 JS 行為 --> <template id="geekbang-t"> <style> p{ background-color: brown; color: cornsilk } div { width: 200px; background-color: bisque; border: 3px solid chocolate; border-radius: 10px; } </style> <div> <p>time.geekbang.org</p <p>time1.geekbang.org</p> </div> <script> function foo() { console.log('inner log') } </script> </template> <script> class GeekBang extends HTMLElement{ constructor(){ super() // 獲取組件模板  const content = document.querySelector('#geekbang-t').content //創建影子DOM節點  const shadowDOM = this.attachShadow({ mode: 'open' }) //將模板添加到影子DOM上  shadowDOM.appendChild(content.cloneNode(true)) } } customElements.define('geek-bang', GeekBang) </script> <geek-bang></geek-bang> <div> <p>time.geekbang.org</p> <p>time1.geekbang.org</p> </div> <geek-bang></geek-bang> </body> </html>

  詳細觀察上面這段代碼,我們可以得出:要使用 WebComponent,通常要實現下面三個步驟。

  首先,使用 template 屬性來創建模板。

  利用 DOM 可以查找到模板的內容,但是模板元素是不會被渲染到頁面上的,也就是說 DOM 樹中的 template 節點不會出現在布局樹中,所以我們可以使用 template 來自定義一些基礎的元素結構,這些基礎的元素結構是可以被重復使用的。一般模板定義好之后,我們還需要在模板的內部定義樣式信息。

  其次,我們需要創建一個 GeekBang 的類。

  在該類的構造函數中要完成三件事:

  1、查找模板內容;

  2、創建影子 DOM;

  3、再將模板添加到影子 DOM 上。

  上面最難理解的是影子 DOM,其實影子 DOM 的作用是將模板中的內容與全局 DOM 和 CSS 進行隔離,這樣我們就可以實現元素和樣式的私有化了。

  你可以把影子 DOM 看成是一個作用域,其內部的樣式和元素是不會影響到全局的樣式和元素的,而在全局環境下,要訪問影子 DOM 內部的樣式或者元素也是需要通過約定好的接口的。

  總之,通過影子 DOM,我們就實現了 CSS 和元素的封裝,在創建好封裝影子 DOM 的類之后,我們就可以使用 customElements.define 來自定義元素了(可參考上述代碼定義元素的方式)。

  最后,就很簡單了,可以像正常使用 HTML 元素一樣使用該元素,如上述代碼中的 <geek-bang></geek-bang>。

  上述代碼最終渲染出來的頁面,如下圖所示:

 

  從圖中我們可以看出,影子 DOM 內部的樣式是不會影響到全局 CSSOM 的。

  另外,使用 DOM 接口也是無法直接查詢到影子 DOM 內部元素的,比如你可以使用 document.getElementsByTagName('div') 來查找所有 div 元素,這時候你會發現影子 DOM 內部的元素都是無法查找的,因為要想查找影子 DOM 內部的元素需要專門的接口,所以通過這種方式又將影子內部的 DOM 和外部的 DOM 進行了隔離。

  通過影子 DOM 可以隔離 CSS 和 DOM,不過需要注意一點,影子 DOM 的 JS 腳本是不會被隔離的,比如在影子 DOM 定義的 JS 函數依然可以被外部訪問,這是因為 JS 語言本身已經可以很好地實現組件化了。

三、瀏覽器如何實現影子 DOM

  關於 WebComponent 的使用方式我們就介紹到這里。WebComponent 整體知識點不多,內容也不復雜,我認為核心就是影子 DOM。上面我們介紹影子 DOM 的作用主要有以下兩點:

1、影子 DOM 中的元素對於整個網頁是不可見的;

2、影子 DOM 的 CSS 不會影響到整個網頁的 CSSOM,影子 DOM 內部的 CSS 只對內部的元素起作用。

  那么瀏覽器是如何實現影子 DOM 的呢?下面我們就來分析下,如下圖:

 

  該圖是上面那段示例代碼對應的 DOM 結構圖,從圖中可以看出,我們使用了兩次 geek-bang 屬性,那么就會生成兩個影子 DOM,並且每個影子 DOM 都有一個 shadow root 的根節點,我們可以將要展示的樣式或者元素添加到影子 DOM 的根節點上,每個影子 DOM 你都可以看成是一個獨立的 DOM,它有自己的樣式、自己的屬性,內部樣式不會影響到外部樣式,外部樣式也不會影響到內部樣式。

  瀏覽器為了實現影子 DOM 的特性,在代碼內部做了大量的條件判斷,比如當通過 DOM 接口去查找元素時,渲染引擎會去判斷 geek-bang 屬性下面的 shadow-root 元素是否是影子 DOM,如果是影子 DOM,那么就直接跳過 shadow-root 元素的查詢操作。所以這樣通過 DOM API 就無法直接查詢到影子 DOM 的內部元素了

  另外,當生成布局樹的時候,渲染引擎也會判斷 geek-bang 屬性下面的 shadow-root 元素是否是影子 DOM,如果是,那么在影子 DOM 內部元素的節點選擇 CSS 樣式的時候,會直接使用影子 DOM 內部的 CSS 屬性。所以這樣最終渲染出來的效果就是影子 DOM 內部定義的樣式。

四、總結

  好了,今天就介紹到這里,下面來總結下本文的主要內容。

  首先,我們介紹了組件化開發是程序員的剛需,所謂組件化就是功能模塊要實現高內聚、低耦合的特性。

  不過由於 DOM 和 CSSOM 都是全局的,所以它們是影響了前端組件化的主要元素。

  基於這個原因,就出現 WebComponent,它包含自定義元素、影子 DOM 和 HTML 模板三種技術,使得開發者可以隔離 CSS 和 DOM。

  在此基礎上,我們還重點介紹了影子 DOM 到底是怎么實現的。

  關於 WebComponent 的未來如何,這里我們不好預測和評判,但是有一點可以肯定,WebComponent 也會采用漸進式迭代的方式向前推進,未來依然有很多坑需要去填。

  思考時間:你是怎么看待 WebComponent 和前端框架(React、Vue)之間的關系的?

  回復:

1、web component是通過瀏覽器引擎提供api接口進行操作,然后在dom,cssom生成過程中控制實現組件化的作用域/執行執行上下文的隔離;

而 vue/react 是在沒有瀏覽器引擎支持的情況下,通過采取一些取巧的手法(比如:JS 執行上下文的封裝利用閉包;樣式的封裝利用文件 hash 值作為命名空間,在 css 選擇的時候多套一層選擇條件(hash值),本質上還是全局的,只是不同組件 css 選擇的時候只能選擇到組件相應的css樣式,實現的隔離)

2、Webcomponent、React以及Vue都實現了DOM的組件化,webcomponent 是W3C的親兒子,通過shadow dom 技術實現dom以及css的隔離;

而React以及Vue則不是正規軍,但是也同樣達到了dom組件化的目的,主要結合已有的html特性實現樣式的隔離比如scoped。

注: 本文出自極客時間(瀏覽器工作原理與實踐),請大家多多支持李兵老師。如有侵權,請及時告知。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM