从去年年底到今年,字节和腾讯各自开源了自己的组件库,各自都有特点,字节的arco只支持react,semi支持react和vue,腾讯的tdesign支持react、vue、angular和小程序,对我而言,我想说的是,UI本身是不分平台、框架、环境的设计表达,但是国内几乎所有的design最终都表达为组件库。我们只能阅读组件库,来掌握这套UI的设计细节。(当然,tdesign等提供了figma素材,可以帮助我们。)但是我们都知道前端的发展迅猛,如果以后有新的技术栈需要使用,而UI库又没有提供,那么就意味着巨大的建设成本。我之前试图使用react来包装vue的组件,以达到vue组件库用到react应用中的效果,但是,最终有两个问题让我放弃,一是要同时加载vue和react库文件,二是包装之后需要多出一层DOM节点。于是乎,利用web components实现跨框架的UI组件库瞬间就进入了我的脑海中。
什么是Web Components
Web Components不是只一个东西,它是一套东西的总称,是一套web标准解决方案,我们主要会用到其中的3个关键要素:customElements、shadowDOM和slot。另外template, html imports也是web components的内容,只是对我们来说它没有那么重要。
Web Components是一套基于HTML原生创建组件的标准。和react或vue的组件不同,它所创建的组件不需要依赖任何框架,就可以在web中运行。这样可以帮助web开发者起到很好的封装作用,把一些业务中常用的UI元素或交互封装为一个HTML元素来在页面中使用。
基于Web Components的UI组件库
基于该标准实现的UI组件库已经被广泛运用了,基于polymer创建的一系列组件,可以扩展你在原生的元素使用中的不足;手绘风的Wired Elements,提供了非常有意思的效果;MDS是Morningstar的企业级设计语言,其实现也只有web components这一种;xy-ui是一个完全用web components实现的UI组件库,可以在react或vue中使用。
设计语言是一家企业自身产品VI的沉淀,和开发本身并非强绑定的,对设计语言的实现,更多是为接手的设计师准备的,如果一套设计语言里只有UI组件,那么当这套UI组件的技术栈过时之后,就很难复原设计原则和规范。而跟随web 标准演进设计语言,不失为一种比较保险的策略。
由于web components是一套web标准,那么也就意味着所有在web上进行开发的应用,都可以使用这套标准,不管react也好,vue也好,在web平台上运行时都依赖于web本身的能力,因此也是支持web components的。所以,web components是解决UI组件库需要在不同框架或技术栈中使用时,是一种不错的思路。
问题
虽然基于web components发布的UI组件库可以很好的做到跨技术栈支持,但是也需要解决一些问题,它是原生的,当我们在react中使用customElements时,还需要通过ref来执行addEventListener来完成一些交互,比较麻烦。另外,react和vue都是基于props的变化来触发界面更新,而标准customElements却没有提供直接监听attributes变化的能力。总之,要让web components在react或vue中按照框架的范式运作,还是比较麻烦的。
基于Web Components的框架
为了让web components能够具备响应式能力,我们必须借助某些框架来实现。基于这些框架,我们可以通过更便捷的方式创建web components,不仅如此,还能解决上面提到的属性监听等问题。附带的,shadowDOM还能原生支持css样式隔离,如果你对此不是很了解,可以阅读我前两天写的文章《ShadowDOM css样式处理详解》,揭开shadowDOM css的神秘面纱。
来自google家的两个框架polymer和lit从两个角度帮我们快速实现自己的响应式web components。polymer注重customElements的实现过程,开发者对customeElements的接口比较了解的话对polymer使用起来会比较顺手。lit比较前卫,模板语法、装饰器等都有体现。另外,腾讯开源的omi也是非常优秀的框架,它基于jsx,需要一套构建工具。
基于Web Components的Tdesign Button
按钮是一个UI组件库的门面,动效是一个组件库的灵魂。TDesign作为腾讯官方出品的跨框架组件库,在不同的前端框架上保持了相同的UI呈现,但区分不同的框架代价就是API命名不得不进行妥协,这也就意味着无法在技术栈切换时做到真正的无缝切换。我基于sfcjs创建了tdesign的button和icon,你如果仅仅是喜欢tdesgin的button,你可以通过下面代码示例,快速使用:
<script src="https://unpkg.com/sfcjs"></script>
<script src="https://unpkg.com/tdesign-sfc/dist/t-button"></script>
然后就可以在整个网站的任意地方使用t-button组件:
<t-button theme="primary">提交</t-button>
你可以通过Demo页面看到所有的按钮效果。
多种颜色主题
点击动效
我们在使用组件库时,按需使用是非常常见的需求。例如现在我们只需要用到button,那么我们就只需要引入button的代码。当然,在一些构建系统中,我们也可以通过babel插件等方法做到。我的意思是,作为UI库的发布者,应该充分考虑到开发者们的需求。
将组件web components化之后,我们可以以更标准的使用方法在任意技术栈中使用它们。当然,原生的使用方法,对于我们写习惯了react或vue而言,会有一些别扭,因为我们无法直接在组件元素上使用@click这样的标记。
为什么要使用sfcjs作为框架?
要制作web components其实并不复杂,只要掌握我前文提到的那些技术点。而且类似polymer等框架也提供了不错的选择,为什么我用sfcjs这个不是专门为创建web components而设计的框架呢?
我想,一个组件的设计和交互,应该通过一个组件文件表达完整。除了.vue文件可以做到,也只有sfcjs了,对比之下,sfcjs可以飞快的创建web components,而且sfcjs完全基于web components实现,具有真正的css隔离能力,所以选择了它。
为什么要选择single file component这种代码组织形式呢?我认为这是最有效的实现设计沉淀的方式。我们阅读其他形式的组件库,发现这些组件库的开发者们,更多是以实现功能为目标,因此代码组织上并没有把一个组件的内容集中表达出来。当过了很久,我们再去阅读代码时,会把整个代码库作为整体去理解代码之间的联系,却忽略了作为组件,在一个组件内应该表达出这个组件设计时的理念和规则。而通过sfc,我们把组件的结构、样式、交互效果都表达了出来,这让我们可以更快更准确的理解这一个组件。
实现层面
用sfcjs开发组件需要做两件事:1. 创建sfc;2. 特权化组件。以创建t-icon组件为例,我简单讲一下我是怎么实现一个tdesign组件的web components化的。
第一步:撰写sfc(icon.htm文件)。
<script>
import props from 'sfc:props';
const { name, size = '1em', color } = props;
</script>
<style>
:host {
font-size: initial;
color: initial;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" (style)="'width:' + size + '; height:' + size + ';' + (color ? 'color:' + color : '')">
<g (if)="name === 'add-circle'">
<path d="M7.5 8.5H4.5V7.5H7.5V4.5H8.5V7.5H11.5V8.5H8.5V11.5H7.5V8.5Z" fill="currentColor" fill-opacity="0.9">
</path>
<path d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14Z" fill="currentColor" fill-opacity="0.9"></path>
</g>
<g (if)="name === 'add'">
<path d="M7.34998 8.64998V12.5H8.64998V8.64998H12.5V7.34998H8.64998V3.5H7.34998V7.34998H3.5V8.64998H7.34998Z" fill="currentColor" fill-opacity="0.9"></path>
</g>
<!-- 其他省略 -->
</svg>
用sfcjs写组件就是这样一个.htm文件,和.vue类似,包含script、style、html三个部分。它有比较特殊的一些语法来,有点像svelte。但这些都是它的用法,本文不详细深入sfcjs,我想表达的是,对于icon这个组件而言,我们可以通过这个sfc直接了当的一眼就完成阅读,知道它的样式和交互(虽然在icon里面显得很弱)。这也就是我为什么强调用single file component的方式组织UI组件代码的原因。如果你去阅读antd的源码或elementui的源码,很难如此轻松的阅读组件的设计细节。
第二步:特权化为custom element.
由于sfcjs并不是专门为创建web components准备的,所以创建上面那个sfc之后,我们只能通过 <t-sfc src="icon.htm" :name="add"></t-sfc> 的方式使用它,虽然也可以,但是和直接用 <t-icon name="add"></t-icon> 相比,就差的很远。因此,我用sfcjs提供的privilege方法进行再度封装,privilege可以实现封装一个custom element的目标。
SFCJS.privilege('t-icon', {
src: 'icon.htm',
props: [
'name',
'size',
'color',
],
events: [
'click',
],
});
其中src所指向的就是我们第一步所创建的sfc。这样就完成了这个组件的创建。默认情况下,我们不需要对组件进行构建打包也可以运行,sfcjs可以通过ajax请求文件后实时编译sfc,只要src那里路径写对就可以。所以,如果你现在就想马上在你的电脑上尝试,你可以创建一个icon.html文件,内容如下:
<!DOCTYPE html>
<t-icon name="search"></t-icon>
<script src="https://unpkg.com/sfcjs"></script>
<script>
SFCJS.privilege('t-icon', {
src: 'https://unpkg.com/tdesign-sfc/packages/t-icon/index.htm',
props: [
'name',
'size',
'color',
],
events: [
'click',
],
});
</script>
接下来在电脑上打开这个文件,就可以发现界面上呈现出一个搜索图标。你可以在这里找到所有图标。
结语
阿里、腾讯、字节等等大厂陆续开源了自己的组件库,也就意味着UI组件已经成为应用构建时的一种标准化的技术选型,这对正在打磨自己的UI组件库的小伙伴们既是挑战,也是机会。我尝试使用web components来封装(重编)tdesign,只是一种实验,虽然你现在可以使用tdesign-sfc,但是它包含的内容比较少,只有两个组件。我只是希望在这个过程中,告诉组件库的开发者们,可以尝试一种基于标准的方案来创建组件库,以可以在不同的框架里面都可以用。同时,基于sfc的理念,我们把单一组件的代码放在一个文件中管理,有助于我们在长期维护中,理解组件的设计全貌。