跳轉到內容

Vue與Web元件

Web Components 是一組原生Web API的總稱,允許開發者建立可重用的自定義元素。

我們認為Vue和Web Components主要是互補的技術。Vue對使用和建立自定義元素都有很好的支援。無論是將自定義元素整合到現有的Vue應用中,還是使用Vue構建和分發自定義元素,你都不是孤軍奮戰。

在Vue中使用自定義元素

Vue在 Custom Elements Everywhere測試中得分完美100%。在Vue應用中消費自定義元素與使用原生HTML元素基本相同,但有一些需要注意的事項

跳過元件解析

預設情況下,Vue 會嘗試將非原生 HTML 標籤解析為已註冊的 Vue 元件,然後再將其作為自定義元素渲染。這將在開發期間導致 Vue 發出“無法解析元件”的警告。為了讓 Vue 知道某些元素應該被當作自定義元素處理並跳過元件解析,我們可以指定 compilerOptions.isCustomElement 選項

如果你在使用帶有構建設定的 Vue,該選項應透過構建配置傳遞,因為它是一個編譯時選項。

示例瀏覽器配置

js
// Only works if using in-browser compilation.
// If using build tools, see config examples below.
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')

示例 Vite 配置

js
// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // treat all tags with a dash as custom elements
          isCustomElement: (tag) => tag.includes('-')
        }
      }
    })
  ]
}

示例 Vue CLI 配置

js
// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => ({
        ...options,
        compilerOptions: {
          // treat any tag that starts with ion- as custom elements
          isCustomElement: (tag) => tag.startsWith('ion-')
        }
      }))
  }
}

傳遞 DOM 屬性

由於 DOM 屬性只能是字串,我們需要將複雜資料作為 DOM 屬性傳遞給自定義元素。當在自定義元素上設定屬性時,Vue 3 會自動使用 in 運算子檢查 DOM 屬性的存在,並且如果鍵存在,將優先將其值設定為 DOM 屬性。這意味著,在大多數情況下,如果你遵循 推薦的最佳實踐,你通常不需要考慮這個問題。

然而,可能存在一些罕見情況,資料必須作為 DOM 屬性傳遞,但自定義元素沒有正確地定義/反映該屬性(導致 in 檢查失敗)。在這種情況下,你可以使用 .prop 修飾符強制將 v-bind 繫結設定為 DOM 屬性。

template
<my-element :user.prop="{ name: 'jack' }"></my-element>

<!-- shorthand equivalent -->
<my-element .user="{ name: 'jack' }"></my-element>

使用 Vue 構建 Web 元件

自定義元素的主要好處是它們可以與任何框架一起使用,甚至可以在沒有框架的情況下使用。這使得它們非常適合分發元件,特別是當終端使用者可能不在使用相同的 frontend 棧時,或者當你想隔離最終應用程式與它使用的元件的實現細節時。

defineCustomElement

Vue 支援透過 defineCustomElement 方法使用與 defineComponent 相同的 Vue 元件 API 建立自定義元素。此方法接受與 defineComponent 相同的引數,但返回一個擴充套件 HTMLElement 的自定義元素建構函式。

template
<my-vue-element></my-vue-element>
js
import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // normal Vue component options here
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement only: CSS to be injected into shadow root
  styles: [`/* inlined css */`]
})

// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)

// You can also programmatically instantiate the element:
// (can only be done after registration)
document.body.appendChild(
  new MyVueElement({
    // initial props (optional)
  })
)

生命週期

  • 當元素的 connectedCallback 首次被呼叫時,Vue 自定義元素將在其陰影根內部掛載一個內部 Vue 元件例項。

  • 當元素的 disconnectedCallback 被呼叫時,Vue 將檢查元素是否在微任務週期後已從文件中分離。

    • 如果元素仍然在文件中,這是一個移動操作,元件例項將被保留;

    • 如果元素已從文件中分離,這是一個刪除操作,元件例項將被解除安裝。

