web components

概念:

web components是原生的组件化开发技术,它可以让我们创建自定义的html元素,并且功能和样式都会封装在组件内部,不会影响其他的元素。

注意:web components与现有的react、vue等库不冲突,是相辅相成的。

web components里面有三个概念:

1. Custom elements(自定义元素)
2. Shadow Dom(影子元素)
3. HTML templates(HTML模版)

Custom elements

自定义元素是在js中通过继承HTMLElement或现有的HTML DOM对象来实现的。

class CustomElement extends HTMLElement { 
  ...
}
生命周期:
  • connectedCallback(挂载时): 当自定义元素第一次被连接到文档DOM时被调用。
  • disconnectedCallback(卸载时): 当自定义元素与文档DOM断开连接时被调用。
  • adoptedCallback(移动时): 当自定义元素被移动到新文档时被调用。
  • attributeChangedCallback(属性变化时): 当自定义元素的一个属性被增加、移除或更改时被调用。

Shadow Dom

shadow dom与普通的document对象几乎一样,它是专门用来操作自定义的html元素,它也是一个树形结构,但是shadow dom完全独立于普通的dom,相当于是一个隔离区,需要把它挂载到一个普通的dom节点上。


HTML templates

html 模版是方便于编写自定义元素的html结构和css样式。它包括两个标签:template和slot。这里的slot与vue中的slot类似,用于指定一些占位的插槽,在外边可以用真实的元素替换掉。

<template>
    <style>
    </style>
    <div>
        <h1></h1>
        <slot name="content"></slot>
    </div>
</template>

举例

<blog-post>

  1. index.html定义模版
<template id="blog-post-template">
    <div>
        <h1>博客标题</h1>
        <slot name="content"></slot>
        <button>查看全文</button>
    </div>
</template>
  1. 在BlogPost.js中定义BlogPost类
class BlogPost extends HTMLElement {
  constructor() {
    // 调用父类的构造函数才能初始化
    super() 
    const template = document.getElementById('blog-post-template')
    // attachShadow获取shadow dom的根元素,mode为open意思是允许通过shadow dom的api来操作和访问该自定义元素内部的dom树。使用appendChild来添加到根元素中。这里cloneNode ,这样子可以使得多次使用自定元素时,内容都是独立的。
    this.attachShadow({ mode: "open" }).appendChild(
      template.content.cloneNode(true)
    )
  }
}
// 将BlogPost注册到自定义元素注册表中,这里的名字必须带有中划线,目的是和原生的html元素区分开
customElements.define("blog-post", BlogPost)
  1. 在index.html使用
<body>
    <blog-post>
        <article slot="content">这是博客内容</article>
    </blog-post>
    
    <!-- type 必须为 module,否则变量名会冲突 -->
    <script src="BlogPost.js" type="module"></script>
</body>
  1. 设置自定义的title属性

index.html:

<blog-post title="博客标题">
    <article slot="content">这是博客内容</articel>
</blog-post>

BlogPost.js

class BlogPost extends HTMLElement {
  constructor() {
    // ...
    // this.shadowRoot是shadowDom中的根元素,它要在调用this.attachShadow之后才能使用。其中的api和document中的几乎一样。
    this.titleEle = this.shadowRoot.querySelector("h1")
  }
  
  static get observedAttributes() {
    return ["title"]
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "title") {
      this.titleEle.textContent = newValue
    }
  }
}

  1. 设置样式

在template中使用style标签编写样式: 在template中写的样式,只会应用到template内部的元素中。

<template>
    <style>
        div {}
        h1 {}
        button {}
    </style>
    <div>
        <h1></h1>
        <slot name="content"></slot>
        <button>查看全文</button>
    </div>
</template>
  1. 在js中定义template
    在html中定义多个template会占用大量的空间和代码,不好维护。可以使用在js中定义template。
const template = document.createElement("template")
template.innerHTML = `
  <style>
    ...
  </style>
  <div>
    <h1></h1>
    <slot name="content"></slot>
    <button>查看全文</button>
  </div>
`
class BlogPost extends HTMLElement {
  constructor() {
    super()
    // const template = document.getElementById('blog-post-template')
    this.attachShadow({ mode: "open" }).appendChild(
      template.content.cloneNode(true)
    )
  }
}

