web components 从0到1

简单例子

// html
<c-button>
  click me
</c-button>


// js
// 定义元素类
class CButton extends HTMLElement{
  constructor(){
    super()
    this._root = this.attachShadow({ mode: 'closed' })
    this.tmp = document.createElement('template')
    this.tmp.innerHTML = `
       <style> 
         .c-button{ border: 0; background: #fff; color: orange }
       </style>
       <button class='c-button'>
         <slot />
       </button>
    `
    this._root.appendChild(this.tmmp.content.cloneNode(true))
  }
}
// 注册元素
window.customElements.define('c-button', CButton)

定义

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。

web components 就是一套提供自定义元素的的技术

组成

  • Custom elements(自定义元素) 提供自定义元素内容及行为的能力
  • Shadow DOM(影子DOM)提供元素封装性
  • HTML templates(HTML模板)提供使用html标签编写自定元素的内容结构

定义元素类

自定义元素通过API window.customElements.define(name, constructor, options) 注册到当前文档中,

该接口主要接收参数:

  • name 元素标签名, 例如: 'c-title' , 使用时: <c-title> </c-title>
  • constructor 元素构造函数,需要继承 HTMLElement 类
  • options 配置属性

所以我们需要通过类的方式定义自定义元素

class CustomElement extends HTMLElement {
   constructor(){
      ....
   }
}
// 注册
window.customElements.define('', CustomElement)

html 模板

HTML templates 提供了两个新的元素标签

  • <template> 模板容器
  • <slot> 内容插槽

自定元素更接近于我们平常定义的UI组件,将具有一定功能的 html, css, js 封装在元素标签内。所以编写的流程也大致相同。

首先我们先定义内容的基础结构

const root = this.attachShadow({ mode: 'closed' })
const tmp = document.querySelector('template')
tmp.innerHTML = `
  // 内容样式 
 <style>
   .c-card{
      width: 400px;
      height: 300px;
      padding: 8px 10px;
      background: #fff;
      border: 1px solid #eee;
   }
  </style>


 // 内容结构
  <div class='c-card'>
     <slot />
  </div>
`
// 挂载内容
root.appendChild(tmp.content)

除了使用模板字符串,还可以使用 html 模板或 document.createElement 构建内容结构

  • html 模板
// html
<template id='tmp'>
  <div class='c-card'>
    <slot />
  </div>
</template>


// js
class Card extends HTMLElement{
  construction(){
    ...
    const tmp = document.querySelector('#tmp')
    root.appendChild(tmp.content)
  }
}
// <template> 内容不会直接显示在html文档中
  • createElement
class Card extends HTMLElement{
  construction(){
    ...
    const tmp = document.createElement('template')
    const slot = document.createElement('slot‘)
    
    card.classList.add('c-card')
    card.appendChild(slot)
    tmp.addpendChild(card)
    root.appendChild(tmp.content)
  }
}

获取/自定义属性

  • 字段属性
// html
<c-card follow='10'> // 在模板中设置属性值
</c-card>


// js
{
  constructor(){
    ...
     const card = document.createElement('div')
    // 通过 getAttribute 查询元素上是否设置自定义属性值
     const follow =  this.getAttribute('title') || 0
     console.log('follow: ', follow, typeof follow) // 值类型未字符串
    card.innerHTML = follow
  }
}
// 使用js设置属性值
const card = document.querySelector('c-card')
card.setAttribute('follow', 20)

这里有几个问题: 1. 通过 getAttribute 获取的值,类型都是字符串。2. 后续修改属性时,没有响应式的修改属性内容, 如果需要响应属性修改,需要配置标签的周期钩子。

  • getter / setter
{
  constructor(){
    this._value = 0
  }
  // 除了通过 getAttribute 获取属性外,
  // 也可以通过 getter, setter 定义属性
  get value(){
     return this._value
  }
  set value(v){
     this._value = v
      console.log('value type: ',typeof v)
  }
}
// 修改属性值
const card = document.querySelector('c-card')
card.value = 20
// value type: number

使用 getter , setter 后,可以通过直接赋值的方式设置属性值。 并且获取的值为原对象值类型,而非字符串

属性响应

上面修改非属性值时,我们无法响应属性的修改。要实现对属性的响应,需要依赖 attributeChangedCallback observedAttributes 两个属性

{
  // 属性响应函数, 属性修改后将触发该函数, 类似 vue 的watch函数
  attributeChangedCallback(name, oldValue, newValue){
    console.log(`属性名称: ${name}`)
    console.log(`旧属性值: ${oldValue}`)
    console.log(`新属性值: ${newValue}`)
  }
  // 并不是所有的属性都触发 attributeChangedCallback,只有在 observedAttributes 内注册的属性,才能触发回调
  // 注册触发响应的属性名称
  static observedAttributes = ['value']
  // observedAttributes 也可以为 getter, setter
}

生命周期钩子

自定义元素内可以配置相应的周期钩子,以执行不同任务.

