本文档主要描述 EE NEXT SDK 团队使用 Web Component 开发前端组件的技术决策,并穿插一些 Web Component 技术标准如 CustomElement、Shadow DOM 的优缺点介绍和踩坑经验。
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>
Web Component 是 W3C 支持的前端组件开发规范,包含 CustomElement、Shadow DOM、Slot 以及 HTML Template 等标准 API 和特性。
Web Component 能让前端开发人员实现跨技术栈和跨浏览器使用的前端组件,目前市面上占有率较高的浏览器均支持这一特性。
Web Component 的跨端和跨技术栈特性,以及天然的样式隔离,能够解决长久以来中台前端组件存在的通病:
容易被业务环境样式污染
用起来麻烦
NEXT SDK 团队需要开发一款新的员工卡片前端组件,用于展示公司 / 组织内的人员信息如姓名、邮箱和工作城市等。
开发新员工卡片需要应对的几个问题:
跨技术栈和跨端使用
员工卡片会被多个商业化应用如飞书招聘、飞书 OKR 等集成,不同业务线可能使用 React15、React16 甚至 Vue.js 作为技术栈。
为了能够适应不同业务环境的技术栈,甚至于浏览器端、WebView 等环境也需要适配,新员工卡片的技术体系需要具备高兼容性。
组件编译体积要尽可能小
作为一款第三方 SDK,NEXT SDK 里的每一款组件都是一个单独的 JavaScript 文件,因此对组件文件大小提出了较高的要求,应尽量避免体积过大带来加载时间长的问题。
样式保护
如果无法做到组件自身的样式保护,势必会被不同业务环境的样式所影响,导致反复出现组件展示错误,造成不必要的返工。
易用性
新员工卡片技术体系需要实现组件化,以降低用户的使用成本,组件的声明、实例化、卸载以及副作用的管理都不是也不应该是业务线 RD 所关心的。
不侵入业务开发环境
不需要业务线 RD 修改自身项目构建流程,完全解耦
受应用场景限制,使用 React 开发组件会遇到如下几个问题:
体积小。Web Component 不需要像 React.js 、Vue.js 和 jQuery 等引入大体积 Runtime 文件,所有组件的声明、实例化和销毁均由浏览器负责,能够使得最终编译体积足够小。
样式隔离。Web Component 体系下的 Shadow DOM 能为组件元素提供天然的样式保护,Shadow DOM 下的子元素不会被外部样式选中,也不会受到外部 JavaScript 影响,最大程度降低了出现样式问题的概率。
使用简单。使用 Web Component 开发的组件可如普通 HTMLElement 元素一样使用,无需特殊处理,使用起来与日常操作 DOM API 较为接近。
稳健性高。尤其是在微前端架构下具有明显的优势,不会因为子应用频繁切换、元素重建和销毁造成 Web Component 组件出现副作用卸载不及时或组件无法重新挂载问题。
Web Component 开发离不开 CustomElement 和 Shadow DOM 两大主力,下面将对这两个概念进行简单介绍。
以下内容将用 CodeSandbox 演示 Web Component 组件简单开发流程,流程包括 CustomElement 和 Shadow DOM 使用,主要开发内容为:
custom-
button,实现简单的按钮元素核心步骤
使用 customElements.define
注册上述组件,注册后便能够直接使用
在入口文件中使用定义的 CustomButton 组件
使用 DOM API 创建一个 CustomButton 实例并插入 DOM 节点中
核心步骤
https://codepen.io/weidongxin/pen/MWmNgOb
CustomElement 能够让开发者快速地创建可复用的 Web 前端组件,并能够如操作普通 HTMLElement 元素一样,新增、修改和销毁组件。
不引入额外的 Runtime ,组件文件体积可以控制得很小。
生命周期完备,利用生命周期能够方便地在组件内创建、管理和销毁副作用,减少 OOM 风险。
使用简单,开发人员可将 CustomElement 视为普通 HTMLElement 如 div、p 和 span 来使用,无需操心组件的创建和销毁。
开发难度低。仅需要具备 ES6 Class 使用经验便可以完成开发。
兼容性要求
一旦定义无法撤回
给 CustomElement 传递参数较为困难
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 可以将一个 “隐藏” 的、独立的 DOM 元素附着至其他元素下,ShadowDOM 内部的样式、行为都不会影响至外部。同样地,外部的样式、行为对 ShadowDOM 内部的影响 “有限”。
Shadow DOM 内的样式也不会泄露至外部,无法影响宿主环境中的样式,内外相互隔离
Shadow DOM 仅是一个具有样式隔离作用的 “DocumentFragment”,自身不会对子元素造成样式上的影响
Shadow DOM 天生的样式隔离能力,使得它较为适合用于充当第三方前端组件的保护伞,尤其适合 EE Next SDK 的应用场景。
尽管 Shadow DOM 能够阻止外部样式表选中其中的子元素,但不能避免 CSS 属性继承,如图所示:
https://codepen.io/weidongxin/pen/NWgpJPJ
Light DOM 这一术语通常出现于存在 Shadow DOM 的场景中,主要指代 Shadow DOM 宿主元素下的所有子孙节点,用于同 Shadow DOM 作区分。
类似于 Vue.js 中的插槽概念,能够将 Shadow DOM 宿主元素下的子孙节点(即 Light DOM)引用至 Shadow DOM 内,将被引用的 DOM 元素 “复刻” 一份并渲染。
若宿主元素下同时存在 Light DOM 和 Shadow DOM,通常情况下会观察到在页面上没有正确渲染出 Light DOM 。
https://codepen.io/weidongxin/pen/ExvyzVd
Shadow DOM 与 Light DOM 不能共存,若两者同时存在则通常情况下 Light DOM 不会被渲染。
仅有正确被引用的 Light DOM 部分才能被 “复制” 进入 Shadow DOM 中进行渲染
被引用的 Light DOM 部分样式依然受外部样式控制,因此能完全复刻 DOM 应有的样式
尽管 Shadow DOM 是属于 Web Component 标准中的一部分,但它并未被限制只能在 CustomElement 下使用,即便是普通 HTMLElement 也能够使用这一技术来实现样式保护。
Shadow DOM 只是一个具有样式隔绝功能的外衣,大多数情况下都不会造成宿主元素、 Shadow DOM 以及被引用的 Light DOM 的样式错误。一旦出现了明显的样式问题,需要以常规 CSS 样式处理的思路解决问题。