原生Web Components实现类Element UI中的Card卡片组件

Web Components 是一个浏览器原生支持的组件化方案,允许你创建新的自定义、可封装、可重用的HTML 标记。不用加载任何外部模块,直接就可以在浏览器中跑。本文就简单介绍一下:使用 Web Components 实现一个类 Element UI 中的 Card 卡片组件。

Web Components && Card

一、前言

随着前端工程化生态日益成熟,出现了很多优秀的框架,如:VueReactAngular等等,极大的提高了日常开发效率。
其中组件化开发发挥了至关重要的作用,但是这些组件化开发都需要依赖第三方框架,编译打包之后才能在浏览器正常使用。
而原生组件 Web Components ,相比与第三方框架使用起来更简单直接,符合直觉,不用加载任何外部模块,代码量小。

二、Web Components 核心组成

  1. 自定义元素(custom element),使用 window.customElements.define API注册
  2. Shadow DOM隔离,影藏标记结构、样式和行为
  3. 可以在<template>中定义标记结构、样式,多次重用。利用 slot 插槽、命名插槽,可以传入定制化的结构UI,使用上类似 Vue 中的 slot 插槽

1. Custom Elements

自定义的 HTML 标签,称为自定义元素(custom element)。根据规范,自定义元素的名称必须包含连词线-,用与区别原生的 HTML 元素。所以,<com-card>不能写成<comcard>

<div id="custom-card" class="com-card">
  <div class="com-card-head">
    <slot name="head"></slot>
  </div>
  <div class="com-card-body">
    <slot></slot>
    <div class="link-wrap">
      <a class="link" href="" title=""></a>
    </div>
  </div>
</div>

<script>
  class ComCard extends HTMLElement {
    constructor() {
      super()
      var tplEle = document.getElementById('custom-card')
      this.append(tplEle)
    }
  }
  window.customElements.define('com-card', ComCard)
</script>

这样就注册了浏览器可识别渲染的一个自定义元素标签。

2. Shadow DOM

Shadow DOM 是对DOM的一个封装。可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。
使用自定义元素的 this.attachShadow() 方法可以开启 Shadow DOM

class ComCard extends HTMLElement {
  constructor() {
    super()
    var shadow = this.attachShadow({mode: 'closed'})  // open
    var tplEle = document.getElementById('custom-card')
    shadow.appendChild(tplEle)
  }
}
window.customElements.define('com-card', ComCard); 

其中参数{ mode: 'closed' },表示 Shadow DOM 是封闭的,不允许外部访问。

3. templates 和 slots

因为组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式。所以,可以把样式写在<template>里面,这样作为自定义元素结构的基础可以被多次重用。

<template id="custom-card-template">
  <style>
    .com-card {
      
    }
  </style>
  <div class="com-card">
    
  </div>
</template>

<script>

  class ComCard extends HTMLElement {
    constructor() {
      super();
      var shadow = this.attachShadow({mode: 'closed'})  // open
      var tplEle = document.getElementById('custom-card-template')
      var content = tplEle.content.cloneNode(true)

      shadow.appendChild(content)
    }
  }

  window.customElements.define('com-card', ComCard);
</script>

三、完整代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Web Component</title>
  <style>
    * {
        box-sizing: border-box;
    }
    body {
        font-size: 14px;
    }
    .box {
        padding: 5px 0 30px;
    }
    .box .caption {
        display: none;
    }
    .box h1 {
        text-align: center;
    }
    .box li {
        color: #666;
        font-size: 14px;
        line-height: 1.8;
        margin-top: 15px;
    }
    .img {
        display: block;
        width: 80%;
        margin: 0 !important;
    }
    .card-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
    }
    .card-title {
        color: #333;
        font-size: 16px;
    }
    .card-head-btn {
        color: #409eff;
        cursor: pointer;
        text-decoration: none !important;
    }
    .card-head-btn:hover {
        text-decoration: none;
    }
  </style>
</head>
<body>

<div class="box">

  <h1>Web Component</h1>

  <com-card data-show-head="0" data-url="https://tiven.cn" data-title="天问博客">
    <div slot="head" class="card-head">
      <div class="card-title">卡片名称</div>
      <a class="card-head-btn">操作按钮</a>
    </div>
    <img class="img" src="https://tiven.cn/static/img/kpl-sunwukong-a3Lt-ed2NG9r4NFDm_9DA.jpg" alt="天問">
  </com-card>

  <br>
  <br>

  <com-card data-show-head="1" data-url="https://tiven.cn/p/de241e23/" data-title="Vite+Vue3+Vant快速构建项目">
    <div slot="head" class="card-head">
      <div class="card-title">卡片名称</div>
      <a class="card-head-btn" onclick="hello()">操作按钮</a>
    </div>
    <img class="img" src="https://tiven.cn/static/img/kpl-xuance-JqX71qH7aTflHV_gqvhIc.jpg" alt="天問">
    <ol>
      <li>君不见黄河之水天上来,奔流到海不复回。</li>
      <li>君不见高堂明镜悲白发,朝如青丝暮成雪。</li>
      <li>天生我材必有用,千金散尽还复来。</li>
    </ol>
  </com-card>

</div>