  • connectedCallback 插入时
  • disconnectedCallback 删除时
  • adoptedCallback 移动时
  • attributeChangedCallback 属性修改时

自定义事件

{
  constructor(){
    const card = document.createElement('div')
    card.addEventListener('click', () => {
      // 创建自定义事件
      const event = new CustomEvent('cardClick', {detail: 'from card'})
      // 抛出事件
      this.dispatchEvent(event)
    })
  }
}
// 挂载事件监听
cont card = document.querySelector('c-card')
card.addEventListener('cardClick', ({detail}) => console.log(detail))

通信

在了解了自定义属性和自定事件后, 基本就满足了元素内与元素外通信的条件了。

模式类似vue的父子通信, 通过自定义属性获取外部值, 通过自定义事件向外抛出数据

{
  constructor(){
    this._value = 0
    this._root = this.attachShadow({ mode: 'closed' })
    this._rootElement = document.createElement('template')
    this._rootElement.innerHTML = `
<div>
 <button> add </button>
</div>
`
this._rootElement.content.querySelector('button')
.addEventListener('click', () => {
const event = new CustomEvent('add', {detail: this._value + 1})
this.dispatchEvent(event)
})
  }
  render(){
    const ele = document.createElement('div')
    ele.innerHTML = this._value
    this._root.appendChild(ele)
 }
 get value(){ return this._value }
 set value(n){ this._value = n; this.render() }
}
const ele = document.querySelect('element')
// 设置初始值
ele.value = 10
// 添加响应回调
ele.addEventListener('add', ({detail}) => {
  ele.value = detail
})

插槽

使用<slot /> 可以为标签添加嵌套功能, 与 vue slot 类似。

{
  constructor(){
    const tmp = `
      <div>
        <div>
          默认插槽: <slot />
        </div>
        <div>
          具名插槽: <slot name='sub' />
        </div>
      </div>
    `
    ...
  }
}
// html
<element>
  <div name='sub'> 附属信息 </div>
  <div> 主内容 </div>  
</element>

隐藏 / 开放

shadow DOM 提供了隐藏元素实现的能力。外部将不能影响或获取到内部元素, 通过设置mode 类型开启 or 关闭。

{
  constructor(){
    // mode 的值可以为 'open' | 'closed'
    this._root = this.attachShadow({ mode: 'closed' }) // 禁止外部访问内部节点
  } 
}

总结

web compoents 可以看作一种官方的组件化方案, 在不依赖其他MVVM框架或编译器的情况下,实现通用性组件。

在开发中,现有的API都比较简略,实际应用依然需要更上层的封装或工程化依赖做辅助。

参考

MDN Web Components

阮一峰 Web Components 入门实例教程

Web Components Tutorial or Beginners

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

推荐阅读更多精彩内容

  • 一、什么是Web Components? Web Components是一个浏览器的新功能,它允许开发者创建可重用...
    TRY_FE阅读 1,416评论 0 2
  • Web Components是W3C制定的一种规范,可用于构建独立的Web应用组件,主要包含以下4个模块:模板元素...
    何幻阅读 2,539评论 0 12
  • 前言 不知不觉,2019年即将接近尾声,现有前端三大框架也各自建立着自己的生态、自己的使用群体。从angular1...
    Kaku_fe阅读 2,778评论 0 19
  • 前言:这周完成了两场技术分享会,下周还有一场,就完成了这阶段的一个重大任务。分享会是关于 TS 的,我这两场分享会...
    CondorHero阅读 993评论 0 2
  • 一、组件化开发 1、组件化开发的优势 管理和使用非常容易,加载或卸载组件,只要添加或删除一行代码就可以了 组件非常...
    高阳刘阅读 480评论 0 0