前端未來趨勢之原生API:Web Components

ESnail發表於2020-11-06

宣告:未經允許,不得轉載。

Web Components 現世很久了,所以你可能聽說過,甚至學習過,非常瞭解了。但是沒關係,可以再重溫一下,溫故知新。

瀏覽器原生能力越來越強。

js

曾經的 JQuery,是前端入門必學的技能,是前端專案必用的一個庫。它的強大之處在於簡化了 dom 操作(強大的選擇器) 和 ajax(非同步) 操作。

現在原生 api querySelector()querySelectorAll()classList 等的出現已經大大的弱化了 dom 操作, fetch、基於 promiseaxios 已經完全替代了 ajax, 甚至更好用了,async-await 是真的好用。

You-Dont-Need-jQuery

css

css 前處理器(如 scssless) 是專案工程化處理 css 的不二選擇。它的強大之處是支援變數樣式規則巢狀函式

現在 css 已經支援變數(--var)了, 樣式規則巢狀也在計劃之中,函式嘛 calc() 也非常強大,還支援 attr() 的使用,還有 css-module 模組化。

不用預編譯,CSS直接寫巢狀的日子就要到了

w3c樣式規則巢狀 css-nesting-module

以前要製作酷炫複雜的 css 樣式及動畫,必須藉助 css 前處理器的變數、函式或者js才行,現在用 (css-doodle)[https://css-doodle.com/] 技術,實現的更酷、更炫。

css-doodle作品集

web components 元件化

Web Components 可以建立可複用的元件,未來的某一天拋棄現在所謂的框架和庫,直接使用原生 API 或者是使用基於 Web Components 標準的框架和庫進行開發,你覺得可能嗎?我覺得是可能的。

vue-lit

vue-lit,描述如下:

Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.

描述用到了 custom elements,而且瀏覽器控制檯 elements 的 DOM 結構中也含有 shadow-root。而 custom element 和 shadow DOM 是 web components 的重要組成。具體看下面 demo,

說明:本文文件示例,都是可以直接複雜到一個 html 文件的 body 中,然後直接在瀏覽中開啟預覽效果的。

  <my-component />
  
  <script type="module">
    import {
      defineComponent,
      reactive,
      html,
      onMounted
    } from 'https://unpkg.com/@vue/lit@0.0.2';

    defineComponent('my-component', () => {
      const state = reactive({
        text: 'Hello World',
      });
      
      function onClick() {
        alert('cliked!');
      }

      onMounted(() => {
        console.log('mounted');
      });

      return () => html`
        <p>
          <button @click=${onClick}>Click me</button>
          ${state.text}
        </p>
      `;
    })
  </script>
前端未來趨勢之原生API:Web Components

原始碼解讀

// lit-html 模板,提供 html 模板(簡單js表示式及事件繫結)、render 渲染能力
import { render } from 'https://unpkg.com/lit-html?module'
// reactivity 是vue3.0的核心,shallowReactive 淺響應,effect 可以理解為 watch,提供屬性響應及部分生命週期處理
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

let currentInstance

export function defineComponent(name, propDefs, factory) {
  if (typeof propDefs === 'function') {
    factory = propDefs
    propDefs = []
  }
  
  // 自定義元素 custom element,原生 API
  customElements.define(
    name,
    class extends HTMLElement {
      // 設定需要監聽的屬性
      static get observedAttributes() {
        return propDefs
      }
      constructor() {
        super()
        // 屬性接入 vue 的響應式
        const props = (this._props = shallowReactive({}))

        currentInstance = this
        // lit-html 的 html 生成的模板
        const template = factory.call(this, props)
        currentInstance = null

        // bm onBeforeMount
        this._bm && this._bm.forEach((cb) => cb())
        // shadowRoot,closed 表示不可以直接通過 js 獲取到定義的 customElement 操作 shadowRoot
        const root = this.attachShadow({ mode: 'closed' })

        let isMounted = false
        effect(() => {
          if (isMounted) {
            // _bu, onBeforeUpdate
            this._bu && this._bu.forEach((cb) => cb())
          }

          // 將 template 內容掛載到 shadowRoot 上
          render(template(), root)

          if (isMounted) {
            // _u,onUpdated
            this._u && this._u.forEach((cb) => cb())
          } else {
            isMounted = true
          }
        })
      }
      // 首次掛載到 dom 上後的回撥,onMounted
      connectedCallback() {
        this._m && this._m.forEach((cb) => cb())
      }
      // 解除安裝, onUnmounted
      disconnectedCallback() {
        this._um && this._um.forEach((cb) => cb())
      }
      // 屬性監聽
      attributeChangedCallback(name, oldValue, newValue) {
        this._props[name] = newValue
      }
    }
  )
}

function createLifecycleMethod(name) {
  return (cb) => {
    if (currentInstance) {
      ;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
    }
  }
}

export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')

export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

shallowReactive 原始碼,函式註釋已經表達的很清楚了,only the root level properties are reactive。物件只有根屬性響應,換言之即,淺響應,和淺拷貝類似。

/**
 * Return a shallowly-reactive copy of the original object, where only the root
 * level properties are reactive. It also does not auto-unwrap refs (even at the
 * root level).
 */
export function shallowReactive<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers
  )
}