<post-list>

  1. 定义模版、创建PostList类
const template = document.createElement("template")
template.innerHTML = `
  <style>
    div {
      ...
    }
    article {
      ...
    }
  </style>
  <div></div>
`

class PostList extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: "open" }).appendChild(
      template.content.cloneNode(true)
    );
  }
}

customElements.define("post-list", PostList);

  1. 加载数据、创建博客列表
class PostList extends HTMLElement {
  constructor() {
    ...
  }

  async connectedCallback() {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts")
    const posts = await res.json()
    this.initPosts(posts)
  }
  
  // 创建博客列表
  initPosts(posts) {
    const div = this.shadowRoot.querySelector("div")
    posts.forEach(post => {
      const blogPostEle = div.appendChild(document.createElement("blog-post"))

      // 博客标题
      blogPostEle.setAttribute("title", post.title)

      // 博客文章
      const article = blogPostEle.appendChild(
        document.createElement("article")
      )
      article.slot = "content"
      article.innerHTML = post.body
    })
  }
}

customElements.define("post-list", PostList)

  1. 给添加事件
class BlogPost extends HTMLElement {
  constructor() {
    // ...
    this.articleSlot = this.shadowRoot.querySelector("slot")
    this.content = ""
    this.article = null // 替换后的元素对象
  }

  slotChange() {
    // assignedElements获取真实的替换元素数组 
    const elements = this.articleSlot.assignedElements()
    const article = elements[0]
    // 真实的article元素保存到article属性中
    this.article = article
    // 保存博客全文到content属性中
    this.content = this.article.innerHTML
    // 把article中的博客全文改成摘要
    this.article.innerHTML = this.getExcept()
  }

  getExcept() {
    return this.content.slice(0, 60) + "..."
  }

  connectedCallback() {
    this.articleSlot.addEventListener("slotchange", this.slotChange.bind(this));
  }
}
  1. 给按钮添加点击事件,维护显示/隐藏状态
class BlogPost extends HTMLElement {
  constructor() {
    // ...
    this.buttonEle = this.shadowRoot.querySelector("button")
    this.showFullArticle = false
  }

  /**
   * 按钮点击事件,控制是否显示全文。
   */
  toggleFull() {
    this.showFullArticle = !this.showFullArticle
    if (this.showFullArticle) {
      this.article.innerHTML = this.content
      this.buttonEle.textContent = "隐藏全文"
    } else {
      this.article.innerHTML = this.getExcept()
      this.buttonEle.textContent = "查看全文"
    }
  }

  connectedCallback() {
    // ...
    this.buttonEle.addEventListener("click", this.toggleFull.bind(this))
  }

  disconnectedCallback() {
    // 卸载事件监听,在组件销毁的时候,释放内存
    this.buttonEle.removeEventListener("click", this.toggleFull())
    this.articleSlot.removeEventListener("slotchange", this.slotChange)
  }
}

总结

创建自定义元素的步骤:

  1. 编写模版代码和样式
  2. 创建自定义元素class,继承HtmlElement
  3. 使用CustomeElement.define() 注册元素
  4. 在构造函数中使用super() 调用父类构造函数,并编写初始化逻辑
  5. 使用生命周期加载数据、注册监听和卸载监听

React与Web Component互相调用

Web Component可以在React中使用。但因为React有自己的模块化机制(Component),以及自己的事件系统(SyntheticEvent), 考虑到调用方式和事件系统的统一,官方推荐将web component包装为react component。

class HelloMessage extends React.Component{
  render() {
    return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
  }
}

React模块也可作为Web Component使用。只需在attachedCallback中调用ReactDOM.render。

class XSearch extends HTMLElement {
  connectedCallback() {
    const mountPoint = document.createElement('span');
    this.attachShadow({ mode: 'open' }).appendChild(mountPoint);

    const name = this.getAttribute('name');
    const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
    ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
  }
}
customElements.define('x-search', XSearch);

文中有什么错误或者不足之处,欢迎大家指正...

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

推荐阅读更多精彩内容