淺析Web components的痛點

Allan91發表於2021-11-12

寫在前面

最近致力於研究 Web components(以下簡稱WC),並且也初有成效的拿到了一定的結果,但今天想回過頭來重新審視一下 WC。

WC 到底是什麼?

援引MDN上的解釋:

Web Components consists of several separate technologies. You can think of Web Components as reusable user interface widgets that are created using open Web technology. They are part of the browser and so they do not need external libraries like jQuery or Dojo. An existing Web Component can be used without writing code, simply by adding an import statement to an HTML page. Web Components use new or still-developing standard browser capabilities.

簡單的講,Web Component 就是把元件封裝成 html 標籤的形式,並且在使用時不需要寫額外的 js 程式碼。

元件是前端的發展方向,拋開周邊技術生態,單純看 React 和 Vue 都是元件框架。因此,WC 可以視為原生標籤的擴充/延伸,說到底,它依舊是一個標籤!

類似 <video></video> 標籤,相比於原生標籤,它多了更為豐富的樣式和可操作屬性。
image.png

谷歌公司由於掌握了 Chrome 瀏覽器,一直在推動瀏覽器的原生元件,即 Web Components API。相比第三方框架,原生元件簡單直接,符合直覺,不用載入任何外部模組,程式碼量小。貌似一切完美,似乎大有可以用來替換React、Vue之類的趨勢。

很多人也提出過將它與三大前端框架比較,比如《Web Component 和類 React、Angular、Vue 元件化技術誰會成為未來?》,其實它們是兩個領域的東西,不具有可比性。WC 最大的優勢在於 CSS 防汙染,瀏覽器原生支援的元件化實現;而 Vue 等 MVVM 框架注重資料分離和自動化繫結的實現。且 WC 中包含的 4 大 web api 是標準規範,使用上滯後性(比如新落地的規範往往要等幾年後才會被人拿出來使用),但 vue、react、ng 等框架不會。

目前存在的缺陷

與其它 web 框架一起使用存在一些小問題,會給開發體驗上造成一些困擾。

1、元件內部事件的回撥

比如,一個彈窗元件(<my-dialog></my-dialog>)中的確定按鈕,那麼它的事件是如何觸發的呢?

class myDialog extends HTMLElement {
  // ...
  connectedCallback() {
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `
      <div class="dialog">
        <div class="dialog-content">
          <div class="dialog-body">
            彈窗內容
          </div>

          <button id="okBtn">確定</button>
        </div>
      </div>
    `;

    shadowRoot.querySelector('#okBtn').addEventListener('click', () => {
      // 元件內部定義事件
      this.dispatchEvent(new CustomEvent('okBtnFn'));
    });
  }
}

customElements.define('my-dialog', myDialog);

現在方案是 custom element 內部自定義事件 new CustomEvent(),外部用 addEventListener監聽。這樣的寫法是很醜陋的,彷彿又回到了原生 JS 寫應用的時代。

<my-dialog></my-dialog>

<script>
  export default {
    created() {
      document.addEventListener('okBtnFn', function(){
        // 點選彈窗按鈕,觸發回撥事件
      });
    }
  }
</script>

2、元件樣式覆蓋

對於開發者來說,難免會遇到需要調整元件內部樣式的時候。無論你是使用antdvant還是使用其它元件庫,但 WC 的 CSS 防汙染機制導致你很難修改內部樣式。這需要你付出一些代價來變相的修改內部樣式,具體方式我在上一篇文章中有寫《Web Components中引入外部CSS的 8 種方法》,其實是很繁瑣,且不符合開發者直覺的。

3、元件內部資源相對路徑問題

就目前來說,任何直接基於 Custom Element v1, Template 和 HTML Import 的元件都無法做到完全資源獨立 —— 在不知道使用方環境且不給使用方增加額外限制的情況下使用內部封裝的任何資原始檔。比如如果你有一個自定義 icon 元件:

class MyIcon extends HTMLElement {
    static get observedAttributes() { return ['name','size','color'] }
    constructor() {
        super();
        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.innerHTML = `
            <svg class="icon" id="icon" aria-hidden="true" viewBox="0 0 1024 1024">
                <use id="use"></use>
            </svg>
        `
    }

    attributeChangedCallback (name, oldValue, newValue) {
        if( name == 'name' && this.shadowRoot){
            // 如果使用的專案中,根目錄沒有 icon.svg 檔案,那就 gg
            this.use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `./icon.svg#icon-${newValue}`);
        }
    }
}

customElements.define('my-icon', MyIcon);

如果使用的專案中,根目錄沒有 icon.svg 檔案,那就 gg。如果你在這裡使用 cdn 路徑,就會出現跨域問題。

4、form表單類元件 value 獲取問題

Shadow DOM 中包含有 <input>、<textarea> 或 <select> 等標籤的 value 不會在 form 表單中自動關聯。

示例程式碼:

// web component
class InputAge extends HTMLElement {
  constructor() {
    super();
  }
  
  // connect component
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'closed' });
    shadow.innerHTML = `<input type="number" placeholder="age" min="18" max="120" />`;
  }
}

// register component
customElements.define( 'input-age', InputAge );

WC 元件被使用後

<form id="myform">
  <input type="text" name="your-name" placeholder="name" />
  <input-age name="your-age"></input-age>

  <button>submit</button>
</form>

<script>
 const form = document.getElementById('myform');

  form.addEventListener('submit', e => {
    
    e.preventDefault();
    console.log('Submitted data:');

    const data = new FormData(form);
    for (let nv of data.entries()) {
      console.log(`  ${ nv[0] }: ${ nv[1] }`);
    }

  });
</script>

提交的時候無法獲取 input-agevalue。當然會有解決方案,但會很複雜。

點選檢視

其它表單標籤獲取 value 解決方案參考:

image.png

點選檢視

5、其它

此外,缺少資料繫結和狀態管理也是 WC 存在的缺陷,此處不再贅述。

寫在後面

  • WC 指在豐富 HTML 的 DOM 特性,讓 HTML 擁有更強大的複用能力
  • WC 可以直接當做原生標籤,在任何前端框架和無框架中執行
  • 結合當下的主流技術棧來說,WC 當前主要問題在於複雜的元件中,資料通訊和事件傳遞存在一定使用成本
  • 相容問題,比如可以覆蓋內部樣式的 :part 方法

以上。

相關文章