从0实现一个简易Button,理解WebComponent规范

此文已同步到因卓诶公众号以及因卓诶博客:

从0实现一个简易Button,理解WebComponent规范


写在前面

博主要在西安找一个好工作,前端/全栈岗位,中级前端岗位,希望目标公司双休,做自研产品的最好,技术氛围好的优先,水友们有岗位或者HR朋友看到我这个文章请与我联系,po一下简历吧:
沈昊.pdf
联系方式微信:npm_install_s

正文

关于WebComponents的文章其实过年就想写的,但是自身的理解都太片面,所以最近才去边学边写。webCompoents下文中简称WC。从毕业开始如果有面试任务我都会尝试地问一下候选人是否了解过WC,很遗憾面试了半百的人,我连了解过ShadowCompoents的人都没遇到过,可能是面试地薪资要求太低,亦或者是有2-3年开发经验的工程师没有留意过类似的规范,今天我们就来好好梳理一下WC是什么,为什么WC影响了我们现在开发前端的方式吧!

是什么

WC是一套技术,允许开发者创建一套可以定制的元素(组件),相关逻辑和样式都会封装在元素中,并且你可以直接使用它。WC的出现解决了以往前端领域中,对多个具有相似性的功能只能复制粘贴从而造成代码臃肿的问题;WC的出现也推动了模块化/组件化的发展,让更多开发者享受封装组件带来的便利。


WC有3个要素:

  1. Custom Element 自定义元素
  2. ShadowDom 影子盒模型
  3. HTML模板

在HTML中有大多数的标签已经是运用到WC的技术了,比如熟悉的input,video,audio,select等等,我们从现在开始从0实现一个Button组件。要掌握WC的运用,需从实践开始。

影子DOM

我们在写HTML的时候,使用一些标签就可以表达一个具有形式和结构的页面,我们人类去编写这样的代码会很容易,但是机器却不会了,机器需要将HTML转换为真正的文档,而页面结构将会被解析成数据模型(对象/节点),浏览器通过创建这样的节点树(DOM)来确定用户写的HTML的层次结构,DOM最主要的特性是实时的,我们可以通过程序去操控它:

const title = document.createElement("div");title.textContent = "hello"document.body.appendChild(title);

这就是为什么我们可以直接通过js来操作页面上的效果就是因为DOM的存在;那么影子DOM的意思其实已经在字面上了,“隐藏在影子中你看不到”的DOM,举一个简单的例子,我们写一个video标签:

<video>   <source src="movie.mp4" type="video/mp4"></video>

控制台显示如下内容:

在video这样的WC下,有许多DOM都隐藏在其中,这些隐藏DOM都是内置好的功能和样式,在shadowDom中所有的样式和逻辑都是与外部隔绝,不会出现css冲突的问题。

创建一个简单的shadowDom:

const title = document.getElementById("title");const shadowRoot = title.attachShadow({mode: 'open'});shadowRoot.innerHTML = "<div>this is shadowDOM</div>"

此shadowRoot存在于title的节点树之下被称之为阴影树,而title就是阴影根,shadowRoot独立于title,shadowRoot中的内容样式逻辑都皆在组件本地,所以这就是shadowRoot不会造成css冲突的原因。

但是需要注意,并不是所有元素都可以承载shadowDOM,以下几种情况shadowDOM将无效/报错:

  1. 已经承载了shadowDOM的元素比如input,textarea等
  2. 元素承载了shadowDOM是img标签

除了我们可以定义”open“的shadowDOM之外,我们还可以定义闭合的shadowDOM:

const shadowRoot = title.attachShadow({mode: 'closed'});

HTML内部的Video标签就是一个闭合的shadowDOM,它的意义主要在于外部的JS是无法访问这个shadowDOM的,无论你使用assignedSlot/composedPath等等都是无效的。请记住闭合的shadowDOM不是很有用处,大可不必使用它,它不是我们理解的“安全的ShadowDOM”。

创建Custom Element

我们创建一个自定义的元素,结合使用shadowDOM,来完成我们开头说的button组件。

customElements.define(  "i-button",  class extends HTMLElement {    constructor() {      super();      const shadowRoot = this.attachShadow({ mode: "open" });      shadowRoot.innerHTML = `        <div class="button">            <slot name="icon"></slot>            <slot></slot>        </div>      `;    }  });

我们通过customElements对象创建一个自定义组件,组件接收一个类,此时i-button的影子DOM就是我们在构造函数中定义的,影子DOM内容是<slot>标签,关于插槽稍后再讲述,我们先尝试使用i-button。

<i-button>提交</i-button>

页面正常渲染我们的button组件,如果不出意外的话,你能在控制台看到我们刚刚编写的自定义元素以及元素下的shadowDOM。

组合和插槽