<template id="custom-card-template">
  <style>
    .com-card {
        min-width: 200px;
        min-height: 100px;
        border-radius: 4px;
        border: 1px solid #ebeef5;
        background-color: #fff;
        overflow: hidden;
        color: #303133;
        transition: .3s;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    }
    .com-card-head {
        padding: 10px 20px;
        border-bottom: 1px solid #ebeef5;
        box-sizing: border-box;
    }
    .com-card-body {
        padding: 20px;
    }
    .link-wrap {
        text-align: left;
        padding-top: 20px;
    }
    .link {
        display: inline-block;
        height: 42px;
        line-height: 43px;
        padding: 0 30px;
        text-align: center;
        cursor: pointer;
        color: #fff;
        background-color: #409eff;
        border-color: #409eff;
        -webkit-appearance: none;
        box-sizing: border-box;
        outline: none;
        transition: .1s;
        font-weight: 500;
        -moz-user-select: none;
        -webkit-user-select: none;
        -ms-user-select: none;
        font-size: 14px;
        border-radius: 4px;
        text-decoration: none !important;
    }
  </style>
  <div class="com-card">
    <div class="com-card-head">
      <slot name="head"></slot>
    </div>
    <div class="com-card-body">
      <slot></slot>
      <div class="link-wrap">
        <a class="link" href="" title=""></a>
      </div>
    </div>
  </div>
</template>

<script>

  class ComCard extends HTMLElement {
    constructor() {
      super();
      var shadow = this.attachShadow({mode: 'closed'})  // open
      var tplEle = document.getElementById('custom-card-template')
      var content = tplEle.content.cloneNode(true)

      var attrList = Array.from(this.attributes);
      var props = attrList.reduce((prev, item)=>{
        prev[item.name] = item.value
        return prev
      }, {})

      if (props['data-show-head']!=='1') {
        var head = content.querySelector('.com-card-head')
        head.remove()
      }

      var urlEle = content.querySelector('.link')
      if (props['data-url'] && props['data-title']) {
        urlEle.href = props['data-url']
        urlEle.title = props['data-title']
        urlEle.innerText = props['data-title']
      } else {
        urlEle.remove()
      }

      shadow.appendChild(content)
    }
    connectedCallback(){
      //在这里发送数据请求(Ajax)
      console.log('connectedCallback')
    }
    //被从文档DOM中删除时调用
    disconnectedCallback(){
      console.log('disconnectedCallback')
    }
    //被移动到新的文档时调用
    adoptedCallback(){
      console.log('adoptedCallback')
    }
    //当增加、删除、修改自身的属性时被调用
    attributeChangedCallback(){
      console.log('attributeChangedCallback')
    }
    
  }

  window.customElements.define('com-card', ComCard);

  function hello() {
    alert('Hello,Web Component')
  }
</script>
</body>
</html>

最终效果如上图所示,具体demo演示地址:https://tiven.cn/demo/web-component.html

四、Web Components vs Vue Components

Vue Component Web Component
data 实例属性
props attributes
watch observedAttributes、attributeChangedCallback
computed getters
methods class methods
mounted connectedCallback
destroyed disconnectedCallback
style scoped template中的style
template template

五、Web Components 生命周期回调函数

  • connectedCallback:当 custom element首次被插入文档DOM时,被调用。
  • disconnectedCallback:当 custom element从文档DOM中删除时,被调用。
  • adoptedCallback:当 custom element被移动到新的文档时,被调用。
  • attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。

六、优点 and 缺点

优点:

  1. 浏览器原生支持,不需要引入额外的第三方库
  2. 语义化
  3. 复用性,移植性高
  4. 不同团队不同项目可以共用组件

缺点:

  1. 需要操作DOM
  2. 目前浏览器兼容性、性能方面不够友好
  3. 和外部css交互比较难

七、基于web components的框架

  • LitElement 是一个快速、轻量级的 Web UI 框架。使用 lit-html 来渲染元素。
  • Polymer 是一款实用、基于事件驱动、封装性和交互性强的 Web UI 框架。
  • Omi 是基于 Web 组件的跨框架跨平台框架 。移动端 & 桌面 & 小程序。

欢迎访问:天问博客

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

推荐阅读更多精彩内容

  • 现在的前端开发基本离不开 React、Vue 这两个框架的支撑,而这两个框架下面又衍生出了许多定义组件库: 这些组...
    wan_c1121阅读 1,061评论 1 2
  • 前言:这周完成了两场技术分享会,下周还有一场,就完成了这阶段的一个重大任务。分享会是关于 TS 的,我这两场分享会...
    CondorHero阅读 965评论 0 2
  • 组件的概念 组件,是数据和方法的一个封装,其定义了一个可重用的软件元素的功能,展示和使用,通常表现为一个或一组可重...
    zx_lau阅读 2,418评论 0 3
  • 前言 不知不觉,2019年即将接近尾声,现有前端三大框架也各自建立着自己的生态、自己的使用群体。从angular1...
    Kaku_fe阅读 2,746评论 0 19
  • 定义 浏览器原生支持的一套组件封装技术。这里有两个关键字: 组件技术 浏览器原生 组件化一直是前端完善的方向,从早...
    cd2001cjm阅读 685评论 0 0