这是第145篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~本文首发于政采云前端博客:WebComponents-LitElement实践
前言Google在2011年首次正式提出WebComponents组件化概念时,它主要依赖三个技术:CustomElement、ShadowDom、HTMLTemplates。直到2015年Google才真正投入生产进行使用,那时其他浏览器厂商还没有大规模支持这个特性,应用起来存在很大的兼容问题。
在这期间,Angular、React和Vue三大框架崛起,并且都有“组件化”这个功能,也形成了各自的生态圈,但都与框架强关联。由于这个原因,开发者对于WebComponents的呼声一直是只增不减。
直到今天,由于各大浏览器厂商的支持并结合polyfills,在使用WebComponents时,兼容性已经不是问题,开发者开始积极探索并实践WebComponents技术。
如何更好地应用WebComponents技术呢?有轻便的框架可以简化原生较为复杂的写法吗?那么我们来看看LitElement做了什么,能不能让WebComponents变得更好用些。
回顾通过阅读上篇文章如何基于WebComponents封装UI组件库,我们掌握了原生WebComponents的一些内容,包括:
三要素和生命周期;
基本的组件通信,包括如何利用observedAttributes属性监听和attributeChangedCallback生命周期获取最新属性和通过CustomEvent抛出自定义事件来模拟实现状态的“双向绑定”;
如何设计组件库;
如何在原生、React和Vue中优雅地使用我们封装的组件。
但使用WebComponents的原生写法确实存在一些不简洁的地方:
属性监听:observedAttributesAPI需要结合attributeChangedCallback生命周期,写起来代码量大;
组件通信时传入复杂数据类型:只能通过stringify后的attribute传递,特殊对象格式如Date,Function等传递起来会非常复杂,和现在的组件库能力上相比功能会比较弱,使用场景相对单一;
组件通信时双向绑定:需要结合自定义事件,写法会比较复杂。
为了更丰富的开发场景和更好的开发体验,LitElement把以上问题进行了归纳转化,即:
如何响应reactiveproperties的变化,并应用到UI上。
如何解决模板语法。
它用了两个核心库来解决这个问题,分别是lit-element和lit-html。
LitElement介绍基本内容Lit的核心是一个组件基类,它提供响应式、scoped样式和一个小巧、快速且富有表现力的声明性模板系统,且支持TypeScript类型声明。Lit在开发过程中不需要编译或构建,几乎可以在无工具的情况下使用。
我们知道HTMLElement是浏览器内置的类,LitElement?基类则是HTMLElement的子类,因此Lit组件继承了所有标准HTMLElement属性和方法。更具体来说,LitElement继承自ReactiveElement,后者实现了响应式属性,而后者又继承自HTMLElement。
创建Lit组件还涉及许多概念,我们一一了解。
定义一个组件Lit组件作为CustomElement的实现,并在浏览器中注册。
原生的写法主要是继承HTMLElement类并重写它的方法。而LitElement框架则是基于HTMLElement类二次封装了LitElement类,它将很多的写法通过一些语法糖的封装变得更简单了,极大地简化了这些代码。开发者只需继承LitElement类开发自己的组件然后通过浏览器原生方法customElements.define注册即可。
exportclassLitButtonextendsLitElement{/*...*/}customElements.define('lit-button',LitButton);当定义一个Lit组件时,就是定义了一个自定义HTML元素。因此,可以像使用任何内置元素一样使用新元素。
<lit-buttontype="primary"></lit-button>渲染组件具有render方法,该方法被调用以渲染组件的内容。
虽然Lit模板看起来像字符串插值,但Lit解析并创建一次静态HTML,然后只更新表达式中需要更改的值。
exportclassLitButtonextendsLitElement{/*...*/render(){//使用模板字符串,可以包含表达式returnhtml`<div><slotname="btnText"></slot></div>`;}}通常,组件的render()方法返回单个TemplateResult对象(与html标记函数返回的类型相同)。
TemplateResult对象:是lit-html接收模板字符串并经过它的html标记函数处理得到的一个纯值对象。
但是,它可以返回Lit可以渲染的任何内容,包括:
primitive原始类型值,如字符串、数字或布尔值。
由html函数创建的TemplateResult对象。
DOM节点。
任何受支持类型的数组或可迭代对象。
响应式propertiesDOM中property与attribute的区别:
attribute是HTML标签上的特性,可以理解为标签属性,它的值只能够是String类型,并且会自动添加同名DOM属性作为property的初始值;
property是DOM中的属性,是JavaScript里的对象,有同名attribiute标签属性的property属性值的改变也并不会同步引起attribute标签属性值的改变;
Lit组件接收标签属性attribute并将其状态存储为JavaScript的class字段属性或properties。响应式properties是可以在更改时触发响应式更新周期、重新渲染组件以及可选地读取或重新写入attribute的属性。每一个properties属性都可以配置它的选项对象。
exportclassLitButtonextendsLitElement{//在静态属性类字段中声明属性,Lit会处理为响应式属性staticproperties={type:{type:String,reflect:true,/*...其他选项属性...*/},other:{type:Object}};/*...*/}它的选项对象可以具有以下属性:
attribute:表示是否与property关联,或者attribute关联属性的自定义名称。默认值:true,表示property会与标签属性attribute进行关联。如果设置为false,则下面的converter转换器、reflect反射和type类型选项将被忽略。主要用来将attribute与property建立关联。
type:在将String类型的attribute转换为property时,Lit的默认属性转换器会将String类型解析为给定的类型。将property反映到attribute时反之亦然。如果设置了converter转换器,则将此字段传递给转换器。如果未指定类型,则默认转换器将其视为String类型。
converter:用于在attribute和property之间转换的自定义转换器。如果未指定,则使用默认属性转换器。主要用来决定attribute与property确定建立关联后如何进行数据转换,毕竟attribute只能是String类型而property却是可以自定义的类型,默认属性转换器则是依据property配置的type选项进行目标类型的转换。上例中表示接受的other属性的attribute后会序列化为目标Object类型。
hasChanged:每当设置属性时调用的函数以确定属性是否已更改,并应触发更新。如果未指定,LitElement将使用严格的不等式检查(newValue!==oldValue)来确定属性值是否已更改。
reflect:property属性值是否反映回关联的attribute属性。默认值:false,即property的改变不会主动引起attribute的改变。上例中表示接收type组件属性properties的改动会同步到对应attribute标签属性上。
state:设置为true以将property属性声明为内部state。内部state的改变也会触发更新,就像响应式属性property,但Lit不会为其生成attribute属性,用户不应从组件外部访问它。这些属性应标记为private或protected。还建议使用前导下划线(_)之类的约定来标识?JavaScript用户的private或protected属性。可以为state内部状态指定的唯一选项是hasChanged函数。
省略选项对象或指定一个空的选项对象等效于为所有选项指定默认值。
另外,Lit为每个响应式属性生成一个getter/setter对。当响应式属性发生变化时,组件会安排更新。Lit也会自动应用super类声明的属性选项。除非需要更改选项,否则不需要重新声明该属性。
样式组件模板被渲染到它的shadowroot。添加到组件的样式会自动作用于shadowroot,并且只会影响组件shadowroot中的元素。
ShadowDOM为样式提供了强大的封装。如果Lit没有使用ShadowDOM,则必须非常小心不要意外地为组件之外的元素设置样式,无论是组件的父组件还是子组件。这可能涉及编写冗长而繁琐的类名。通过使用ShadowDOM,Lit确保编写的任何选择器仅适用于Lit组件的shadowroot中的元素。
可以使用标记的模板css函数在静态styles类字段中定义scoped样式。
exportclassLitButtonextendsLitElement{//使用纯CSS为组件定义scoped样式staticstyles=css`.lit-button{display:inline-block;padding:4px20px;font-size:14px;line-height:1.5715;font-weight:400;border:1pxsolid#1890ff;border-radius:2px;background-color:#1890ff;color:#fff;box-shadow:02px#00000004;cursor:pointer;}`;/*...*/}如图同样应用了lit-button样式,但样式只对shodowroot中的部分起作用。
静态styles类字段的值可以是:
单个标记的模板文字。
staticstyles=css`...`;一组标记的模板文字。
staticstyles=[css`...`,css`...`];此外,styles也支持在样式中使用表达式、使用语句、继承父类样式、共享样式、使用unicode?escapes以及在模板template中使用样式等功能。Lit也提供了两个指令,classMap和styleMap,可以方便地在HTML模板中条件式的应用class和style。
import{LitElement,html,css}from'lit';import{classMap}from'lit/directives/class-map.js';import{styleMap}from'lit/directives/style-map.js';exportclassLitButtonextendsLitElement{staticproperties={classes:{},styles:{},};staticstyles=css`.lit-button{display:inline-block;padding:4px20px;font-size:14px;line-height:1.5715;font-weight:400;border:1pxsolid#1890ff;border-radius:2px;background-color:#1890ff;color:#fff;box-shadow:02px#00000004;cursor:pointer;}.someclass{color:#000;}.anotherclass{font-size:16px;}`;constructor(){super();this.classes={'lit-button':true,someclass:true,anotherclass:true};this.styles={fontFamily:'Roboto'};}render(){returnhtml`<divclass=${classMap(this.classes)}style=${styleMap(this.styles)}><slotname="btnText"></slot></div>`;}}customElements.define('lit-button',LitButton);生命周期Lit组件可以继承原生的自定义元素生命周期方法。但如果需要使用自定义元素生命周期方法,确保调用super类的生命周期,以保证父子组件生命周期的一致。
标准的自定义组件生命周期
constructor():创建元素时调用。适用于执行必须在第一次更新之前完成的一次性初始化任务。
connectedCallback():在将组件添加到文档的DOM时调用。适用于仅在元素连接到文档时才发生的任务。其中最常见的是将事件侦听器添加到元素节点。
disconnectedCallback():当组件从文档的DOM中移除时调用,用于移除对元素的引用。比如移除添加到元素节点的事件侦听器。
attributeChangedCallback():当元素的observedAttributes之一更改时调用。
adoptedCallback():当组件移动到新文档时调用。
connectedCallback(){super.connectedCallback()addEventListener('keydown',this._handleKeydown);}disconnectedCallback(){super.disconnectedCallback()window.removeEventListener('keydown',this._handleKeydown);}除了标准的自定义元素生命周期之外,Lit组件还实现了响应式更新周期。Lit异步执行更新,因此属性更改是批处理的,如果在请求更新后但在更新开始之前发生了更多属性更改,则所有更改都将在同一个更新中进行。当响应式prpperties属性发生变化或显式调用requestUpdate()方法时,将触发响应更新周期,它会将更改呈现给DOM。
响应式更新周期
第一阶段:触发更新
haschanged():在设置响应式属性时隐式调用。默认情况下hasChanged()会进行严格的相等性检查,如果返回true,则会安排更新。
requestUpdate():调用requestUpdate()来安排显式更新。如果需要在与属性无关的内容发生更改时更新和呈现元素,将很有用。
connectedCallback(){super.connectedCallback();this._timerInterval=setInterval(()=>this.requestUpdate(),1000);}disconnectedCallback(){super.disconnectedCallback();clearInterval(this._timerInterval);}第二阶段:执行更新
shouldUpdate():调用以确定是否需要更新周期。
willUpdate():在update()之前调用以计算更新期间所需的值。
update():调用以更新组件的DOM。
render():由update()调用,并应实现返回用于渲染组件DOM的可渲染结果(例如TemplateResult)。
第三阶段:完成更新
firstUpdated():在组件的DOM第一次更新后调用,紧接在调用updated()之前。
updated():每当组件的更新完成并且元素的DOM已更新和呈现时调用。
updateComplete():updateCompletePromise在元素完成更新时更新为resolved状态。
其他:
performUpdate():调用performUpdate()以立即处理挂起的更新。这通常不需要,但在需要同步更新的极少数情况下可以这样做。
hasUpdated():如果组件至少更新过一次,则hasUpdated属性返回true。仅当组件尚未更新时,才可以在任何生命周期方法中使用hasUpdated来执行工作。
getUpdateComplete():在执行updateComplete之前等待其他条件执行完成。
整个流程图示如下:
了解了基本的概念和内容,如果你做过任何现代的、基于组件的Web开发,你应该对Lit的系列概念和用法感到似曾相识并且容易上手。下面通过一些案例了解LitElement的其他特性。
传入复杂数据类型对于复杂数据的处理,为什么会存在这个问题,根本原因还是因为attribute标签属性值只能是String类型,其他类型需要进行序列化。在LitElement中,只需要在父组件模板的属性值前使用(.)操作符,这样子组件内部properties就可以正确序列化为目标类型。
<lit-buttontype="primary"></lit-button>0<lit-buttontype="primary"></lit-button>1这样可以支持各种类型数据的传递使用。
数据的双向绑定<lit-buttontype="primary"></lit-button>2<lit-buttontype="primary"></lit-button>3这里子组件接收了父组件的value属性,默认值设为了'default',在子组件内通过监听输入事件更新了value值,因为value属性配置了reflect为true,即可将属性值的改变反映回关联的attribute属性。
如图:input组件默认值为'default'并在紧接着输入'123'后,组件的标签属性value同时发生了变化。
这时在父组件通过获取子组件的attribute即可获得子组件同步改动的值。以此实现数据的双向绑定,但LitElement本身是单向的数据流。
指令使用指令是可以通过自定义表达式呈现方式来扩展Lit的函数。Lit包含许多内置指令,可帮助满足各种渲染需求:以组件缓存为例。
在更改模板而不是丢弃DOM时缓存渲染的DOM。在大型模板之间频繁切换时,可以使用此指令优化渲染性能。
<lit-buttontype="primary"></lit-button>4这个例子在模板中使用了语句表达式,再通过click事件切换组件时展示不同的模板内容;引入了cache指令函数,实现了DOM的缓存。
LitElement内置了大量的指令函数可以使用。
此外,它还有丰富的Mixins和Decoratrs等内容值得细细学习,在此不再做过多展开。
总结总的来说,LitElement在WebComponents开发方面有着很多比原生的优势,它具有以下特点:
简单:在WebComponents标准之上构建,Lit添加了响应式、声明性模板和一些周到的功能,减少了模板文件。
快速:更新速度很快,因为Lit会跟踪UI的动态部分,并且只在底层状态发生变化时更新那些部分——无需重建整个虚拟树并将其与DOM的当前状态进行比较。
轻便:Lit的压缩后大小约为5KB,有助于保持较小的包大小并缩短加载时间。
高扩展性:lit-html基于标记的template,它结合了ES6中的模板字符串语法,使得它无需预编译、预处理,就能获得浏览器原生支持,并且扩展能力强。
兼容良好:对浏览器兼容性非常好,对主流浏览器都能有非常好的支持。
结合这些点,基本可以满足项目开发中的大部分场景。
以上就是关于LitElement介绍的主要内容,更多内容可以前往官网学习了解,文中案例地址可以在此获得,同时推荐安装lit-pluginVSCode插件来更好的预览和改动代码。
尾声我们知道,W3C仿照jQuery的$函数,实现了querySelector()和querySelectorAll()方法并逐渐取代了jQuery快速选择DOM元素的功能,加速了jQuery的没落,带着前端迈向了新的阶段。那么随着WebComponents的不断发展,它会取代现有的前端框架吗?
现阶段来看,还并不会,因为WebComponents与各前端框架之间的关系是“共存”而非互斥,两者可以完美的互补。虽然前端框架React和Vue中组件化是其中非常重要的功能,但它们还有页面路由,数据绑定,模块化,CSS预处理器,虚拟DOM,Diff算法以及各种庞大的生态等功能。而Webcomponents所解决的仅仅是组件化这么一项功能。不论是React还是Vue,从它们的官方文档有关于WebComponents的说明中,都可以更好帮助我们理解它们与WebComponents之间的关系。
UI组件库shoelace
WiredElements
UI5WebComponents
Kor
参考资料WebComponents|MDN
webcomponents/polyfills|Github
LitElement|官方文档
logo设计
创造品牌价值
¥500元起
APP开发
量身定制,源码交付
¥2000元起
商标注册
一个好品牌从商标开始
¥1480元起
公司注册
注册公司全程代办
¥0元起
查
看
更
多