我们当中那些曾经是Web开发人员超过几年的人使用了多个JavaScript框架编写了代码。有了所有的选择 - 反应,苗条,vue,ancular,lood-几乎是不可避免的。在跨框架工作时,我们必须处理的更令人沮丧的事情之一是重新创建所有这些低级UI组件:按钮,表,下拉次数等。特别令人沮丧的是,我们通常会在一个框架中定义它们,说出反应,但是如果我们想在友好中构建某些东西,则需要重写它们。或Vue。或固体。等等。
如果我们可以以框架 - 敏捷的方式定义这些低级UI组件,然后在框架之间重新使用它们,这会更好吗?当然会的!我们可以; Web组件是一种方式。这篇文章将向您展示如何。
截至目前,网络组件的SSR故事有点缺乏。声明性的影子DOM(DSD)是服务器端渲染的方式,但是,在撰写本文时,它与您喜欢的应用程序框架(例如Next,Remix或Sveltekit)并未集成在一起。如果这是您的要求,请务必检查DSD的最新状态。但是否则,如果SSR不是您使用的东西,请继续阅读。
Web组件本质上是您定义自己的HTML元素,例如
能够定义与任何特定组件不绑定的自定义HTML元素令人兴奋。但是这种自由也是一个限制。独立于任何JavaScript框架而存在意味着您无法真正与那些JavaScript框架进行交互。想想一个React组件,该组件获取一些数据,然后呈现其他一些反应组件,并沿数据传递。这实际上不能用作Web组件,因为Web组件不知道如何渲染React组件。
Web组件特别像叶片成分一样出色。叶片成分是在组件树中渲染的最后一件事。这些是接收一些道具并渲染一些UI的组件。这些不是坐在组件树中间的组件,而是沿着环境的数据传递数据等 - 不管哪个JavaScript框架都在为应用程序的其余部分供电。
与其构建一些无聊(和常见),例如按钮,让我们构建一些不同的东西。在我的上一篇文章中,我们研究了使用模糊图像预览以防止内容回流,并在加载图像时为用户提供体面的UI。我们查看了base64编码图像的模糊,降级版本的编码,并在加载真实图像时在UI中显示了这一点。我们还考虑使用称为Blurhash的工具生成令人难以置信的紧凑,模糊预览。
该帖子向您展示了如何生成这些预览并将其在React项目中使用。这篇文章将向您展示如何使用Web组件中的这些预览,以便可以通过任何JavaScript框架使用它们。
但是我们需要在跑步之前步行,因此我们首先要浏览一些微不足道和愚蠢的东西,以确切了解Web组件的工作原理。
这篇文章中的所有内容都将在没有任何工具的情况下构建Vanilla Web组件。这意味着代码将具有一些样板,但应相对易于遵循。诸如LIT或模板之类的工具设计用于构建Web组件,可用于删除大部分样板。我敦促您检查一下!但是对于这篇文章,我更喜欢更多的样板,以换取不必介绍和教授其他依赖性。
让我们构建JavaScript组件的经典“ Hello World”:柜台。我们将渲染一个值,并为该值增加一个按钮。简单而无聊,但它可以让我们查看最简单的Web组件。
为了构建Web组件,第一步是制作一个JavaScript类,该类从htmlelement继承:
类计数器扩展htmlelement {}
最后一步是注册Web组件,但前提是我们尚未注册它:
if(!customElements.get(“ counter-wc”)){ customElements.define(“ counter-wc”,计数器); }
而且,当然,将其渲染:
<counter-wc> </counter-wc>
两者之间的所有内容都是使Web组件可以做我们想做的任何事情。一种常见的生命周期方法是ConnectedCallback,当将我们的Web组件添加到DOM中时,该方法会触发。我们可以使用该方法渲染我们想要的任何内容。请记住,这是一个从HTMLElement继承的JS类,这意味着我们的此值是Web组件元素本身,使用了您已经知道和喜欢的所有正常DOM操纵方法。
最简单,我们可以做到这一点:
类计数器扩展htmlelement { connectedCallback(){ this.innerhtml =“ <div style="'颜色:绿色'">嘿</div>”; } } if(!customElements.get(“ counter-wc”)){ customElements.define(“ counter-wc”,计数器); }
…这会很好。
让我们添加一些有用的交互式内容。我们需要一个来保持当前数值和
constructor(){ 极好的(); const容器= document.createelement('div'); this.valspan = document.createelement('span'); const regrement = document.createelement('button'); rezement.InnerText ='regrement'; rezement.AddeventListener('click',()=> { 这个。#value = this。#currentValue 1; }); container.appendchild(this.valspan); container.AppendChild(document.createelement('br')); Container.AppendChild(增量); this.container =容器; } connectedCallback(){ this.appendchild(this.container); this.update(); }
如果您真的被手动DOM创建所困扰,请记住,您可以设置InnerHTML,甚至可以将模板元素作为Web组件类的静态属性创建,请克隆它,并插入新的Web组件实例中的内容。我可能没有考虑其他选项,或者您始终可以使用LIT或SCENN等WEB组件框架。但是对于这篇文章,我们将继续保持简单。
继续前进,我们需要一个可设置的JavaScript类属性,名为Value
#CurrentValue = 0; 设置#value(val){ 这个。#currentValue = val; this.update(); }
它只是设置器的标准类属性,以及第二个属性以保持值。一个有趣的转折是,我将私有JavaScript类属性语法用于这些值。这意味着在我们的Web组件之外没有人可以触摸这些值。这是所有现代浏览器中都支持的标准JavaScript,因此不要害怕使用它。
或者,如果您愿意,请随时称其为_value。最后,我们的更新方法:
更新() { this.valspan.innertext = this。#currentValue; }
有用!
显然,这不是您要大规模维护的代码。如果您想仔细观察,这是一个完整的工作示例。就像我说的那样,诸如LIT和模板之类的工具旨在使其更简单。
这篇文章不是深入研究Web组件。我们不会涵盖所有的API和生命周期;我们甚至不会覆盖阴影根或插槽。这些主题有无穷无尽的内容。我的目标是提供足够不错的介绍,以引发一些兴趣,并提供一些有用的指导,以实现与您已经知道和喜欢的流行JavaScript框架实际使用Web组件。
为此,让我们稍微增强我们的计数器Web组件。让我们接受颜色属性,以控制显示的值的颜色。而且,我们还可以接受增量属性,因此该Web组件的消费者一次可以将其增加2、3、4。为了驱动这些状态变化,让我们在Svelte Sandbox中使用我们的新计数器 - 我们会稍微做出反应。
我们将从与以前相同的Web组件开始,然后添加颜色属性。为了配置我们的Web组件以接受并响应属性,我们添加了一个静态观察到的属性,该属性返回我们的Web组件聆听的属性。
静态观察结果= [“颜色”];
有了将其添加,我们可以添加一个属性callback lifecycle方法,该方法将在设置或更新中列出的任何属性时运行。
attributechangedCallback(名称,oldValue,newValue){ 如果(name ===“颜色”){ this.update(); } }
现在,我们更新我们的更新方法以实际使用它:
更新() { this.valspan.innertext = this._currentValue; this.valspan.style.color = this.getAttribute(“ color”)|| “黑色的”; }
最后,让我们添加我们的增量属性:
增量= 1;
简单而谦虚。
让我们使用刚刚做的东西。我们将进入我们的Svelte应用程序组件,并添加类似的内容:
<script> 令颜色=“红色”; </script> 主要的 { 文字平衡:中心; } <ain> 红色 绿色 蓝色 <counter-wc color="{color}"> </counter-wc> </ain>
而且有效!我们的计数器渲染,增量和下拉列表更新颜色。如您所见,我们将颜色属性呈现在我们的Svelte模板中,当值更改时,Svelte会在我们的基础Web组件实例上处理调用SetAttribute的腿部工作。这里没有什么特别的:对于任何HTML元素的属性,这是同一件事。
通过增量道具,事情变得有些有趣。这不是我们Web组件上的属性;这是Web组件类的道具。这意味着需要在Web组件的实例上设置它。忍受我,因为事情会稍微简化。
首先,我们将在我们的Svelte组件中添加一些变量:
让增量= 1; 让Wcinstance;
我们的计数器组件的强大雄厚将使您增加1个,或以2:2:
regrement = 1}>增量1 regrement = 2}>增量2
但是,从理论上讲,我们需要获得我们的Web组件的实际实例。每当我们与React添加REF时,这都是同一件事。使用Svelte,这是一个简单的绑定:此指令:
<counter-wc bind color="{color}"> </counter-wc>
现在,在我们的Svelte模板中,我们聆听对组件的增量变量的更改,并设置基础Web组件属性。
$:{ 如果(wcinstance){ wcinstance.increment =增量; } }
您可以在此现场演示中对其进行测试。
显然,我们不想为我们需要管理的每个Web组件或道具执行此操作。如果我们只能像平时对组件道具那样在网络组件上设置增量,并且让它仅仅工作,那就不好了吗?换句话说,如果我们可以删除WCinstance的所有用法并使用此更简单的代码:
<counter-wc rezement="{rezement}" color="{color}"> </counter-wc>
事实证明我们可以。该代码有效; Svelte为我们处理了所有的腿部工作。在此演示中检查一下。这是几乎所有JavaScript框架的标准行为。
那么,为什么我向您展示设置Web组件的道具的手动方法呢?两个原因:了解这些事情的工作原理很有用,片刻之前,我说这适用于所有JavaScript框架。但是,有一个框架令人疯狂,不像我们刚刚看到的那样支持Web组件道具设置。
反应。地球上最受欢迎的JavaScript框架不支持与Web组件的基本互动。这是一个众所周知的问题,反应是独一无二的。有趣的是,这实际上是在React的实验分支中固定的,但是由于某种原因没有合并到第18版。也就是说,我们仍然可以跟踪它的进度。您可以通过现场演示自己尝试。
当然,解决方案是使用参考,抓住Web组件实例,并在该值更改时手动设置增量。看起来像这样:
导入React,{usestate,useref,useffect}来自'react'; 导入'./counter-wc'; 导出默认函数app(){ const [增量,setIncrement] = usestate(1); const [color,setColor] = usestate('red'); const wcref = useref(null); useeffect(()=> { wcref.current.increment =增量; },[增量]); 返回 ( <div> <div classname="“增量"> <button onclick="{()="> setIncrement(1)}>通过1 </button>增量 setIncrement(2)}>通过2 增量 </div> <select value="{color}" onchange="{(e)="> setColor(e.target.value)}> 红色 绿色 蓝色 </select> <counter-wc ref="{wcref}" engrement="{rezement}" color="{color}"> </counter-wc> </div> ); }
正如我们讨论的那样,对每个Web组件属性手动编码根本是根本不可扩展的。但是,一切都没有丢失,因为我们有几个选择。
我们有属性。如果您单击上面的React演示,则增量道具无法正常工作,但是颜色正确更改。我们不能用属性编码所有内容吗?可悲的是,不。属性值只能是字符串。这在这里已经足够了,我们将能够通过这种方法有所了解。诸如增量之类的数字可以从字符串转换。我们甚至可以json stringify/解析对象。但是最终,我们需要将函数传递到Web组件中,到那时我们将无法选择。
有一个古老的说法,您可以通过增加一定程度的间接来解决计算机科学中的任何问题(除了间接程度太多的问题外)。设置这些道具的代码非常可预测且简单。如果我们将其隐藏在图书馆中怎么办? LIT背后的聪明人有一个解决方案。此库将其提供给Web组件后,为您创建一个新的React组件,并列出所需的属性。虽然聪明,但我不喜欢这种方法。
我不喜欢将Web组件一对一的映射到手动创建的React组件,而是只有一个React组件,我们将Web组件标签名称传递给(在我们的情况下为Counter-WC),以及所有属性和属性,以及所有属性 - 对于此组件,为了使我们的Web组件呈现我们的Web组件,然后添加ref,然后添加probim and a probim and a probimute and a probimute and a probim utibute and a probimutibutibute and a probim utibute utibute。在我看来,这是理想的解决方案。我不知道这样做的库,但是创建应该很简单。让我们试一试!
这是我们要寻找的用法:
<wcwrapper wctag="“" counter-wc engrement="{rezement}" color="{color}"></wcwrapper>
WCTAG是Web组件标签名称;其余的是我们要通过的属性和属性。
这是我的实施方式:
导入React,{createElement,useref,useLayouteffect,emo}来自'react'; const _wcwrapper =(props)=> { const {wctag,儿童,... restprops} = props; const wcref = useref(null); uselayouteffect(()=> { const wc = wcref.current; for(const [键,值]的object..entries(restprops)){ 如果(wc中的键){ 如果(wc [key]!== value){ wc [key] = value; } } 别的 { if(wc.getAttribute(key)!== value){ wc.setAttribute(键,值); } } } }); 返回createElement(wctag,{ref:wcref}); }; 导出const wcwrapper = memo(_wcwrapper);
最有趣的行是:
返回createElement(wctag,{ref:wcref});
这就是我们在用动态名称中创建react元素的方式。实际上,这就是通常将JSX转移到的反应。我们所有的DIV都转换为CreateElement(“ DIV”)调用。通常,我们不需要直接调用此API,但是当我们需要时它在那里。
除此之外,我们还希望在我们传递给组件的每个道具中运行布局效果并循环。我们循环遍历所有这些,并检查它是否是检查Web组件实例对象及其原型链的属性,该链接将捕获所有在类原型上浮起的getters/setter。如果不存在此类属性,则假定它是一个属性。无论哪种情况,我们仅在值实际更改时将其设置。
如果您想知道为什么我们使用USELAYOUTEFFECT而不是使用效果,那是因为我们想在渲染内容之前立即运行这些更新。另外,请注意,我们对USELAYOUTEFFECT没有依赖性数组;这意味着我们希望在每个渲染上运行此更新。这可能是有风险的,因为React倾向于重新渲染很多。我通过将整个东西包裹在react.memo中来改善这一点。这本质上是现代版本的react.purecomponent,这意味着该组件只有在任何实际道具都更改时才会重新渲染 - 它可以通过简单的平等检查来检查这种情况是否发生。
这里唯一的风险是,如果您通过一个对象支柱,即您直接在不重新分配的情况下进行突变,那么您将不会看到更新。但这是高度灰心的,尤其是在React社区中,因此我不必担心。
在继续之前,我想喊一件最后一件事。您可能对使用方式不满意。同样,此组件是这样使用的:
<wcwrapper wctag="“" counter-wc engrement="{rezement}" color="{color}"></wcwrapper>
具体来说,您可能不喜欢将Web组件标签名称传递到
<wcwrapper wctag="“" counter-wc engrement="{rezement}" color="{color}"></wcwrapper>
…对此:
<counter-wc ref="{wcref}</pre"><p>您甚至可以编写一个codemod来在任何地方执行此操作,然后完全删除<wcwrapper>。实际上,刮擦:全局搜索和用正则替换可能会起作用。</wcwrapper></p> <h3>实施</h3> <p>我知道,似乎花了一段时间到达这里。如果您还记得,我们最初的目标是将您在上一篇文章中查看的图像预览代码中获取,并将其移至Web组件,以便在任何JavaScript框架中使用它。 React缺乏适当的Interop,为混合物增加了很多细节。但是,既然我们对如何创建Web组件有一个不错的处理,并使用它,实现几乎将是反气候的。</p> <p>我将在此处放置整个Web组件,并召集一些有趣的位。如果您想在行动中看到它,这是一个工作的演示。它将在我最喜欢的三种最喜欢的编程语言上的三本最喜欢的书之间切换。每本书的URL每次都会是唯一的,因此您可以看到预览,尽管您可能想在DevTools网络选项卡中插入事物,以真正看到事情发生。</p> <details><summary>查看整个代码</summary><pre rel="View full code" data-line="">类书房扩展了htmlelement { 静态观察到= ['url']; attributechangedCallback(名称,oldValue,newValue){ 如果(name ==='url'){ this.CreateMainImage(newValue); } } 设置预览(val){ this.previewel = this.createpreview(val); this.render(); } CreatePreview(val){ if(typeof val ==='string'){ 返回base64preview(val); } 别的 { 返回blurhashpreview(val); } } createMainImage(url){ this.loaded = false; const img = document.createelement('img'); img.alt ='书籍封面'; img.addeventlistener('load',()=&gt; { 如果(img === this.imageel){ this.loaded = true; this.render(); } }); img.src = url; this.imageel = img; } connectedCallback(){ this.render(); } 使成为() { const elementMaybe = this.poaded? this.imageel:this.previewel; SyncsingleChild(this,elementMaybe); } }
首先,我们注册我们感兴趣的属性并在变化时做出反应:
静态观察到= ['url']; attributechangedCallback(名称,oldValue,newValue){ 如果(name ==='url'){ this.CreateMainImage(newValue); } }
这导致创建我们的图像组件,仅加载时才会显示:
createMainImage(url){ this.loaded = false; const img = document.createelement('img'); img.alt ='书籍封面'; img.addeventlistener('load',()=> { 如果(img === this.imageel){ this.loaded = true; this.render(); } }); img.src = url; this.imageel = img; }
接下来,我们将拥有我们的预览属性,它可以是我们的base64预览字符串,也可以是我们的Blurhash数据包:
设置预览(val){ this.previewel = this.createpreview(val); this.render(); } CreatePreview(val){ if(typeof val ==='string'){ 返回base64preview(val); } 别的 { 返回blurhashpreview(val); } }
此辩护给我们需要的任何辅助功能:
函数base64preview(val){ const img = document.createelement('img'); img.src = val; 返回IMG; } 函数BlurhashPreview(Preview){ const canvasel = document.createelement('canvas'); const {w:width,h:height} = preview; canvasel.width = width; canvasel.height =高度; const pixels = decode(preview.blurhash,宽度,高度); const ctx = canvasel.getContext('2d'); const imagedata = ctx.createimagedata(宽度,高度); imagedata.data.set(像素); ctx.putimagedata(Imagedata,0,0); 返回画布; }
最后,我们的渲染方法:
connectedCallback(){ this.render(); } 使成为() { const elementMaybe = this.poaded? this.imageel:this.previewel; SyncsingleChild(this,elementMaybe); }
还有一些帮助所有东西将所有东西绑在一起的方法:
导出函数SyncsingleChild(容器,儿童){ const CurrentChild = container.firstelementChild; if(currentchild!== child){ ClearContainer(容器); if(child){ container.AppendChild(儿童); } } } 导出函数clearContainer(el){ 让孩子 while(((孩子= el.firstelementChild)){ El.Removechild(儿童); } }
它比我们在框架中构建它所需的样板要多一点,但是好处是,我们可以在我们想要的任何框架中重新使用它 - 尽管正如我们所讨论的那样,React现在需要一个包装器。
我已经提到了Lit的React包装器。但是,如果您发现自己使用模具,它实际上支持仅用于React的单独输出管道。微软的好人还创建了类似于LIT的包装器的东西,该包装器附加在快速Web组件库上。
正如我提到的,所有未命名React的框架都将处理为您设置Web组件属性。请注意,有些具有一些特殊的语法风味。例如,使用solid.js,
Web组件是Web开发环境中有趣的,通常是未充分利用的部分。它们可以通过管理UI或“ Leaf”组件来帮助您减少您对任何单个JavaScript框架的依赖。在将它们创建为网络组件(而不是Svelte或React组件)的同时,并不会那么符合人体工程学,但上升过程是它们将被广泛重复使用。
以上是构建可互操作的网络组件的详细内容。更多信息请关注PHP中文网其他相关文章!