本文档主要描述 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 的跨端和跨技术栈特性,以及天然的样式隔离,能够解决长久以来中台前端组件存在的通病:
只能工作在 React16 技术栈
容易被业务环境样式污染
用起来麻烦
NEXT SDK 团队需要开发一款新的员工卡片前端组件,用于展示公司 / 组织内的人员信息如姓名、邮箱和工作城市等。
开发新员工卡片需要应对的几个问题:
跨技术栈和跨端使用
员工卡片会被多个商业化应用如飞书招聘、飞书 OKR 等集成,不同业务线可能使用 React15、React16 甚至 Vue.js 作为技术栈。
为了能够适应不同业务环境的技术栈,甚至于浏览器端、WebView 等环境也需要适配,新员工卡片的技术体系需要具备高兼容性。
组件编译体积要尽可能小
作为一款第三方 SDK,NEXT SDK 里的每一款组件都是一个单独的 JavaScript 文件,因此对组件文件大小提出了较高的要求,应尽量避免体积过大带来加载时间长的问题。
样式保护
如果无法做到组件自身的样式保护,势必会被不同业务环境的样式所影响,导致反复出现组件展示错误,造成不必要的返工。
易用性
新员工卡片技术体系需要实现组件化,以降低用户的使用成本,组件的声明、实例化、卸载以及副作用的管理都不是也不应该是业务线 RD 所关心的。
不侵入业务开发环境
不需要业务线 RD 修改自身项目构建流程,完全解耦
受应用场景限制,使用 React 开发组件会遇到如下几个问题:
仅能工作在 React 高版本应用中,若要在低版本 React 或 Vue.js 场景中使用,则需要投入额外的研发成本单独开发,开发成本和维护成本变高
受 EE NEXT SDK 集成方式限制(CDN 接入),员工卡片组件代码打包体积不能过大,否则会因加载时间过长而导致组件生效滞后。一般情况下,React 体系的 runtime 均比较大,所以在体积控制上并不是特别理想。
在进行纯 React 组件开发时,若要实现样式隔离或样式保护效果,通常会选择许多 css-in-js 方案如 emotion 。早期时,EE NEXT SDK 团队大量采用了 emotion 来保证组件样式不会被业务系统样式污染,但常常引入意料不到的样式泄露问题,最终放弃了继续使用该工具。
1 . 兼容性高。作为浏览器原生支持的技术标准,使用 Web Component 开发的前端组件天然能够无视技术栈和浏览器等差异,在大多数环境下均能正常使用。
2 . 体积小。Web Component 不需要像 React.js 、Vue.js 和 jQuery 等引入大体积 Runtime 文件,所有组件的声明、实例化和销毁均由浏览器负责,能够使得最终编译体积足够小。
3 . 样式隔离。Web Component 体系下的 Shadow DOM 能为组件元素提供天然的样式保护,Shadow DOM 下的子元素不会被外部样式选中,也不会受到外部 JavaScript 影响,最大程度降低了出现样式问题的概率。
4 . 使用简单。使用 Web Component 开发的组件可如普通 HTMLElement 元素一样使用,无需特殊处理,使用起来与日常操作 DOM API 较为接近。
5 . 稳健性高。尤其是在微前端架构下具有明显的优势,不会因为子应用频繁切换、元素重建和销毁造成 Web Component 组件出现副作用卸载不及时或组件无法重新挂载问题。
Web Component 开发离不开 CustomElement 和 Shadow DOM 两大主力,下面将对这两个概念进行简单介绍。
以下内容将用 CodeSandbox 演示 Web Component 组件简单开发流程,流程包括 CustomElement 和 Shadow DOM 使用,主要开发内容为:
新建一个 custom-
button,实现简单的按钮元素
使用 Shadow DOM 将组件内容封装起来,避免被外部样式影响
核心步骤
使用 ES6 Class 定义一个 CustomButton 组件
使用 customElements.define
注册上述组件,注册后便能够直接使用
在入口文件中使用定义的 CustomButton 组件
使用 DOM API 创建一个 CustomButton 实例并插入 DOM 节点中
核心步骤
1 . 在 custom-button 下挂载一个 Shadow Root
2 . 在 Shadow Root 内添加一个样式表,使其能够影响 Shadow DOM 内元素,同时不影响外部
https://codepen.io/weidongxin/pen/MWmNgOb
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 });
// 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 内的样式也不会泄露至外部,无法影响宿主环境中的样式,内外相互隔离
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 不会被渲染。
在 Shadow DOM 内使用 Slot 元素对 Light DOM 进行引用
仅有正确被引用的 Light DOM 部分才能被 “复制” 进入 Shadow DOM 中进行渲染
被引用的 Light DOM 部分样式依然受外部样式控制,因此能完全复刻 DOM 应有的样式
尽管 Shadow DOM 是属于 Web Component 标准中的一部分,但它并未被限制只能在 CustomElement 下使用,即便是普通 HTMLElement 也能够使用这一技术来实现样式保护。
Shadow DOM 只是一个具有样式隔绝功能的外衣,大多数情况下都不会造成宿主元素、 Shadow DOM 以及被引用的 Light DOM 的样式错误。一旦出现了明显的样式问题,需要以常规 CSS 样式处理的思路解决问题。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8