<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Web Components 上手指南

          共 13092字,需瀏覽 27分鐘

           ·

          2021-02-27 23:03

          現(xiàn)在的前端開發(fā)基本離不開 React、Vue 這兩個框架的支撐,而這兩個框架下面又衍生出了許多的自定義組件庫:
          • Element(Vue)
          • Ant Design(React)

          這些組件庫的出現(xiàn),讓我們可以直接使用已經(jīng)封裝好的組件,而且在開源社區(qū)的幫助下,出現(xiàn)了很多的模板項目( vue-element-admin、Ant Design Pro ),能讓我們快速的開始一個項目。

          雖然 React、Vue 為我們的組件開發(fā)提供了便利,但是這兩者在組件的開發(fā)思路上,一個是自創(chuàng)的 JSX 語法,一個是特有的單文件模板的語法,兩者的目標都是想提供一種組件的封裝方法。畢竟都有其原創(chuàng)的東西在里面,和我們剛開始接觸的 Web 基礎(chǔ)的 HTML、CSS、JS 的方式還是有些出入的。今天介紹的就是,通過 HTML、CSS、JS 的方式來實現(xiàn)自定義的組件,也是目前瀏覽器原生提供的方案:Web Components。

          什么是 Web Components?

          Web Components 本身不是一個單獨的規(guī)范,而是由一組DOM API 和 HTML 規(guī)范所組成,用于創(chuàng)建可復用的自定義名字的 HTML 標簽,并且可以直接在你的 Web 應(yīng)用中使用。

          代碼的復用一直都是我們追求的目標,在 JS 中可復用的代碼我們可以封裝成一個函數(shù),但是對于復雜的HTML(包括相關(guān)的樣式及交互邏輯),我們一直都沒有比較好的辦法來進行復用。要么借助后端的模板引擎,要么借助已有框架對 DOM API 的二次封裝,而 Web Components 的出現(xiàn)就是為了補足瀏覽器在這方面的能力。

          如何使用 Web Components?

          Web Components 中包含的幾個規(guī)范,都已在 W3C 和 HTML 標準中進行了規(guī)范化,主要由三部分組成:

          • Custom elements(自定義元素):一組 JavaScript API,用來創(chuàng)建自定義的 HTML標簽,并允許標簽創(chuàng)建或銷毀時進行一些操作;
          • Shadow DOM(影子DOM):一組 JavaScript API,用于將創(chuàng)建的 DOM Tree 插入到現(xiàn)有的元素中,且 DOM Tree 不能被外部修改,不用擔心元素被其他地方影響;
          • HTML templates(HTML模板):通過 <template><slot> 直接在 HTML 文件中編寫模板,然后通過 DOM API 獲取。

          Custom elements(自定義元素)

          瀏覽器提供了一個方法:customElements.define() , 來進行自定義標簽的定義。該方法接受三個參數(shù):

          • 自定義元素的名稱,一個 DOMString 標準的字符串,為了防止自定義元素的沖突,必須是一個帶短橫線連接的名稱(e.g. custom-tag)。
          • 定義自定義元素的一些行為,類似于 React、Vue 中的生命周期。
          • 擴展參數(shù)(可選),該參數(shù)類型為一個對象,且需要包含 extends 屬性,用于指定創(chuàng)建的元素繼承自哪一個內(nèi)置元素(e.g. { extends: 'p' })。

          下面通過一些例子,演示其用法,完整代碼放到了 JS Bin 上。

          創(chuàng)建一個新的 HTML 標簽

          先看看如何創(chuàng)建一個全新的自定義元素。

          class HelloUser extends HTMLElement {
            constructor() {
              // 必須調(diào)用 super 方法
              super();

              // 創(chuàng)建一個 div 標簽
              const $box = document.createElement("p");
              let userName = "User Name";
              if (this.hasAttribute("name")) {
                // 如果存在 name 屬性,讀取 name 屬性的值
                userName = this.getAttribute("name");
              }
              // 設(shè)置 div 標簽的文本內(nèi)容
              $box.innerText = `Hello ${userName}`;

              // 創(chuàng)建一個 shadow 節(jié)點,創(chuàng)建的其他元素應(yīng)附著在該節(jié)點上
              const shadow = this.attachShadow({ mode"open" });
              shadow.appendChild($box);
            }
          }

          // 定義一個名為 <hello-user /> 的元素
          customElements.define("hello-user", HelloUser);
          <hello-user name="Shenfq"></hello-user>

          這時候頁面上就會生成一個 <p> 標簽,其文本內(nèi)容為:Hello Shenfq。這種形式的自定義元素被稱為:Autonomous custom elements,是一個獨立的元素,可以在 HTML 中直接使用。

          擴展已有的 HTML 標簽

          我們除了可以定義一個全新的 HTML 標簽,還可以對已有的 HTML 標簽進行擴展,例如,我們需要封裝一個與 <ul> 標簽?zāi)芰︻愃频慕M件,就可以使用如下方式:

          class SkillList extends HTMLUListElement {
            constructor() {
              // 必須調(diào)用 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 方法的第三個參數(shù),且第二參數(shù)的類,也需要繼承需要擴展標簽的對應(yīng)的類。使用的時候,只需要在標簽加上 is 屬性,屬性值為第一個參數(shù)定義的名稱。

          生命周期

          自定義元素的生命周期比較簡單,一共只提供了四個回調(diào)方法:

          • connectedCallback:當自定義元素被插入到頁面的 DOM 文檔時調(diào)用。
          • disconnectedCallback:當自定義元素從 DOM 文檔中被刪除時調(diào)用。
          • adoptedCallback:當自定義元素被移動時調(diào)用。
          • attributeChangedCallback: 當自定義元素增加、刪除、修改自身屬性時調(diào)用。

          下面演示一下使用方法:

          class HelloUser extends HTMLElement {
            constructor() {
              // 必須調(diào)用 super 方法
              super();

              // 創(chuàng)建一個 div 標簽
              const $box = document.createElement("p");
              let userName = "User Name";
              if (this.hasAttribute("name")) {
                // 如果存在 name 屬性,讀取 name 屬性的值
                userName = this.getAttribute("name");
              }
              // 設(shè)置 div 標簽的文本內(nèi)容
              $box.innerText = `Hello ${userName}`;

              // 創(chuàng)建一個 shadow 節(jié)點,創(chuàng)建的其他元素應(yīng)附著在該節(jié)點上
              const shadow = this.attachShadow({ mode"open" });
              shadow.appendChild($box);
            }
            connectedCallback() {
              console.log('創(chuàng)建元素')
              // 5s 后移動元素到 iframe
              setTimeout(() => {
                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>

          在元素被創(chuàng)建后,等待 5s,然后將自定義元素移動到 iframe 文檔中,這時候能看到控制臺會同時出現(xiàn) 刪除元素移動元素 的 log。

          Console

          Shadow DOM(影子DOM)

          在前面介紹自定義元素的時候,已經(jīng)用到了 Shadow DOM。Shadow DOM 的作用是讓內(nèi)部的元素與外部隔離,讓自定義元素的結(jié)構(gòu)、樣式、行為不受到外部的影響。

          我們可以看到前面定義的 <hello-user> 標簽,在控制臺的 Elements 內(nèi),會顯示一個 shadow-root ,表明內(nèi)部是一個 Shadow DOM。

          Shadow DOM

          其實 Web Components 沒有提出之前,瀏覽器內(nèi)部就有使用 Shadow DOM 進行一些內(nèi)部元素的封裝,例如 <video> 標簽。我們需要現(xiàn)在控制臺的配置中,打開 Show user agent ashdow DOM 開關(guān)。

          設(shè)置

          然后在控制臺的 Elements 內(nèi),就能看到 <video> 標簽內(nèi)其實也有一個 shadow-root

          video 標簽

          創(chuàng)建 Shadow DOM

          我們可以在任意一個節(jié)點內(nèi)部創(chuàng)建一個 Shadow DOM,在獲取元素實例后,調(diào)用 Element.attachShadow() 方法,就能將一個新的 shadow-root 附加到該元素上。

          該方法接受一個對象,且只有一個 mode 屬性,值為 openclosed,表示 Shadow DOM 內(nèi)的節(jié)點是否能被外部獲取。

          <div id="root"></div>
          <script>
            // 獲取頁面的
            const $root = document.getElementById('root');
            const $p = document.createElement('p');
            $p.innerText = '創(chuàng)建一個 shadow 節(jié)點';
            const shadow = $root.attachShadow({mode'open'});
            shadow.appendChild($p);
          </script>
          Shadow DOM

          mode 的差異

          前面提到了 mode 值為 openclosed,主要差異就是是否可以使用 Element.shadowRoot 獲取到 shadow-root 進行一些操作。

          <div id="root"></div>
          <script>
            // 獲取頁面的
            const $root = document.getElementById('root');
            const $p = document.createElement('p');
            $p.innerText = '創(chuàng)建一個 shadow 節(jié)點';
            const shadow = $root.attachShadow({mode'open'});
            shadow.appendChild($p);
            console.log('is open', $div.shadowRoot);
          </script>
          open mode
          <div id="root"></div>
          <script>
            // 獲取頁面的
            const $root = document.getElementById('root');
            const $p = document.createElement('p');
            $p.innerText = '創(chuàng)建一個 shadow 節(jié)點';
            const shadow = $root.attachShadow({mode'closed'});
            shadow.appendChild($p);
            console.log('is closed', $div.shadowRoot);
          </script>
          closed mode

          HTML templates(HTML模板)

          前面的案例中,有個很明顯的缺陷,那就是操作 DOM 還是得使用 DOM API,相比起 Vue 得模板和 React 的 JSX 效率明顯更低,為了解決這個問題,在 HTML 規(guī)范中引入了 <tempate><slot> 標簽。

          使用模板

          模板簡單來說就是一個普通的 HTML 標簽,可以理解成一個 div,只是這個元素內(nèi)的所以內(nèi)容不會展示到界面上。

          <template id="helloUserTpl">
            <p class="name">Name</p>
            <a target="blank" class="blog">##</a>
          </template>

          在 JS 中,我們可以直接通過 DOM API 獲取到該模板的實例,獲取到實例后,一般不能直接對模板內(nèi)的元素進行修改,要調(diào)用 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() {
                // 必須調(diào)用 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'));
                }
                // 創(chuàng)建一個 shadow 節(jié)點,創(chuàng)建的其他元素應(yīng)附著在該節(jié)點上
                const shadow = this.attachShadow({ mode"closed" });
                shadow.appendChild(content);
              }
            }

            // 定義一個名為 <hello-user /> 的元素
            customElements.define("hello-user", HelloUser);
          </script>

          添加樣式

          <template> 標簽中可以直接插入 <style> 標簽在,模板內(nèi)部定義樣式。

          <template id="helloUserTpl">
            <style>
              :host {
                display: flex;
                flex-direction: column;
                width200px;
                padding20px;
                background-color#D4D4D4;
                border-radius3px;
              }

              .name {
                font-size20px;
                font-weight600;
                line-height1;
                margin0;
                margin-bottom5px;
              }

              .email {
                font-size12px;
                line-height1;
                margin0;
                margin-bottom15px;
              }
            
          </style>
            <p class="name">User Name</p>
            <a target="blank" class="blog">##</a>
          </template>

          其中 :host 偽類用來定義 shadow-root的樣式,也就是包裹這個模板的標簽的樣式。

          占位元素

          占位元素就是在模板中的某個位置先占據(jù)一個位置,然后在元素插入到界面上的時候,在指定這個位置應(yīng)該顯示什么。

          <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">歡迎關(guān)注公眾號:更了不起的前端</p>
          </hello-user>

          這里用的用法與 Vue 的 slot 用法一致,不做過多的介紹。

          總結(jié)

          到這里 Web Components 的基本用法就介紹得差不多了,相比于其他的支持組件化方案的框架,使用 Web Components 有如下的優(yōu)點:

          • 瀏覽器原生支持,不需要引入額外的第三方庫;
          • 真正的內(nèi)部私有化的 CSS,不會產(chǎn)生樣式的沖突;
          • 無需經(jīng)過編譯操作,即可實現(xiàn)的組件化方案,且與外部 DOM 隔離;

          Web Components 的主要缺點就是標準可能還不太穩(wěn)定,例如文章中沒有提到的模板的模塊化方案,就已經(jīng)被廢除,現(xiàn)在還沒有正式的方案引入模板文件。而且原生的 API 雖然能用,但是就是不好用,要不然也不會出現(xiàn) jQuery 這樣的庫來操作 DOM。好在現(xiàn)在也有很多基于 Web Components 實現(xiàn)的框架,后面還會開篇文章專門講一講使用 Web Components 的框架 lit-htmllit-element

          好啦,今天的文章就到這里了,希望大家能有所收獲。


          瀏覽 74
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品乱码 | 夜色AV8888 | 亚洲国产精品久久久久 | 成人高清无码视频 | 中文字幕在线观看国产 |