likes
comments
collection
share

试试 Web Component: 比你想象的容易

作者站长头像
站长
· 阅读数 12

组件化

组件化是前端的发展方向,现在流行的 React 和 Vue 都是组件框架。近些年来前端的更新速度太快了,每当出一个新框架,我们都大喊「学不动了」,然而变化的是框架,是我们,不变的是思想, 当我们站在一个局外人的角度去审视这些框架,每个框架都有自己的策略,但殊途同归,他们都是组件化的产物,组件化的核心思想是「高内聚,低耦合」

高内聚、低耦合,是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计。 它的考量标准是类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。

就目前我们使用的框架而言,确实给我们带来了极大的便利

组件化的好处:

  • 代码复用率提高,带来的是工作效率的提升
  • 代码可维护性、可读性提升
  • 团队效率提升

但有些问题也是不可避免的:

  • 不同技术栈的组件无法复用,比如vue的组件无法在react中使用,react的组件无法在vue中使用,而我们却要为了两套技术栈维护同样的功能
  • 对于同一个技术栈,我们的组件库也不能随便升级,因为升级可能会造成原来的功能不能使用。
  • 在我们使用组件库的时候,有时候会遇到样式冲突的问题
  • 我们提到的组件化更多的是源码层面针对不同功能的组件化,源码经过编译后并不在是组件,而是一个个的js文件

Web Components的出现,就是为了解决这些问题

Web Components

Web Components是Web原生提供的封装组件的方式,让开发者定义一些可重复使用的自定义元素。 主要包含custom elements、shadow dom、html templates部分。 下面我们通过一个例子来看看Web Components的使用

当我们要在页面上渲染如下内容, 图片

试试 Web Component: 比你想象的容易 我们在页面上,只需要这样写就可以了

<fruit-info
    name="apple"
    color="red"
></fruit-info>

customElements.define()

自定义元素使用前,必须先定义,所有的自定义元素都必须继承自 HTMLElement,定义的方式如下:

class FruitInfo extends HTMLElement {
  constructor() {
    super();
  }
}

然后使用 customElements.define() 方法注册自定义元素,使其与自定义类关联起来

customElements.define('fruit-info', FruitInfo);

自定义元素

接下来我们往自定义元素中添加一些内容,一个是name,一个是color

<template id="fruitInfoTemplate">
  <div class="container">
    <div>水果名字:<span class="name"></span></div>
    <div>水果颜色:<span class="color"></span></div>
  </div>
</template>

namecolor是我们从组件外传递进来的入参,相当于react中的props,我们可以通过this.getAttribute()方法获取到这些入参,然后将这些入参渲染到组件中

class FruitInfo extends HTMLElement {
       constructor() {
          super();
          let shadow = this.attachShadow( { mode: 'closed' } );

          let templateElem = document.getElementById('fruitInfoTemplate');
          let content = templateElem.content.cloneNode(true);
          content.querySelector('.name').innerText = this.getAttribute('name');
          content.querySelector('.color').innerText = this.getAttribute('color');

          shadow.appendChild(content);
       }
    }
customElements.define('fruit-info', FruitInfo);

首先我们定义了shadow,shadow是Web Components中的一个重要概念,它是一个DOM树,但是它和普通的DOM树有所不同,它是一个封闭的DOM树,外部无法访问到它的内容,我们可以通过attachShadow()方法来创建一个shadow,attachShadow()方法接收一个参数,modemode有两个值,openclosedopen表示外部可以访问到shadow中的内容,closed表示外部无法访问到shadow中的内容,我们这里使用的是closed,因为我们不希望外部访问到shadow中的内容, 然后我们通过document.getElementById()方法获取到模板,然后通过content.cloneNode()方法克隆模板,然后将克隆的模板添加到shadow中,这样我们就完成了自定义元素的定义,

组件样式

按照组件化思想,样式应该和组件在一起,应该影响外部的样式,我们把样式放在<template>

<template id="fruitInfoTemplate">
  <style>
      :host {
          font-size: 20px;
      }
      .container {
          width: 300px;
          height: 200px;
          background-color: wheat;
      }

      .name {
          margin-bottom: 10px;
      }
      .color {
          background-color: #ccc;
      }
  </style>

  <div class="container">
    <div>水果名字:<span class="name"></span></div>
    <div>水果颜色:<span class="color"></span></div>
  </div>
</template>

目前为止,我们已经完成了一个简单的组件,但组件是为了复用,我们需要将组件封装起来,然后通过import的方式引入到页面中,这样我们就可以在页面中使用组件了

组件封装

我们的html还是只保留使用组件的代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<fruit-info
    name="apple"
    color="blue"
></fruit-info>

<script type="module">
import FruitInfo from "./FruidInfo.js";

customElements.define('fruit-info', FruitInfo);
</script>
</body>
</html>

我们把template和组件定义都抽离出去,放在一个js文件中

const template = document.createElement('template');
template.innerHTML = `
<style>
      :host {
          font-size: 20px;
      }
      .container {
          width: 300px;
          height: 200px;
          background-color: wheat;
      }

      .name {
          margin-bottom: 10px;
      }
      .color {
          background-color: #ccc;
      }
  </style>

  <div class="container">
    <div>水果名字:<span class="name"></span></div>
    <div>水果颜色:<span class="color"></span></div>
  </div>
`

export default class FruitInfo extends HTMLElement {
    constructor() {
       super();
       let shadow = this.attachShadow( { mode: 'closed' } );

       let content = template.content.cloneNode(true);
       content.querySelector('.name').innerText = this.getAttribute('name');
       content.querySelector('.color').innerText = this.getAttribute('color');

       shadow.appendChild(content);
    }
}

上面有一个小的改变,我们template是动态创建出来的,组件的定义没有太大的变化,最后在使用的时候,我们通过import引入,并关联组件

通过这个例子,我们可以发现Web Components的使用非常简单,他的实现不是一个单一的技术,而是由DOM APIHTML 规范所组成。它其中就包含了:

  • custom elements
  • shadow dom
  • html templates
  • ES Modules

Web Components优势

Web Components优势都是建立在它是Web原生提供的基础上的,框架会变,但是Web原生不会变

  • 浏览器原生支持,无需引入第三方库
  • 可复用性,组件可以在不同的框架中使用,因为是原生的语法,所以我们可以在任何其他框架中使用,而不用考虑框架的束缚
  • 组件样式封装,组件样式不会影响外部样式,样式隔离带来的安全空间让我们不用再考虑样式污染问题

Web Components 的限制

  • CSS 的隔离,导致和外部 CSS 交互比较难
  • document 、 window、 document.body、 document.head是不可配置的,不能被覆盖。
  • 服务器端渲染(SSR)仍然存在问题,在服务器上渲染 Web Component 没有标准,因此每个框架的做法都略有不同
  • shadow dom的内容是不可访问的,这意味着我们在这里可能会遇到问题,比如全局弹窗,Web Componewnt中的内容获取,与window,document等交互可能并不如我们往常期望的效果一样

Web Components 的应用

目前我了解到的使用Web Components的有部分微前端框架,例如:无界、micro-app,还有一些第三方库:Lit、Omi,大家有兴趣的可以自行了解下

最后

Web Components 有着其天然的优势,能让我们的开发发生巨大变化,但目前来看,还需做出更多的努力让 Web Components 变得更加优秀,更加便利,才能让它真正的走进我们的生产环境中