单篇文章中的网络组件
Web Components 是 W3C 推动的一项标准,旨在丰富 HTML 的 DOM 特性,让 HTML 有更强大的复用能力。
一、没有对比,没有伤害
放眼前端技术发展历程,代码复用和模块化是永远绕不开的话题,我们看看前端三大件(CSS,HTML,JavaScript)表现如何。
CSS 远有 @import url
语法,近有 sass,less,stylus,css modules 等一大堆方案实现,原生的 CSS 体系也支持定义变量和使用了:
:root {
--first-color: #488cff;
--second-color: #ffff8c;
}
#firstParagraph {
background-color: var(--first-color);
color: var(--second-color);
}
JavaScript 不遑多让, IIFE 模式和原型模式是其逻辑复用的基本能力。近几年 CMD,AMD,CommonJS 等各种规范对 JS 模块化持续加成,ES6 的定版更是官方爸爸的大力加持。相比之下,HTML 就略显惨淡。
早年间 HTML 似乎有一个 imports 语法支持导入 html 片段 <link rel="import" href="myfile.html">
,但笔者没使用过,不做过多讨论。但到 MDN 查了一下发现已经不再支持这个语法了。。。HTML Imports
当然,在复用和组件化的潮流中 HTML 也不是那么的格格不入,毕竟时下最流行的 Vue,React,Ng 都是实现自定义 HTML 组件强有力框架。然而框架各自为阵,语法和使用方式不同,学习成本不同,总不是长久之计。
正所谓,天下大势,合久必分,分久必合。
Web Components 能否带来新的大一统局势呢,让我们细细看来。
二、Web Components 基础
1.三板斧
首先要明确的是 Web Components 是一项标准,目前它包含三项主要技术,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突。
这三项技术分别为(摘自 mdn):
- Custom elements(自定义元素): 一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。
- Shadow DOM(影子 DOM):一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
-
HTML templates(HTML 模板):
<template>
和<slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
闲话少叙,让我们看看该如何使用它们。
2.先看个栗子 ????
假设我们要实现一个博客列表页,大概长这个样子:
按照组件化的思想,我们希望每个博文都是单个组件,可单独使用。首先我们使用 template
构造博客组件内容结构:
<template id="tpl">
<style>
article {width: 20%; margin: 20px auto; border: solid 1px gray; padding: 8px;}
header {
background: lightblue; color: #fff;
font-size: 24px; border: solid 1px lightblue;
}
</style>
<article>
<header>博客标题</header>
<section>博客内容博客内容博客内容博客内容博客内容博客内容...</section>
</article>
</template>
然后使用 customElement
创建自定义组件:
class SelfDiv extends HTMLElement {
constructor() {
super()
document.body.appendChild(tpl.content.cloneNode(true))
}
}
customElements.define('my-blog', SelfDiv)
最后,在页面上使用 <my-blog />
标签就能出现我们想要的内容。详细代码请猛戳这里。至此,HTML 原生自带的自定义组件功能完全满足需求,市面上各种令人头大的前端框架都可以抛之脑后啦,完结撒花✿✿ヽ(°▽°)ノ✿!!!
============================= 一条讲道理的分割线 ======================
然而事情并没这么简单,仔细查看 SelfDiv
组件会发现我们只是单纯的把 template
内容挂在 body 上,一点也不优雅;而且说好的 shadow DOM
呢,咋也没见着。不急,接下来让我们慢慢丰富这个例子。
首先,思考以下两个问题:
a.怎么把组件脱离真实 DOM,做到内部独立?
b.组件如何做到内容自定义?
3.shadow DOM 改造
针对第一个问题,其核心诉求是希望自定义组件做到内部独立(样式,事件等),借用 shadow 可以实现这一目标。定义 shadow DOM 只需要一个方法即可:Element.attachShadow(shadowRootInit)
,其中 shadowRootInit
是一个字典对象,包含两个值:mode 和 delegatesFocus;具体内容详见 attachShadow。
我们将刚才的组件进行改造:
class SelfDiv extends HTMLElement {
constructor() {
super()
let shadow = this.attachShadow({ mode: 'open' })
shadow.appendChild(tpl.content.cloneNode(true))
}
}
改造关键点是借助 attachShadow 创建了一个 shadowRoot 对象,然后把模板内容挂在 shadowRoot 上。为了验证 shadow 的隔离性,我们在页面填写一些其他内容:
<my-blog />
<article>shadow 外面的 article 元素</article>
可以看到页面效果如下:
细心的朋友会发现,自定义元素外的 article 并没有应用上边框和背景样式效果。
article {width: 20%; margin: 20px auto; border: solid 1px gray; padding: 8px;}
这是因为整个 shadowRoot 对象和文档对象完全隔离的,结构大致如下:
至此,我们的组件脱离了原生 DOM 的魔爪,可*灵活地配置在页面任何地方。
4.template 和 slot 的妙用
再看第二个问题:组件如何做到内容自定义?
说白了咱现在的组件不能自定义博客标题和内容,完全不可用。这个时候就要请出 slot 插槽。插槽具有一个 name 属性作为标识,通常在 template 中定义,在使用时可以根据标识对其填充任何 HTML 元素。定义略显晦涩,直接看代码:
<template id="tpl">
<style>
article {width: 20%; margin: 20px auto; border: solid 1px gray; padding: 8px;}
header {
background: lightblue; color: #fff;
font-size: 24px; border: solid 1px lightblue;
}
</style>
<article>
<header><slot name='title'>博客标题</slot></header>
<section><slot name='cont'>博客内容博客内容博客内容博客内容博客内容博客内容...</slot></section>
</article>
</template>
还是刚才的 template 结构,只不过这里我们把标题用 name='title'
的具名 slot 包裹起来,而博客内容用的是 name='cont'
的 slot。然后在页面这样使用自定义组件:
<my-blog>
<span slot='title'>第一篇博文</span>
<p slot='cont'>这是第一篇博文内容这是第一篇博文内容这是第一篇博文内容这是第一篇博文内容...</p>
</my-blog>
<my-blog>
<span slot='title'>第二篇博文</span>
<p slot='cont'>这是第二篇博文内容...</p>
</my-blog>
页面展示的效果如下,可以看到组件已经做到完全自定义。完整例子可以猛戳这里:codepen.io/xutaogit/pe…
三、Web Components 进阶
相信看完上文整个例子,我们已经对 Web Compoents 的基本功能大致掌握了。但要把它们真正投入生产使用,总觉得还差强人意。你可能会吐槽:
- 现在的模板都太简单了,我想随意操作模板的内容,给它添加事件啥的。。。
- 自定义组件确实有了,但我想在组件不同的阶段做不同的事情,怎么搞呢?
- slot 看着挺有用,但它使用规则定义的太死了,不太灵活的亚子
- 。。。
问题看似很多,但归根到底还是没把”三板斧“吃透,接下来我们将它抬上熔炉,一起来”炼炼“看。
Custom elements
前文我们只讲到如何创建一个自定义元素,然后将其挂载到真实 DOM 或者 shadow DOM 上。接下来进一步看看它还有哪些其他特别的功能。
1.定义和使用
你可以通过 CustomElementRegistry.define() 返回的绑定在 window 对象上的 customElements
实例进行元素自定义。其语法为 customElements.define(name, constructor, options);
- name : 元素名。只能用短线连接的英文字符,不能是单个单词。例:name='my-comp'
- constructor:元素构造器。必须继承自 HTMLElement 或其子类(例如:HTMLParagraphElement 等)
- options:【可选】控制元素如何定义,只支持一个选项
extends
,用于指明定义的元素继承自何种类型元素。(例如:{ extends: 'p' })
前两个参数应该比较好理解,第三个参数特别要注意:extends 指明的元素和 constructor 继承的元素保持一致。代码演示如下:
class ExtendP extends HTMLParagraphElement {
constructor (){
super()
let iRoot = this.attachShadow({mode:'open'})
let span = document.createElement('span')
span.innerHTML = "扩展自 HTMLParagraphElement"
iRoot.appendChild(span)
}
}
customElements.define('extend-p', ExtendP, {extends: 'p'})
这里 extends 指明的元素是 HTMLParagraphElement,在调用 define 方法的时候 extends 必须指明为 p,同时组件也只能用在 p 元素上,方式上有别于一般的自定义元素:
<p is="extend-p"></p>
以上,总结出两条自定义组件的使用方法:
- 扩展自 HTMLElement 的自定义元素,直接使用
<元素名></元素名>
- 扩展自特定子类 Element 的元素,使用 is 属性访问
<扩展Element is="元素名"></扩展Element>
实例相关代码:codepen.io/xutaogit/pe…
2.生命周期
在custom element的构造函数中,可以指定多个不同的回调函数,它们将会在元素的不同生命时期被调用:
- connectedCallback:当 custom element 首次被插入文档DOM时,被调用。
- disconnectedCallback:当 custom element 从文档DOM中删除时,被调用。
- adoptedCallback:当 custom element 被移动到新的文档时,被调用。
- attributeChangedCallback: 当 custom element 增加、删除、修改自身属性时,被调用。
具体使用方法可以参考官网文档,实例代码搬运至此,可点击体验:生命周期。 特别要说明的一点是 attributeChangedCallback
方法的触发依赖于 get 函数 observedAttributes 的实现。
attributeChangedCallback(name, oldValue, newValue) {
console.log('name 为属性名,需要通过 observedAttributes 监听才能触发获得值');
// do something
}
static get observedAttributes() {return ['attr1', 'attr2']; } // return 返回一个数组,可用于监听多个属性
Shadow DOM
这一“板斧”算是 WebComponents 最重要的特性了,有了它才真正实现了 HTML 的复用 —— 将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离。查看之前博文例子可以看到 Shadow DOM 接口返回一个 shadow-root
节点,在此节点内部的样式和行为都不会影响外面元素,可作为微前端子应用间样式隔离的选择方案之一,这属于另一话题暂且按下不表。
1. shadow 模式
可以使用 Element.attachShadow() 方法将一个 shadow root 附加到任何一个元素上,它接受一个配置对象作为参数,该对象有一个 mode
属性,值可以是 open
或者 closed
:
let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});
open
表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,可以使用 DOM APIs 对它进行操作。
class SelfDiv extends HTMLElement {
constructor() {
super()
let shadow = this.attachShadow({mode: 'open'})
shadow.appendChild(tpl.content.cloneNode(true))
console.log('===shadow root===', this.shadowRoot)
}
}
2.可绑定的类型
还有一点要特别注意的事,不是任何类型的元素都可以附加到 shadow root 下面。出于安全考虑,目前只有这些类型元素可以附加到 shadow DOM 上(有效的自定义元素也可):可附加的元素
template 和 slot
前文提到 slot 的使用规则过于刻板不太灵活,其实 slot 分为具名和非具名两种,可书写更灵活的使用方式。这里有几个要点需特别说明:
- 如果你调用
template
时未显式使用具名 slot,具名 slot 会显示默认内容。 - 具名 slot 被使用时,非具名 slot 默认不展示
- 如果在 template 中定义了非具名 slot,使用模板时标签间的内容会直接替换非具名 slot 部分。
假设 template 有两个 slot 如下:
<template id="tpl">
<header>
<slot name='title'>具名slot:博客标题</slot>
</header>
<slot>非具名slot</slot>
<section>博客内容博客内容博客内容</section>
</template>
该模板有一个具名 title
的 slot 和一个非具名的 slot,若使用该模板进行组件自定义:
class MyBlog extends HTMLElement {
constructor(){
super()
let shadow = this.attachShadow({mode:'open'})
shadow.appendChild(tpl.content.cloneNode(true))
}
}
customElements.define('my-blog', MyBlog)
场景一:在页面上使用时,直接访问 <my-blog></my-blog>
,具名 slot 和非具名 slot 都直接展示:
<!-- 直接调用,未使用slot,具名和非具名都直接展示-->
<my-blog></my-blog>
场景二:如果访问具名 slot,且模板内不传入其他内容,只会展示具名 slot 部分:
<!-- 使用具名slot,非具名slot不展示 -->
<my-blog>
<h2 slot='title'>自定义标题</h2>
</my-blog>
场景三:同时为具名和非具名 slot 赋值:
<!-- 模板非slot 内容会替换非具名内容 -->
<my-blog>
<h2 slot='title'>自定义标题</h2>
<span>自定义非具名部分</span>
</my-blog>
具体代码可以猛戳自定义组件slot。
四、生态和周边
目前市面上有一些基于 WebComponent 标准开发的组件库:Omiu,xy-ui。另外还发现一个比较有意思的 CSS 库 css-doodle,用 WebComponent 实现酷炫的 CSS 效果,可以为个人网站增色不少。
以上,谢谢阅读。
上一篇: 非常实用,全国十大另类行业网站导航站
下一篇: 六种病态的 "你好世界"!(混乱代码)
推荐阅读
-
连体网络孪生神经网络及其在医疗场景中的应用
-
询问如何直观比较损失在连体网络 EN 中的作用
-
问 在连体架构中,梯度如何飞回网络?即使使用不同的模型,所有 CNN 模型的权重也是相同的。
-
streampetr 原始网络 nuscenes 数据 pkl 文件中字段的含义
-
了解子网掩码的功能及其在网络规划中的重要性
-
微信 "扫一扫 "物联网,全面揭秘 "扫一扫 "背后的扫盲技术!-1.1 扫一扫感知物体是做什么的? 1.1 微信扫一扫是做什么的? 扫一扫识物是指以图片或视频(商品图片:鞋/包/美妆/服饰/家电/玩具/图书/食品/珠宝/家具/其他商品)为输入媒介,挖掘微信内容生态中的有价值信息(电商+百科+资讯,如图1所示),并展示给用户。这里的电商基本涵盖了微信小程序覆盖上亿SKU的全量优质电商,可以支持用户货比N家并直接下单购买,百科和资讯则聚合了微信内的头部自媒体如搜狗、搜搜、百度等,向用户展示和分享拍摄商品相关的内容资讯。 图 1 扫一扫识别功能示意图 欢迎大家更新iOS新版微信→扫一扫→识货,亲自体验,也欢迎大家通过识货界面的反馈按钮向我们提交反馈意见。 扫一扫识物实景图展示 1.2 扫一扫识物有哪些使用场景? 扫一扫识物的目的是为用户访问微信内部生态内容开辟一个新窗口,以用户扫图片为输入形式,为用户提供微信生态内容中的百科、资讯、电商等作为展示页面。除了用户熟悉的扫一扫操作外,我们还将进一步拓展长按操作,让用户更方便地进行扫一扫操作。"扫一扫知事 "的落地场景主要涵盖三大部分: a. 科普知识: a.科普知识。用户通过扫一扫,可以在微信生态圈中获取该对象的百科、资讯等常识或趣闻,帮助用户更好地了解该对象; b.购物场景。同样的搜索功能支持用户看到喜欢的商品立即检索到微信小程序电商中的同款商品,支持用户即扫即购; c.广告场景。扫一扫识别物体可以辅助公众号文章、视频更好地理解其中蕴含的图片信息,从而更好地投放匹配广告,提高点击率。 1.3 Sweep Sense 为 Sweep 家族带来了哪些新技术? 对于扫一扫来说,大家耳熟能详的应该就是扫一扫二维码、扫一扫小程序码、扫一扫条形码、扫一扫翻译了。无论是各种形式的编码还是文字字符,都可以看作是图片的一种特定编码形式,而物的识别则是对自然场景图片的识别,这对于扫一扫家族来说是一个质的飞跃,我们希望从物的识别入手,进一步拓展扫一扫对自然场景图片的理解能力,比如扫酒、扫车、扫植物、扫人脸等服务,如下图3所示。 图 3 Sweep 家族
-
2022 年网络安全:关注质量》报告中对数据安全热点和趋势的解释
-
在深度神经网络程序中,前向函数实际上是用来表示程序逻辑的,尤其是网络的前向传播过程。它描述了输入数据如何通过网络的每一层,并最终得到输出预测结果的流程
-
aspectFill中的滑动图像自适应图像组件
-
一种结构设计模式,允许在对象中动态添加新行为。它通过创建一个封装器来实现这一目的,即把对象放入一个装饰器类中,然后把这个装饰器类放入另一个装饰器类中,以此类推,形成一个封装器链。这样,我们就可以在不改变原始对象的情况下动态添加新行为或修改原始行为。 在 Java 中,实现装饰器设计模式的步骤如下: 定义一个接口或抽象类作为被装饰对象的基类。 公共接口 Component { void operation; } } 在本例中,我们定义了一个名为 Component 的接口,该接口包含一个名为 operation 的抽象方法,该方法定义了被装饰对象的基本行为。 定义一个实现基类方法的具体装饰对象。 公共类 ConcreteComponent 实现 Component { public class ConcreteComponent implements Component { @Override public void operation { System.out.println("ConcreteComponent is doing something...") ; } } 定义一个抽象装饰器类,该类继承于基类,并将装饰对象作为一个属性。 公共抽象类装饰器实现组件 { protected Component 组件 public Decorator(Component component) { this.component = component; } } @Override public void operation { component.operation; } } } 在这个示例中,我们定义了一个名为 Decorator 的抽象类,它继承了 Component 接口,并将被装饰对象作为一个属性。在操作方法中,我们调用了被装饰对象上的同名方法。 定义一个具体的装饰器类,继承自抽象装饰器类并实现增强逻辑。 公共类 ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component 组件) { super(component); } } public void operation { super.operation System.out.println("ConcreteDecoratorA 正在添加新行为......") ; } } 在本例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,它继承自装饰器抽象类,并实现了操作方法的增强逻辑。在操作方法中,我们首先调用被装饰对象上的同名方法,然后添加新行为。 使用装饰器增强被装饰对象。 公共类 Main { public static void main(String args) { Component 组件 = new ConcreteComponent; component = new ConcreteDecoratorA(component); 组件操作 } } 在这个示例中,我们首先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传递。最后,调用装饰器的操作方法,实现对被装饰对象的增强。 使用场景 在 Java 中,装饰器模式被广泛使用,尤其是在 I/O 中。Java 中的 I/O 库使用装饰器模式实现了不同数据流之间的转换和增强。 让我们打开文件 a.txt,从中读取数据。InputStream 是一个抽象类,FileInputStream 是专门用于读取文件流的子类。BufferedInputStream 是一个支持缓存的数据读取类,可以提高数据读取的效率,具体代码如下: @Test public void testIO throws Exception { InputStream inputStream = new FileInputStream("C:/bbb/a.txt"); // 实现包装 inputStream = new BufferedInputStream(inputStream); byte bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); } } } } 其中 BufferedInputStream 对读取数据进行了增强。 这样看来,装饰器设计模式和代理模式似乎有点相似,接下来让我们讨论一下它们之间的区别。 第三,与代理模式的区别: 代理模式的目的是控制对对象的访问,它在对象外部提供一个代理对象来控制对原对象的访问。代理对象和原始对象通常实现相同的接口或继承相同的类,以确保两者可以相互替换。 装饰器模式的目的是动态增强对象的功能,而这是通过对象内部的包装器来实现的。在装饰器模式中,装饰器类和被装饰对象通常实现相同的接口或继承自相同的类,以确保两者可以相互替代。装饰器模式也被称为封装器模式。 在代理模式中,代理类附加了与原类无关的功能。