Web Component 實踐 - EE NEXT SDK
引言
本文檔主要描述 EE NEXT SDK 團隊使用 Web Component 開發前端組件的技術決策,並穿插一些 Web Component 技術標準如 CustomElement、Shadow DOM 的優缺點介紹和踩坑經驗。
EE NEXT SDK 是什麼?
EE NEXT SDK 是字節跳動 - 效率工程 - 中臺前端團隊開發的一款功能性前端業務組件合集,它能夠將效率工程中臺團隊的 AI 搜索、員工信息查詢、AI 信息增強等能力以前端組件形式對外輸出。
EE NEXT SDK 包含諸如搜索、員工卡片以及知識卡片等多個具有配套後端服務的前端業務組件,只提供 CDN 接入方式,因此業務線在使用 SDK 時通常會以如下形式接入。
<!document>
<head>
<script async src="https://{{cdn-host}}/{{cdn-path}}/user-card-v1.0.0.js"></script>
</head>
EE NEXT SDK 在 Web Component 上的實踐
Web Component 簡要介紹
Web Component 是 W3C 支持的前端組件開發規範,包含 CustomElement、Shadow DOM、Slot 以及 HTML Template 等標準 API 和特性。
Web Component 能讓前端開發人員實現跨技術棧和跨瀏覽器使用的前端組件,目前市面上佔有率較高的瀏覽器均支持這一特性。
爲什麼在 EE NEXT SDK 中使用 Web Component
Web Component 的跨端和跨技術棧特性,以及天然的樣式隔離,能夠解決長久以來中臺前端組件存在的通病:
-
只能工作在 React16 技術棧
-
容易被業務環境樣式污染
-
用起來麻煩
技術背景
NEXT SDK 團隊需要開發一款新的員工卡片前端組件,用於展示公司 / 組織內的人員信息如姓名、郵箱和工作城市等。
技術挑戰
開發新員工卡片需要應對的幾個問題:
跨技術棧和跨端使用
員工卡片會被多個商業化應用如飛書招聘、飛書 OKR 等集成,不同業務線可能使用 React15、React16 甚至 Vue.js 作爲技術棧。
爲了能夠適應不同業務環境的技術棧,甚至於瀏覽器端、WebView 等環境也需要適配,新員工卡片的技術體系需要具備高兼容性。
組件編譯體積要儘可能小
作爲一款第三方 SDK,NEXT SDK 裏的每一款組件都是一個單獨的 JavaScript 文件,因此對組件文件大小提出了較高的要求,應儘量避免體積過大帶來加載時間長的問題。
樣式保護
如果無法做到組件自身的樣式保護,勢必會被不同業務環境的樣式所影響,導致反覆出現組件展示錯誤,造成不必要的返工。
易用性
新員工卡片技術體系需要實現組件化,以降低用戶的使用成本,組件的聲明、實例化、卸載以及副作用的管理都不是也不應該是業務線 RD 所關心的。
不侵入業務開發環境
不需要業務線 RD 修改自身項目構建流程,完全解耦
傳統 React 組件開發方式能否滿足新員工卡片的技術場景?
-
受應用場景限制,使用 React 開發組件會遇到如下幾個問題:
-
僅能工作在 React 高版本應用中,若要在低版本 React 或 Vue.js 場景中使用,則需要投入額外的研發成本單獨開發,開發成本和維護成本變高
-
受 EE NEXT SDK 集成方式限制(CDN 接入),員工卡片組件代碼打包體積不能過大,否則會因加載時間過長而導致組件生效滯後。一般情況下,React 體系的 runtime 均比較大,所以在體積控制上並不是特別理想。
-
在進行純 React 組件開發時,若要實現樣式隔離或樣式保護效果,通常會選擇許多 css-in-js 方案如 emotion 。早期時,EE NEXT SDK 團隊大量採用了 emotion 來保證組件樣式不會被業務系統樣式污染,但常常引入意料不到的樣式泄露問題,最終放棄了繼續使用該工具。
Web Component 能夠解決什麼問題
-
兼容性高。作爲瀏覽器原生支持的技術標準,使用 Web Component 開發的前端組件天然能夠無視技術棧和瀏覽器等差異,在大多數環境下均能正常使用。
-
體積小。Web Component 不需要像 React.js 、Vue.js 和 jQuery 等引入大體積 Runtime 文件,所有組件的聲明、實例化和銷燬均由瀏覽器負責,能夠使得最終編譯體積足夠小。
-
樣式隔離。Web Component 體系下的 Shadow DOM 能爲組件元素提供天然的樣式保護,Shadow DOM 下的子元素不會被外部樣式選中,也不會受到外部 JavaScript 影響,最大程度降低了出現樣式問題的概率。
-
使用簡單。使用 Web Component 開發的組件可如普通 HTMLElement 元素一樣使用,無需特殊處理,使用起來與日常操作 DOM API 較爲接近。
-
穩健性高。尤其是在微前端架構下具有明顯的優勢,不會因爲子應用頻繁切換、元素重建和銷燬造成 Web Component 組件出現副作用卸載不及時或組件無法重新掛載問題。
最終決策
- 使用 Web Component 作爲員工卡片技術棧
Web Component 是什麼
Web Component 開發離不開 CustomElement 和 Shadow DOM 兩大主力,下面將對這兩個概念進行簡單介紹。
快速上手
以下內容將用 CodeSandbox 演示 Web Component 組件簡單開發流程,流程包括 CustomElement 和 Shadow DOM 使用,主要開發內容爲:
-
新建一個
custom-
button,實現簡單的按鈕元素 -
使用 Shadow DOM 將組件內容封裝起來,避免被外部樣式影響
創建 CustomElement
核心步驟
-
使用 ES6 Class 定義一個 CustomButton 組件
-
使用
customElements.define
註冊上述組件,註冊後便能夠直接使用 -
在入口文件中使用定義的 CustomButton 組件
-
使用 DOM API 創建一個 CustomButton 實例並插入 DOM 節點中
使用 Shadow DOM 用於保護樣式
核心步驟
-
在 custom-button 下掛載一個 Shadow Root
-
在 Shadow Root 內添加一個樣式表,使其能夠影響 Shadow DOM 內元素,同時不影響外部
https://codepen.io/weidongxin/pen/MWmNgOb
最終頁面結構
CustomElement
CustomElement 能夠讓開發者快速地創建可複用的 Web 前端組件,並能夠如操作普通 HTMLElement 元素一樣,新增、修改和銷燬組件。
優勢
-
CustomElement 定義的組件可在任何支持這一特性的瀏覽器或 WebView 中使用。
-
不引入額外的 Runtime ,組件文件體積可以控制得很小。
-
生命週期完備,利用生命週期能夠方便地在組件內創建、管理和銷燬副作用,減少 OOM 風險。
-
使用簡單,開發人員可將 CustomElement 視爲普通 HTMLElement 如 div、p 和 span 來使用,無需操心組件的創建和銷燬。
-
開發難度低。僅需要具備 ES6 Class 使用經驗便可以完成開發。
不足
-
兼容性要求
-
CustomElement 定義時需提供 ES6 Class 類型參數,無法傳遞 ES5 的構造函數
-
需要引入額外的 Polyfill 解決問題
-
一旦定義無法撤回
-
使用 customElements.define 定義過的組件無法取消聲明,即不存在 unRegistry 操作
-
無法使用不同的 ES6 Class 定義同名 CustomElement,僅首次定義生效
-
多團隊協作時有概率產生 CustomElement 命名衝突,需妥善處理
-
給 CustomElement 傳遞參數較爲困難
-
通常只能給 CustomElement 傳遞字符串類型 props
-
若要傳遞引用類型則需要顯式地獲取元素並將其視爲普通對象,方可進行賦值(代碼演示)
class CustomButton extends HTMLElement { xxx };
// 定義 custom-button 元素
window.customElements.define('custom-button', CustomButtom);
const customButton = document.body.querySelector('custom-button');
// 傳遞引用類型 props
customButton.message = { name: 'xxx' };
customButton.setParams({ age: 23 });
-
與 React.js 的 JSX 模板配合不夠,無法爲 CustomElement 添加自定義事件
-
難以優雅地對外暴露數據、事件和狀態,致使內部較爲封閉
// Home.tsx
// 以下示例無法在 custom-button 上註冊一個類型爲 popout 的 CustomEvent
const Home: FC = (props) => {
const onPopout = () => {};
return <custom-button onPopout={onPopout}></custom-button>
};
// 需進行如下改造
const Home: FC = (props) => {
const onPopout = () => {};
const ref = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!ref.current);
ref.current.addEventListener('popout', onPopout);
return () => {
ref.current.removeEventListener('popout', onPopout);
};
}, [ref]);
return <custom-button ref></custom-button>
};
Shadow DOM
Shadow DOM 可以將一個 “隱藏” 的、獨立的 DOM 元素附着至其他元素下,ShadowDOM 內部的樣式、行爲都不會影響至外部。同樣地,外部的樣式、行爲對 ShadowDOM 內部的影響 “有限”。
優點
-
Shadow DOM 的子元素不會被外部環境樣式所選中,起到了一定程度的樣式隔離作用
-
Shadow DOM 內的樣式也不會泄露至外部,無法影響宿主環境中的樣式,內外相互隔離
-
Shadow DOM 僅是一個具有樣式隔離作用的 “DocumentFragment”,自身不會對子元素造成樣式上的影響
作用
Shadow DOM 天生的樣式隔離能力,使得它較爲適合用於充當第三方前端組件的保護傘,尤其適合 EE Next SDK 的應用場景。
使用 Shadow DOM 時的常見誤區和解決方案
1. Shadow DOM 並非完全樣式隔絕
儘管 Shadow DOM 能夠阻止外部樣式表選中其中的子元素,但不能避免 CSS 屬性繼承,如圖所示:
示例
優化方法
- 在 Shadow DOM 最根部使用一個 div 元素用於阻斷所有來自祖先元素的 CSS 屬性繼承
https://codepen.io/weidongxin/pen/NWgpJPJ
2. Light DOM 、Shadow DOM 和 Slot
Light DOM 概念介紹
Light DOM 這一術語通常出現於存在 Shadow DOM 的場景中,主要指代 Shadow DOM 宿主元素下的所有子孫節點,用於同 Shadow DOM 作區分。
Slot
類似於 Vue.js 中的插槽概念,能夠將 Shadow DOM 宿主元素下的子孫節點(即 Light DOM)引用至 Shadow DOM 內,將被引用的 DOM 元素 “復刻” 一份並渲染。
Light DOM 和 Shadow DOM 不能同時被渲染
問題表現
若宿主元素下同時存在 Light DOM 和 Shadow DOM,通常情況下會觀察到在頁面上沒有正確渲染出 Light DOM 。
https://codepen.io/weidongxin/pen/ExvyzVd
問題原因
Shadow DOM 與 Light DOM 不能共存,若兩者同時存在則通常情況下 Light DOM 不會被渲染。
解決辦法
-
在 Shadow DOM 內使用 Slot 元素對 Light DOM 進行引用
-
僅有正確被引用的 Light DOM 部分才能被 “複製” 進入 Shadow DOM 中進行渲染
-
被引用的 Light DOM 部分樣式依然受外部樣式控制,因此能完全復刻 DOM 應有的樣式
3. Shadow DOM 並非 CustomElement 專屬
儘管 Shadow DOM 是屬於 Web Component 標準中的一部分,但它並未被限制只能在 CustomElement 下使用,即便是普通 HTMLElement 也能夠使用這一技術來實現樣式保護。
4. Shadow DOM 並不會造成莫名其妙的樣式問題
Shadow DOM 只是一個具有樣式隔絕功能的外衣,大多數情況下都不會造成宿主元素、 Shadow DOM 以及被引用的 Light DOM 的樣式錯誤。一旦出現了明顯的樣式問題,需要以常規 CSS 樣式處理的思路解決問題。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/F5P0hySgkjQ2LEowwQfSsw