Web Components入坑扫盲篇

3,582 阅读8分钟

本文作为入门篇, 了解web components的一些基本概念,也包含一些简单的例子, 供大家参考。(另: 本文引用了MDN中Web Components相关内容, 需要更全面的了解可以点击魔法传送门

入门 - 本文

  1. Web components的出现是想解决什么问题.
  2. Web components的构成.
  3. Web components的hello world
  4. Web components生命周期
  5. Web components的template 和 slot
  6. Web components的css 样式隔离

进阶 - 下一篇

  1. Web componets 基础库 lit-element
  2. Web components 跟现代框架(例如: react)如何一起使用
  3. Web components如何让用户自定义自己的样式(从webcomponents外部设置)

Web components的出现是想解决什么问题.

引用官网概念和使用篇章的一段话:

作为开发者,我们都知道尽可能多的重用代码是一个好主意。这对于自定义标记结构来说通常不是那么容易 — 想想复杂的HTML(以及相关的样式和脚本),有时您不得不写代码来呈现自定义UI控件,并且如果您不小心的话,多次使用它们会使您的页面变得一团糟。

额, 说说我的理解。从业务上讲, 写前端就是写很多符合业务的组件然后拼在一起。我们现在的使用方式其实无论是使用vue还是react我们的初衷我认为很重要一点是想让开发者较为容易的创建自定义组件, 并且利用mvvm的特性来管理我们的业务逻辑, 尽可能的达成视图逻辑分离, 同时让我们的组件易于复用。vue和react算是社区给出的答卷, 而web components感觉就是官方给的答卷。就是提供了js的api让我们更容易的创建自定义组件, 易于复用,并且可以和现有的framework共存, 并且不用担心跟原有的代码冲突

不跟原有的代码冲突这个就很强了, 我粗浅的意识到, 这个好像可以解决css冲突的问题,世纪难题啊。不过,貌似styled-components现在我们也用得挺好(狗头)。但是对于一些比较老的项目, 比如使用了boostrap这种库, 有全局的样式入侵, 此时你要使用类似antd这种组件库, 那么在使用过程中很可能会出现一些不可预知的问题, 总会影响到你的使用预期。

所以看一下优点:

  1. 各个浏览器平台对web components的支持推进速度很快

  2. 提供了基于设计token友好的样式自定义样式的方法

  3. css的shadow dom隔离带来的便利

我觉得web components是一道想解决前端开发痛点的综合题的答案。至于好与不好, 希望我的文章可以给你一定的帮助。

背景大概是这些,让我们进入主题。

web components的构成

Web components最核心的概念就3个:

  1. Custom elements: 这个是最核心的, 我们可以通过js的api去定义元素行为, 然后在我们使用的时候去使用他。(我的理解是, 这个相当于提供了一个基类,这个基类提供了一些基础的能力, 例如事件注册这样的基础能力。)
  2. Shadow dom: 这个其次, 我们可以跟页面其他部分的dom隔离开来, 保持元素功能的私有。(这个很实用, css私有化真心推荐, 但是这样是否也存在一些问题, 例如象字体这种, 就是需要全局设置的属性, 我们应该不需要重新在组件层去重新定义吧?)
  3. Html templates: 提供了template和slot标签, 让你可以灵活的编写并且重用你的组件。

这三就是理解web components的基本概念。 Custom element 提供了js api让我们可以有定义组件复杂交互的基本行为, shadow dom让我们真正的有一个自定义组件, 与原有的dom结构隔离开, 让我们的组件可以拥有期望的样式显示, html templates提供了灵活的模版, 以及插槽的能力,让我们可以编写更强大, 复用性更强的组件。

web components的hello world

虽然很土,但是这个必须要有

web components

class HelloWorld extends HTMLElement {
  constructor() {
    // 必须首先调用 super 方法, 继承基类
    super();

    // 初始化web component
    this.init();
  }
  init() {
    var shadow = this.attachShadow({mode: 'open'});
    var wrapper = document.createElement('span');
		wrapper.innerHTML = "Hello World.";
    shadow.appendChild(wrapper);
  }
}

customElements.define('hello-world', HelloWorld);

// 使用
<hello-world />
  

// 下图是render的html结构

稍微解释一下代码, 我们基于HTMLElement创建了一个自定义类HelloWorld. 调用super方法获得基类能力, 然后在这里我执行了一个初始化方法init(). 在init中我们做了这些事:

  1. 基于基类上的attachShadow创建shadow-root元素节点
  2. 同时创建了一个span元素, 内容是hello world
  3. 把span元素append 到shadow元素中
  4. 使用的时候, 先使用customElements.define注册对应的自定义事件。然后就可以直接使用了
    1. 创建custom element分为两种
      1. 内建: 本身就是你可以基于现有的html元素去扩展, 在使用的时候, is="custom name"即可
      2. 自定义
    2. 关于customElements.define我们要注意自定义元素的命名规则
      1. 表示所创建的元素名称的符合 DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线
      2. 用于定义元素行为的
      3. 可选参数,一个包含 extends 属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。

这个例子很简单, 只是告诉了我们如何去使用web component. 同时我们也接触到了custom element 以及shadow dom。 这是我们的第一步, hello world。

Web components生命周期

没想到吧, shadow dom 也存在生命周期。生命周期包含以下四个:

  • connectedCallback: 当 custom element首次被插入文档DOM时,被调用。
  • disconnectedCallback: 当 custom element从文档DOM中删除时,被调用。
  • adoptedCallback: 当 custom element被移动到新的文档时,被调用。
  • attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。
class HelloWorld extends HTMLElement {
  constructor() {
    // 必须首先调用 super 方法, 继承基类
    super();

    // 初始化web component
    this.init();
  }
  connectedCallback() {
		console.log("connectedCallback.");
	}
  disconnectedCallback() {
		console.log("disconnectedCallback.");
	}
  adoptedCallback(){
    console.log("adoptedCallback.");
  }
  attributeChangedCallback(name, oldValue, newValue) {
    console.log("attributeChangedCallback. change " + name + "value is" + newValue + "old Value is" + oldValue);
  }
  init() {
    var shadow = this.attachShadow({mode: 'open'});
    var wrapper = document.createElement('span');
		wrapper.innerHTML = "Hello World.";
    shadow.appendChild(wrapper);
  }
}

customElements.define('hello-world', HelloWorld);


document.body.append(document.createElement("hello-world"));

这个周期没啥好说的, 就是进入的时候, connectedCallback退出的时候, disconnectedCallback, 比较特殊的是可以监听属性变化。

Web components的template 和 slot

template

template和slot我们在使用vue的时候都已经接触过了。但是这个模版跟vue里的模版还是差别很大的。先看一下使用例子:

<template id="template">
  <style>
     p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
	<p>
    Halo template
  </p>
</template>

如果我们在页面中插入这段html, 在页面上不会显示对应的p标签。直到你根据id拿到这个dom并且添加到dom树中, 此时模版开始生效:

let template = document.querySelector("#template");
let templateContent = template.content;
document.body.appendChild(templateContent);

结合web components 使用template:

customElements.define('custom-p',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.querySelector('template');
      let templateContent = template.content;

      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(templateContent.cloneNode(true));
  }
})
slot

