WebComponents-LitElement实践
发布时间:2025-05-24 00:36:53 发布人:远客网络
一、WebComponents-LitElement实践
这是第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。
Lit的核心是一个组件基类,它提供响应式、scoped样式和一个小巧、快速且富有表现力的声明性模板系统,且支持TypeScript类型声明。Lit在开发过程中不需要编译或构建,几乎可以在无工具的情况下使用。
我们知道HTMLElement是浏览器内置的类,LitElement?基类则是HTMLElement的子类,因此Lit组件继承了所有标准HTMLElement属性和方法。更具体来说,LitElement继承自ReactiveElement,后者实现了响应式属性,而后者又继承自HTMLElement。
创建Lit组件还涉及许多概念,我们一一了解。
Lit组件作为CustomElement的实现,并在浏览器中注册。
原生的写法主要是继承HTMLElement类并重写它的方法。而LitElement框架则是基于HTMLElement类二次封装了LitElement类,它将很多的写法通过一些语法糖的封装变得更简单了,极大地简化了这些代码。开发者只需继承LitElement类开发自己的组件然后通过浏览器原生方法customElements.define注册即可。
当定义一个Lit组件时,就是定义了一个自定义HTML元素。因此,可以像使用任何内置元素一样使用新元素。
组件具有render方法,该方法被调用以渲染组件的内容。
虽然Lit模板看起来像字符串插值,但Lit解析并创建一次静态HTML,然后只更新表达式中需要更改的值。
通常,组件的render()方法返回单个TemplateResult对象(与html标记函数返回的类型相同)。
TemplateResult对象:是lit-html接收模板字符串并经过它的html标记函数处理得到的一个纯值对象。
但是,它可以返回Lit可以渲染的任何内容,包括:
primitive原始类型值,如字符串、数字或布尔值。
由html函数创建的TemplateResult对象。
任何受支持类型的数组或可迭代对象。
DOM中property与attribute的区别:
attribute是HTML标签上的特性,可以理解为标签属性,它的值只能够是String类型,并且会自动添加同名DOM属性作为property的初始值;
property是DOM中的属性,是JavaScript里的对象,有同名attribiute标签属性的property属性值的改变也并不会同步引起attribute标签属性值的改变;
Lit组件接收标签属性attribute并将其状态存储为JavaScript的class字段属性或properties。响应式properties是可以在更改时触发响应式更新周期、重新渲染组件以及可选地读取或重新写入attribute的属性。每一个properties属性都可以配置它的选项对象。
它的选项对象可以具有以下属性:
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样式。
如图同样应用了lit-button样式,但样式只对shodowroot中的部分起作用。
此外,styles也支持在样式中使用表达式、使用语句、继承父类样式、共享样式、使用unicode?escapes以及在模板template中使用样式等功能。Lit也提供了两个指令,classMap和styleMap,可以方便地在HTML模板中条件式的应用class和style。
Lit组件可以继承原生的自定义元素生命周期方法。但如果需要使用自定义元素生命周期方法,确保调用super类的生命周期,以保证父子组件生命周期的一致。
constructor():创建元素时调用。适用于执行必须在第一次更新之前完成的一次性初始化任务。
connectedCallback():在将组件添加到文档的DOM时调用。适用于仅在元素连接到文档时才发生的任务。其中最常见的是将事件侦听器添加到元素节点。
disconnectedCallback():当组件从文档的DOM中移除时调用,用于移除对元素的引用。比如移除添加到元素节点的事件侦听器。
attributeChangedCallback():当元素的observedAttributes之一更改时调用。
adoptedCallback():当组件移动到新文档时调用。
除了标准的自定义元素生命周期之外,Lit组件还实现了响应式更新周期。Lit异步执行更新,因此属性更改是批处理的,如果在请求更新后但在更新开始之前发生了更多属性更改,则所有更改都将在同一个更新中进行。当响应式prpperties属性发生变化或显式调用requestUpdate()方法时,将触发响应更新周期,它会将更改呈现给DOM。
haschanged():在设置响应式属性时隐式调用。默认情况下hasChanged()会进行严格的相等性检查,如果返回true,则会安排更新。
requestUpdate():调用requestUpdate()来安排显式更新。如果需要在与属性无关的内容发生更改时更新和呈现元素,将很有用。
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就可以正确序列化为目标类型。
这样可以支持各种类型数据的传递使用。
这里子组件接收了父组件的value属性,默认值设为了'default',在子组件内通过监听输入事件更新了value值,因为value属性配置了reflect为true,即可将属性值的改变反映回关联的attribute属性。
如图:input组件默认值为'default'并在紧接着输入'123'后,组件的标签属性value同时发生了变化。
这时在父组件通过获取子组件的attribute即可获得子组件同步改动的值。以此实现数据的双向绑定,但LitElement本身是单向的数据流。
指令是可以通过自定义表达式呈现方式来扩展Lit的函数。Lit包含许多内置指令,可帮助满足各种渲染需求:以组件缓存为例。
在更改模板而不是丢弃DOM时缓存渲染的DOM。在大型模板之间频繁切换时,可以使用此指令优化渲染性能。
这个例子在模板中使用了语句表达式,再通过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之间的关系。
webcomponents/polyfills|Github
二、深度解析vue3.2中defineCustomElement底层原理
WebComponents
WebComponents是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。相当于是浏览器原生的定义组件的方式,不用通过vue或者react这些框架实现组件的定义
WebComponents是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。相当于是浏览器原生的定义组件的方式,不用通过vue或者react这些框架实现组件的定义
customElements是Window对象上的一个只读属性,接口返回一个CustomElementRegistry对象的引用,可用于注册新的customelements,或者获取之前定义过的自定义元素的信息。
HTML内容模板(<template>)元素是一种用于保存客户端内容机制,该内容在加载页面时不会呈现,但随后可以(原文为maybe)在运行时使用JavaScript实例化。将模板视为一个可存储在文档中以便后续使用的内容片段。虽然解析器在加载页面时确实会处理<template>元素的内容,但这样做只是为了确保这些内容有效;但元素内容不会被渲染。
content获取DocumentFragment元素片段的内容相当于通过document.createDocumentFragment()创建的元素片段,
ShadowDOMAPI的ShadowRoot接口是一个DOM子树的根节点,它与文档的主DOM树分开渲染。
你可以通过使用一个元素的Element.shadowRoot属性来检索它的参考,假设它是由Element.attachShadow()创建的并使mode设置为open.
通过Element.attachShadow()挂载影子DOM
三、js冲突怎么解决
1、a.最容易出现的就是js的命名冲突
2、变量有全局变量和局部变量当全局变量变量和局部变量名称一致时,就会js冲突,由于变量传递数值或地址不同就会产生JavaScript错误,甚至死循环。
3、函数中有很多方法,不同的js之间可能函数名一样,这就使得程序执行时不知道改调用那个方法或者哪个方法执行后的结果,最终导致程序加载失败。
4、一般是命名导致JS冲突。解决方法主要是查找在加载的js中是否具有相同命名的情况,然后修改命名
5、如果在js中调用了window.onload= function(),同时在页面上又有body onload语句,会导致window.onload事件覆盖掉body onload事件而出现错误.
6、解决方法:attachEvent给onload添加所需运行的函数。
7、c.浏览器不兼容问题,虽然这个不属于js冲突但是也常见的js的原因之一
8、(1)现有问题:现有代码中存在许多document.formName.item("itemName")这样的语句,不能在Fx下运行
9、(2)解决方法:改用document.formName.elements["elementName"]
10、(1)现有问题:现有代码中许多集合类对象取用时使用(),IE能接受,Fx不能。
11、(2)解决方法:改用[]作为下标运算。如:document.forms("formName")改为document.forms["formName"]。又如:document.getElementsByName("inputName")(1)改为document.getElementsByName("inputName")
12、(1)现有问题:使用window.event无法在Fx上运行
13、(2)解决方法: Fx的event只能在事件发生的现场使用,此问题暂无法解决。可以这样变通:
14、<inputtype="button" name="someButton" value="提交
15、"onclick="javascript:gotoSubmit()"/>
16、<script language="javascript">
17、<input type="button" name="someButton" value="提交"
18、onclick="javascript:gotoSubmit(event)"/>
19、<script language="javascript">
20、evt= evt? evt:(window.event? window.event: null);
21、此外,如果新代码中第一行不改,与老代码一样的话(即gotoSubmit调用没有给参数),则仍然只能在IE中运行,但不会出错。所以,这种方案tpl部分仍与老代码兼容。
22、④HTML对象的id作为对象名的问题
23、(1)现有问题:在IE中,HTML对象的ID可以作为document的下属对象变量名直接使用。在Fx中不能。
24、(2)解决方法:用getElementById("idName")代替idName作为对象变量使用。
25、⑤用idName字符串取得对象的问题
26、(1)现有问题:在IE中,利用eval(idName)可以取得id为idName的HTML对象,在Fx中不能。
27、(2)解决方法:用getElementById(idName)代替eval(idName)。
28、⑥变量名与某HTML对象id相同的问题
29、(1)现有问题:在Fx中,因为对象id不作为HTML对象的名称,所以可以使用与HTML对象id相同的变量名,IE中不能。
30、(2)解决方法:在声明变量时,一律加上var,以避免歧义,这样在IE中亦可正常运行。
31、此外,最好不要取与HTML对象id相同的变量名,以减少错误。
32、(1)现有问题:在IE中,event对象有x, y属性,Fx中没有。
33、(2)解决方法:在Fx中,与event.x等效的是event.pageX。但event.pageX IE中没有。
34、故采用event.clientX代替event.x。在IE中也有这个变量。
35、event.clientX与event.pageX有微妙的差别(当整个页面有滚动条的时候),不过大多数时候是等效的。如果要完全一样,可以稍麻烦些:
36、mX= event.x? event.x: event.pageX;
37、(3)其它:event.layerX在IE与Fx中都有,具体意义有无差别尚未试验。
38、(1)现有问题:在IE中可以用window.testFrame取得该frame,Fx中不行
39、(2)解决方法:在frame的使用方面Fx和ie的最主要的区别是:如果在frame标签中书写了以下属性:
40、<frame src="xx.htm" id="frameId" name="frameName"/>
41、那么ie可以通过id或者name访问这个frame对应的window对象,而Fx只可以通过name来访问这个frame对应的window对象,例如如果上述frame标签写在最上层的window里面的htm里面,那么可以这样访问
42、 ie: window.top.frameId或者window.top.frameName来访问这个window对象
43、Fx:只能这样window.top.frameName来访问这个window对象
44、另外,在Fx和ie中都可以使用window.top.document.getElementById("frameId")来访问frame标签,并且可以通过window.top.document.getElementById("testFrame").src='xx.htm'来切换frame的内容,也都可以通过window.top.frameName.location='xx.htm'来切换frame的内容
45、⑨在Fx中,自己定义的属性必须getAttribute()取得
46、⑩在Fx中没有parentElement parement.children而用
47、parentNode parentNode.childNodes
48、childNodes的下标的含义在IE和Fx中不同,Fx使用DOM规范,childNodes中会插入空白文本节点。
49、一般可以通过node.getElementsByTagName()来回避这个问题。当html中节点缺失时,IE和Fx对parentNode的解释不同,例如:
50、Fx中input.parentNode的值为form,而IE中input.parentNode的值为空节点
51、Fx中节点没有removeNode方法,必须使用如下方法node.parentNode.removeChild(node)
52、(1)现有问题:在IE中不能使用const关键字。如const constVar= 32;在IE中这是语法错误。
53、(2)解决方法:不使用const,以var代替。
54、Fx的body在body标签没有被浏览器完全读入之前就存在,而IE则必须在body完全被读入之后才存在
55、在js中如果书写url就直接写&不要写&例如var url='xx.jsp?objectName=xx&objectEvent=xxx';
56、frm.action= url那么很有可能url不会被正常显示以至于参数没有正确的传到服务器
57、一般会服务器报错参数没有找到,当然如果是在tpl中例外,因为tpl中符合xml规范,要求&书写为&
58、(1)现有问题:在Fx中,所有节点均有nodeName值,但textNode没有tagName值。在IE中,nodeName的使用好象
59、有问题(具体情况没有测试,但我的IE已经死了好几次)。
60、(2)解决方法:使用tagName,但应检测其是否为空。
61、元素属性:IE下input.type属性为只读,但是Fx下可以修改
62、document.getElementsByName()和document.all[name]的问题
63、(1)现有问题:在IE中,getElementsByName()、document.all[name]均不能用来取得div元素(是否还有其它不能取的元素还不知道)。