web components

2023-03-30  本文已影响0人  林llgb

概念:

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 { 
  ...
}
生命周期:

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);

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

上一篇 下一篇

猜你喜欢

热点阅读