提供了更高的灵活度。基于上面的例子:

<template id="template">
  <style>
     p {
      color: white;
      background-color: #666;
      padding: 5px;
    }
  </style>
	<p>
    Halo template <slot name="name"></slot>
  </p>
</template>

然后在使用的时候我们就可以这样:

<custom-p>
  <span slot="name">hawei</span>
</custom-p>

Web components的css 样式隔离

终于到这个最重要的属性了。刚才在前面讨论为什么出现web components的时候, 我们已经讨论过了, shadow dom是跟我们页面中的dom是分开渲染的, 所以能达到样式隔离的过程。这一点大家可以复习一下浏览器render 的过程。此处不在赘述。接下来看一个例子:

class HelloWorld extends HTMLElement {
  constructor() {
    // 必须首先调用 super 方法, 继承基类
    super();
    this.shadow = this.attachShadow({mode: 'open'});

    // 初始化web component
    this.init();
  }
  init() {
    this.addStyle();
    this.addNode();
  }
  
  addNode() {
    const wrapper = document.createElement('span');
    wrapper.className = "hawei1"
		wrapper.innerHTML = "Hello World1.";
    const wrapper1 = document.createElement('span');
    wrapper1.className = "hawei2"
		wrapper1.innerHTML = "Hello World2.";
    this.shadow.appendChild(wrapper);
    this.shadow.appendChild(wrapper1);
  }
  
  addStyle() {
    const styleDom = document.createElement("style");
    styleDom.textContent = `
			.hawei1 {
				color: orange;
			}
		`;
    
    this.shadow.appendChild(styleDom);
  }
}

customElements.define('hello-world', HelloWorld);

function renderDom() {
  document.body.innerHTML=`
		<style>
		.hawei1 {color: red;}
		.hawei2 {color: green;}
		</style>
  	<div class="hawei1">hawei</div>
		<div class="hawei2">hawei2</div>
  `;
  
}

renderDom();

document.body.append(document.createElement("hello-world"));

可以看到, 在renderDom中我们创建了两个div 并且给了class为hawei1, 和hawei2. 而在shadow dom 中, 我也创建了两个span 元素, 也给了class为hawei1和hawei2. 并且同时给了不同的颜色样式。 render的结果如下:

可以看到, 在renderDom中的样式跟shadow dom中的样式互不影响。我觉得这算是web components的一大卖点了。虽然styled-components也可以解决这个问题, 但是毕竟随机字符串生成也是需要性能的。而这种官方的解决方案我觉得从架构的角度解决了这个问题。

嘤嘤嘤, 学不动了学不动了。

引用