effect 原始碼,粗略的可以看到裡面有 dep 依賴,還有 oldValue、newValue 處理。

通過分析,vue-lit 應該是將 vue3.0 的響應式和 web components 做的一個嘗試。用 lit-html 的原因時因為支援模板支援簡單js表示式及事件繫結(原生template目前只有slot插槽)

css-doodle

實際上,前面介紹的 css-doodle 也是一個 web component。是瀏覽器原生就支援的。

示例:藝術背景圖

  <script src="https://unpkg.com/css-doodle@0.8.5/css-doodle.min.js"></script>

  <css-doodle>
    :doodle { 
      @grid: 1x300 / 100vw 40vmin; 
      overflow: hidden;
      background: linear-gradient(rgba(63, 81, 181, .11), #673AB7);
    }

    align-self: flex-end;
    --h: @r(10, 80, .1);
    @random(.1) { --h: @r(85, 102, .1) }

    @size: 1px calc(var(--h) * 1%);
    background: linear-gradient(transparent, rgba(255, 255, 255, .4), transparent);
    background-size: .5px 100%;
    transform-origin: center 100%;
    transform: translate(@r(-2vmin, 2vmin, .01), 10%) rotate(@r(-2deg, 2deg, .01));
    
    :after {
      content: '';
      position: absolute;
      top: 0;
      @size: calc(2px * var(--h));
      transform: translateY(-50%) scale(.14);
      background: radial-gradient(@p(#ff03929e, #673ab752, #fffa) @r(40%), transparent 50%) 50% 50% / @r(100%) @lr() no-repeat;
    }
  </css-doodle>

dom 結構:

前端未來趨勢之原生API:Web Components

input、select 等內建 html 元素

input、select 也是 web component。但是是內建的,預設看不到 shadowRoot 結構,需要開啟瀏覽器控制檯的設定,勾選Show user agent shadow DOM,才可以在控制檯elements中看到其結構。

設定
前端未來趨勢之原生API:Web Components


dom 結構
前端未來趨勢之原生API:Web Components

web components 元件化由 3 部分組成。

  • Custom elements(自定義元素):一組JavaScript API,允許您定義custom elements及其行為,然後可以在您的使用者介面中按照需要使用它們。
  • Shadow DOM(影子DOM):一組JavaScript API,用於將封裝的“影子”DOM樹附加到元素(與主文件DOM分開呈現)並控制其關聯的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被指令碼化和樣式化,而不用擔心與文件的其他部分發生衝突。
  • HTML templates(HTML模板)<template><slot> 元素使您可以編寫不在呈現頁面中顯示的標記模板。然後它們可以作為自定義元素結構的基礎被多次重用。

Custom elements

使用者可以使用 customElements.define 自定義 html 元素。

customElements.define(elementName, class[, extendElement]);
  • elementName: 名稱不能是單個單詞,必須用短橫線分隔。
  • class: 用以定義元素行為的類,包含生命週期。
  • extendElement: 可選引數,一個包含 extends 屬性的配置物件,指定建立元素繼承哪個內建 HTML 元素

根據定義,得出有兩種 custom element:

  • Autonomous custom elements: 獨立元素,不繼承內建的HTML元素。和 html 元素一樣使用,例如<custom-info></custom-info>
  • Customized built-in elements: 繼承內建的HTML元素。使用先寫出內建html元素便籤,通過 is 屬性指定 custom element 名稱,例如<p is="custom-info"></p>

還有生命週期:

  • connectedCallback:當 custom element首次被插入文件DOM時,被呼叫。
  • disconnectedCallback:當 custom element從文件DOM中刪除時,被呼叫。
  • adoptedCallback:當 custom element被移動到新的文件時,被呼叫。
  • attributeChangedCallback: 當 custom element增加、刪除、修改自身屬性時,被呼叫。

示例:獨立元素。

  <button onclick="changeInfo()">更改內容</button>
  <custom-info text="hello world"></custom-info>

  <script>
    // Create a class for the element
    class CustomInfo extends HTMLElement {
      // 必須加這個屬性監聽,返回需要監聽的屬性,才能觸發 attributeChangedCallback 回撥
      static get observedAttributes() {
        return ['text'];
      }

      constructor() {
        // Always call super first in constructor
        super();

        // Create a shadow root
        const shadow = this.attachShadow({mode: 'open'});
        // Create p
        const info = document.createElement('p');
        info.setAttribute('class', 'info');

        // Create some CSS to apply to the shadow dom
        const style = document.createElement('style');
        console.log(style.isConnected);

        style.textContent = `
          .info {
            color: red;
          }
        `;

        // Attach the created elements to the shadow dom
        shadow.appendChild(style);
        console.log(style.isConnected);
        shadow.appendChild(info);
      }

      connectedCallback () {
        // 賦值
        this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text')
      }

      attributeChangedCallback(name, oldValue, newValue) {
        // TODO
        console.log(name, oldValue, newValue)
        this.shadowRoot.querySelector('.info').textContent = newValue
      }
    }

    // Define the new element
    customElements.define('custom-info', CustomInfo);

    function changeInfo() {
      document.querySelector('custom-info').setAttribute('text', 'custom element')
    }
  </script>

示例:繼承元素

<p is="custom-info" text="hello world"></p>

  <script>
    // Create a class for the element,extend p element
    class CustomInfo extends HTMLParagraphElement {
      constructor() {

        super();

        const shadow = this.attachShadow({mode: 'open'});
        const info = document.createElement('span');
        info.setAttribute('class', 'info');

        const style = document.createElement('style');
        console.log(style.isConnected);

        style.textContent = `
          .info {
            color: red;
          }
        `;

        shadow.appendChild(style);
        console.log(style.isConnected);
        shadow.appendChild(info);
      }

      connectedCallback () {
        this.shadowRoot.querySelector('.info').textContent = this.getAttribute('text')
      }
    }

    // Define the new element, extend p element
    customElements.define('custom-info', CustomInfo, {extends: 'p'});
  </script>

更多,請參考:Custom elements

Shadow DOM

Web components 的重要功能是封裝——可以將標記結構、樣式和行為隱藏起來,並與頁面上的其他程式碼相隔離,保證不同的部分不會混在一起,使程式碼更加乾淨、整潔。Shadow DOM 介面是關鍵所在,它可以將一個隱藏的、獨立的 DOM 附加到一個元素上。

附加到哪個元素上,和定義 custom element 時有關,如果是獨立元素,附加到 document body 上;如果是繼承元素,則附加到繼承元素上。

前端未來趨勢之原生API:Web Components

可以和操作普通 DOM 一樣,利用 API 操作 Shoadow DOM。

let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});

open 表示可以通過頁面內的 JavaScript 方法來獲取 Shadow DOM,如'document.querySelector('custom-info').shadowRoot'。反之,獲取不到。

更多,請參考:Shadow DOM

HTML templates

template 和 slot 元素可以建立出非常靈活的 shadow DOM 模板,來填充 custom element。 對於重複使用的 html 結構,可以起到簡化作用,非常有意義。

示例

<!-- 顯示 default text -->
  <custom-info></custom-info>

  <!-- 顯示 template info -->
  <custom-info>
    <span slot="info">template info</span>
  </custom-info>

  <template id="custom-info">
    <style>
      p {
        color: red;
      }
    </style>
    <p><slot name="info">default text</slot></p>
  </template>

  <script>
    class CustomInfo extends HTMLElement {
      constructor() {
        super();

        const shadowRoot = this.attachShadow({mode: 'open'});

        const customInfoTpCon = document.querySelector('#custom-info').content;
        
        shadowRoot.appendChild(customInfoTpCon.cloneNode(true));
      }
    }

    customElements.define('custom-info', CustomInfo);
  </script>

更多,請參考:HTML templates and slots

web components 示例

web component todolist

前端未來趨勢之原生API:Web Components

其他庫 todolist 大比拼

前端未來趨勢之原生API:Web Components

看圖,結果不言而喻。

總結

瀏覽器原生能力正在變得很強大。web component 值得擁抱一下。雖然 template 還不是很完善(不支援表示式),但這也只是白板上的一個黑點。

參考:

  1. 尤大 3 天前發在 GitHub 上的 vue-lit 是啥?
  2. Web Components
  3. web-components-todo

相關文章