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 將自動處理適當的屬性 / 屬性之間的反射。屬性總是反射到相應的屬性。
具有原始值(
string
、boolean
或number
)的屬性會反射為屬性。
Vue 還會自動將宣告為
Boolean
或Number
型別的 props 轉換為所需的型別,當它們作為屬性(屬性始終是字串)設定時。例如,給定以下 props 宣告jsprops: { 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.0
或 vue-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平臺的最新標準保持同步,並且如果它使我們的工作變得更容易,我們將樂於利用平臺提供的任何東西。然而,我們的目標是提供既好用又實用的解決方案。這意味著我們必須以批判性的心態吸收新的平臺功能——這涉及到填補標準不足的地方。