组合在shadowDOM中是一个很重要的概念,我们在HTML中使用各式各样的标签完成页面,页面的构成是各种标签的组合,而组件也是一样,例如video标签,我们通过video的子级source来定义媒介资源地址,但是它却不会渲染。

别着急,我们先来梳理下几个术语概念:

Light DOM

指的是用户编写的内容,比如在上文中,我们使用了i-button组件,在这个组件中我们写下了字 “提交”那么此时“提交”就是Light DOM,此时“提交”是实际子元素,它是真实存在的,而不像是shadowDOM。

Shadow DOM

具体的意义上文提到过,补充一下ShadowDOM对组件而言是本地的,它还可以定义一些“标记”或者说是“插槽

使用过vue的水友们,应该更能体会插槽带来的便利,插槽是组件内部的占位符,方便使用者编写的LightDOM按照指定的方式和组件一起呈现出来,那么这个指定的方式也就是插槽的类型了:

具名插槽

在组件内部定义的<slot name="icon"></slot>之后,我们使用组件的时候可以这么使用:

<i-button>    <img slot="icon" src="icon.png"></img>    提交</i-button>

默认插槽

默认插槽就是我们组件中写的内容,比如就是上文中的提交二字,它没有被slot标记,就会默认放在组件中<slot></slot>的位置渲染。如果用户不在组件提供LigntDOM,那么我们可以定义一个后备插槽以便备用:

/* 默认渲染的位置 */<slot></slot><slot>如果用户没传递内容,那么将会显示我</slot>

当用户编写的LightDOM被组件定义的插槽使用了,那么此时,这个元素并不是被插槽移动了位置,插槽没有移动位置的功能,其实就是浏览器把LightDOM元素渲染到了shadowDOM的位置上了而已。

理解了插槽之后,我们就可以使用更多的标签将其组合在一起,构成一个较为完整且实用的组件了,当然还有样式!

样式

ShadowDOM最有趣的特型就是作用域CSS了,外部的css选择器不会影响到shadowDOM,内部的也不会影响外部的,css的作用域为阴影根。我们来定义一下Button组件的样式:

#shadow-root<style> .button{   background: red; }</style><div class="button">  <slot name="icon"></slot>  <slot></slot></div>

oh, no,尽管它定义成了红色很丑,但是不妨碍我们研究它的作用域CSS;

<link rel="stylesheet" href="styles.css"><div></div>

还可以加入link标签引入一个css,这个css也是带有作用域的。我们在写WC的时候,不仅会需要组件自身维护自己的样式,也需要外部组件可以通过一种方法改变组件内部样式,这样既保证了封装性又有灵活性,那么:host这个伪类是需要了解的。

:host是一个选择宿主的伪类选择器,我们可以使用:host来匹配宿主或者宿主下的元素:

<style>    :host{        color: #fff;    }</style>

也可以匹配阴影根下的元素:

:host(.button){    background: blue;}

也可以定义插槽样式:

::slotted(.icon){   width: 2px;   height: 2px;}

从外部定义自定义组件的样式,直接使用元素名进行设置:

i-button:hover{    opacity: 0.8;}

当外部设置了样式之后,优先级会大于内部的css规则,比如:

:host{   opacity: 0.1;}

通常开发者编写自己的组件的时候,会使用CSS自定义属性,而使用者可以修改其CSS自定义属性:

<style>    i-button{        /*我很喜欢红色*/        --diy-bg: red;     }</style><i-button background></i-button>

影子dom这样写:

:host([background]){    background: var(--diy-bg, black);}

我们在使用组件的时候给自定义元素设置了一个值为red,然后在自定义元素中加了一个“background”的属性,然后在其shadowDOM中匹配了元素如果有属性background的话:就设置背景为外部传入的“red”,如果外部没有传入,则就是默认的“black“。

当然开发组件的时候,我们需要告知使用者一些内置的css自定义属性。

技巧

使用css containment

我们可以使用“css遏制”来优化web组件重排重绘的性能,当web组件内部进行了UI/位置变更,势必会引起页面的重排和重绘,使用css遏制之后告诉浏览器,组件这一块是一块独立的DOM,浏览器就不会造成整个页面的重排重绘了,只会在组件内部进行重排和重绘。

:host {  display: block;  contain: content; /* Boom. CSS containment FTW. */}

使用Template

使用Template代替innerHTML,使用template之后你会发现,vue的组件就是使用这种方式来呈现组件的,它们都是一样的!使用template会更清晰地看到组件的DOM结构。

<template id="i-button">    <div class="button">      <slot name="icon"></slot>      <slot></slot>    </div></template><script>    customElements.define(      "i-button",      class extends HTMLElement {        constructor() {          super();          var template = document            .getElementById('i-button')            .content;          const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(template.cloneNode(true));          `;        }      }    );  </script>    // 使用Button组件    <i-button></i-button>    <style>         // 增加一些Style样式    </style>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容