屬性

  • 使用 props 選項宣告的所有屬性都將定義為自定義元素上的屬性。Vue 將自動處理適當的屬性 / 屬性之間的反射。

    • 屬性總是反射到相應的屬性。

    • 具有原始值(stringbooleannumber)的屬性會反射為屬性。

  • Vue 還會自動將宣告為 BooleanNumber 型別的 props 轉換為所需的型別,當它們作為屬性(屬性始終是字串)設定時。例如,給定以下 props 宣告

    js
    props: {
      selected: Boolean,
      index: Number
    }

    以及自定義元素的使用

    template
    <my-element selected index="1"></my-element>

    在元件中,selected 將被轉換為 true(布林值)和 index 將被轉換為 1(數字)。

事件

透過 this.$emit 或設定 emit 發射的事件將以原生的 CustomEvents 在自定義元素上分發。額外的事件引數(負載)將作為陣列在 CustomEvent 物件的 detail 屬性上公開。

插槽

在元件內部,可以使用 <slot/> 元素像往常一樣渲染插槽。然而,當消費生成的元素時,它只接受 原生插槽語法

  • 作用域插槽 不受支援。

  • 當傳遞命名插槽時,使用 slot 屬性而不是 v-slot 指令

    template
    <my-element>
      <div slot="named">hello</div>
    </my-element>

提供 / 注入

Provide / Inject API 和其 Composition API 等價物 也在 Vue 定義的自定義元素之間工作。然而,請注意,這僅在自定義元素之間工作。也就是說,Vue 定義的元素無法注入非自定義元素 Vue 元件提供的屬性。

應用級別配置

您可以使用 configureApp 選項配置 Vue 自定義元素的應用例項。

js
defineCustomElement(MyComponent, {
  configureApp(app) {
    app.config.errorHandler = (err) => {
      /* ... */
    }
  }
})

SFC 作為自定義元素

defineCustomElement 也與 Vue 單檔案元件(SFC)一起工作。然而,在預設的工具設定中,SFC 中的 <style> 仍然會在生產構建期間提取併合併到單個 CSS 檔案中。當使用 SFC 作為自定義元素時,通常希望將 <style> 標籤注入到自定義元素的 shadow root 中。

官方的 SFC 工具支援以“自定義元素模式”匯入 SFC(需要 @vitejs/plugin-vue@^1.4.0vue-loader@^16.5.0)。在自定義元素模式下載入的 SFC 將其 <style> 標籤內聯為 CSS 字串,並在元件的 styles 選項下公開。這將由 defineCustomElement 捕獲並在例項化時注入到元素的 shadow root 中。

要選擇此模式,只需在元件檔名字尾為 .ce.vue 即可。

js
import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'

console.log(Example.styles) // ["/* inlined css */"]

// convert into custom element constructor
const ExampleElement = defineCustomElement(Example)

// register
customElements.define('my-example', ExampleElement)

如果您想自定義在自定義元素模式下應匯入哪些檔案(例如,將 所有 SFC 視為自定義元素),可以將 customElement 選項傳遞給相應的構建外掛

Vue 自定義元素庫的技巧

使用 Vue 構建自定義元素時,元素將依賴於 Vue 的執行時。這取決於使用的功能,大約有 ~16kb 的基線大小成本。這意味著如果你只發送單個自定義元素,使用 Vue 不是很理想 - 你可能想使用純 JavaScript、petite-vue 或專門針對小執行時大小進行最佳化的框架。然而,如果您正在傳送帶有複雜邏輯的自定義元素集合,基線大小是合理的,因為 Vue 將允許每個元件以更少的程式碼編寫。你一起傳送的元素越多,這種權衡就越好。

如果自定義元素將用於同時使用 Vue 的應用程式中,您可以選擇將 Vue 從構建包外部化,以便元素將使用宿主應用程式相同的 Vue 版本。

建議匯出單個元素建構函式,以便您的使用者可以按需匯入並使用他們想要的標籤名註冊。您還可以匯出一個方便的函式來自動註冊所有元素。以下是一個 Vue 自定義元素庫的示例入口點:

js
import { defineCustomElement } from 'vue'
import Foo from './MyFoo.ce.vue'
import Bar from './MyBar.ce.vue'

const MyFoo = defineCustomElement(Foo)
const MyBar = defineCustomElement(Bar)

// export individual elements
export { MyFoo, MyBar }

export function register() {
  customElements.define('my-foo', MyFoo)
  customElements.define('my-bar', MyBar)
}

如果您有大量元件,您還可以利用構建工具功能,例如 Vite 的 glob 匯入 或 webpack 的 require.context 從目錄中載入所有元件。

Web 元件和 TypeScript

如果您正在開發應用程式或庫,您可能想對 Vue 元件進行型別檢查,包括定義為自定義元素的元件。

自定義元素使用原生 API 在全域性範圍內註冊,因此預設情況下,在 Vue 模板中使用時不會有型別推斷。為了為註冊為自定義元素的 Vue 元件提供型別支援,我們可以在 Vue 模板和/或 JSX 中使用 GlobalComponents 介面 來註冊全域性元件型別。

typescript
import { defineCustomElement } from 'vue'

// vue SFC
import CounterSFC from './src/components/counter.ce.vue'

// turn component into web components
export const Counter = defineCustomElement(CounterSFC)

// register global typings
declare module 'vue' {
  export interface GlobalComponents {
    Counter: typeof Counter
  }
}

Web 元件與 Vue 元件

一些開發者認為應避免框架專有的元件模型,並且僅使用自定義元素可以使應用程式“面向未來”。在這裡,我們將嘗試解釋為什麼我們認為這是一種過於簡單化的看法。

自定義元素和 Vue 元件之間確實存在一定程度的特性重疊:它們都允許我們定義具有資料傳遞、事件發射和生命週期管理的可重用元件。然而,Web 元件 API 相對低階且基礎。要構建實際的應用程式,我們需要許多額外的功能,而這些功能平臺並未涵蓋。

  • 一個宣告式和高效的模板系統;

  • 一個便於跨元件邏輯提取和重用的響應式狀態管理系統;

  • 一種在伺服器上渲染元件並在客戶端(SSR)水合它們的效能方式,這對於 SEO 和 Web Vitals 指標,如 LCP 非常重要。原生自定義元素的 SSR 通常涉及在 Node.js 中模擬 DOM,然後序列化已更改的 DOM,而 Vue SSR 會在可能的情況下編譯為字串連線,這要高效得多。

Vue 的元件模型正是為了滿足這些需求而設計的。

有了有能力的工程團隊,您可能在原生自定義元素之上構建等效的功能 - 但這也意味著您正在承擔長期維護自建框架的負擔,同時失去了像 Vue 這樣的成熟框架的生態系統和社群優勢。

還有一些框架是以自定義元素作為元件模型的基礎構建的,但它們不可避免地必須引入他們自己的解決方案來解決上述問題。使用這些框架意味著接受他們對如何解決這些問題的技術決策——儘管可能被宣傳,但這並不自動使你免受未來潛在變動的風險。

也有一些領域我們發現自定義元素存在侷限性

  • 渴望的槽位評估阻礙了元件組合。Vue的作用域槽是元件組合的一種強大機制,由於原生槽位的渴望特性,自定義元素無法支援。渴望的槽位還意味著接收元件無法控制何時或是否渲染槽位內容。

  • 如今,要將帶有作用域CSS的自定義元素與陰影DOM一起釋出,需要將CSS嵌入JavaScript中,以便在執行時將其注入到陰影根中。它們還會在SSR場景中導致標記中出現重複的樣式。在這個領域有一些平臺功能正在開發中——但截至目前,它們尚未得到普遍支援,並且仍存在生產效能/SSR問題需要解決。在此期間,Vue SFCs提供了CSS作用域機制,支援將樣式提取到純CSS檔案中。

Vue將始終與Web平臺的最新標準保持同步,並且如果它使我們的工作變得更容易,我們將樂於利用平臺提供的任何東西。然而,我們的目標是提供既好用又實用的解決方案。這意味著我們必須以批判性的心態吸收新的平臺功能——這涉及到填補標準不足的地方。

Vue和Web Components已載入