customElements 实战之 Lite-embed

  • 2019 年 11 月 29 日
  • 筆記

一、Lite-embed 简介

Lite-embed 的灵感来源于 paulirish 大神的 lite-youtube-embed 项目:

Provide videos with a supercharged focus on visual performance. This custom element renders just like the real thing but approximately 224X faster. 提供具有视觉效果的视频。这个自定义元素的渲染方式与真实的效果一样,但是速度提高了约 224 倍。

Lite-embed 是基于 customElements Web Components 规范开发的组件,支持以 iframe 方式快速地嵌入第三方站点,如 BilibiliYoukuQQYoutubeVimeoCodepen 等。

通过扩展 Lite-embed 项目中 services.ts 服务类的匹配规则,开发者可以方便地内嵌其它支持 iframe 方式嵌入的站点,除此之外基于 services.ts 服务类,也可以让富文本编辑器支持自动解析剪贴板中的网址,自动以 iframe 的方式嵌入所指定的内容。这里我们以 B 站的某个视频为例,它的原始地址是:

https://www.bilibili.com/video/av53834726?spm_id_from=333.851.b_62696c695f7265706f72745f616  e696d65.73

其对应的 iframe 内嵌代码如下:

<iframe src="//player.bilibili.com/player.html?aid=53834726&cid=94168196&page=1"     scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

当用户需要嵌入上述网址对应的视频时,一般需要手动点击视频下方的分享链接,然后复制上述的 iframe 内嵌代码,再添加到目标页面中。Lite-embed 所实现的功能之一就是实现自动解析,即根据设置的地址,按照一定的匹配规则,最终生成对应的 iframe 内嵌代码。对于上述的需求,Lite-embed 使用起来也很简单,具体如下:

<!--  Bilibili -->  <h2>www.bilibili.com</h2>  <lite-embed src="https://www.bilibili.com/video/av53834726?     spm_id_from=333.851.b_62696c695f7265706f72745f616e696d65.73" height="200">  </lite-embed>

当然如果只是实现上述功能的话,那么 Lite-embed 并没有多大的意义。Lite-embed 除了实现自动解析功能之外,还实现了在悬停视频封面或海报时,预热(可能)要使用的 TCP 连接和 iframe 内嵌网页懒加载的功能。

二、Lite-embed 开发实战

2.1 实现自动解析

前面我们已经简单介绍了 Lite-embed 的功能,下面我们来介绍一下如何一步步实现 Lite-embed 组件。首先我们先来定义 LiteEmbed 类,该类继承于 HTMLElement 类,在 LiteEmbed 类中除了前面示例中使用的 src 和 height 属性之外,我们还定义了 posterUrl、prefetchUrlSet 和 embedOption 属性。

class LiteEmbed extends HTMLElement {    static prefetchUrlSet = new Set() // 预取URL链接集合    private src: string // 内嵌网页的url地址    private height: number // 高度    private posterUrl: string // 封面url地址    private embedOption: EmbedOption | null // 内嵌站点的配置信息  }

embedOption 属性的类型是 EmbedOption,它用于表示内嵌站点的配置信息,EmbedOption 接口定义:

export interface EmbedOption {    site: string    height: number    source: string    embed: string    html: string    preconnects: string[]  }

接着我们来介绍如何实现自动解析,要实现自动解析的前提是原始 url 地址和 iframe 内嵌地址这两个地址之间存在一定的映射规则。以 B 站为例,它们之间的映射规则如下:

通过观察上图可知原始 url 地址上的 av 字符串之后的序列号对应 iframe src 地址中 aId 参数的值。所以我们可以利用正则表达式来实现地址的映射,具体如下:

bilibili: {    regex: /https?://www.bilibili.com/video/av([^?]+)?.+/,    embedUrl: 'https://player.bilibili.com/player.html?aid=<%= remote_id %>&page=1',    html: `<iframe scrolling='no' frameborder='no' allowtransparency='true'     allowfullscreen='true' style='width: 100%;' height="{{HEIGHT}}" src="{{SRC}}"></iframe>`,    height: 498,    preconnects: ['https://player.bilibili.com', 'https://api.bilibili.com',     'https://s1.hdslb.com']  },

上面除了定义了地址映射相关的 regex、embedUrl 和 html 三个属性之外,我们还定义了 height 和 preconnects 属性,分别表示 iframe 的默认高度和预链接地址列表。除了 B 站之外,目前 Lite-embed 还支持 YoukuQQYoutubeVimeoCodepen 等站点,为了统一处理映射规则并方便后期扩展,我们来新增一个 Matcher 类,具体代码如下:

Matcher 类

export default class Matcher {    static matches(url: string): EmbedOption | null {      if (!url) return null      let result = null      for (let site of Object.keys(RULES)) {        if ((result = Matcher.match(site, url)) != null) {          return result        }      }      return result    }      static match(site: string, url: string): EmbedOption | null {      // const defaultIdsHandler = (ids: string[]) => ids.shift()!      const { regex, embedUrl, html, height, id = defaultIdsHandler, preconnects } =        RULES[site]      const matches: RegExpExecArray | null = regex.exec(url)      if (matches != null) {        const result = matches.slice(1)        const embed = embedUrl.replace(/<%= remote_id %>/g, id(result))        return {          site,          source: url,          height,          embed,          preconnects,          html        }      }      return null    }  }

在 Matcher 类中我们定义了两个静态方法,即 matches 和 match 方法。在 matches 方法内部会获取预设的规则,然后逐一进行地址匹配。而 match 方法内部实现的主要功能是地址的映射和参数的填充。介绍完自动解析的实现方式,接下来我们来介绍如何预热 TCP 链接。

2.2 预热 TCP 链接

在介绍如何预热 TCP 链接前,我们需要了解一些前置知识,如 HTML link 标签 rel 属性的一些特殊用途和自定义元素的生命周期钩子。

在实际开发中可以通过设置 link 标签 rel 属性来提升网页的渲染速度(有兼容性问题),常见的类型如下:

  • prefetch:提示浏览器提前加载链接的资源,因为它可能会被用户请求。建议浏览器提前获取链接的资源,因为它很可能会被用户请求。 从 Firefox 44 开始,考虑了 crossorigin 属性的值,从而可以进行匿名预取。
  • preconnect:向浏览器提供提示,建议浏览器提前打开与链接网站的连接,而不会泄露任何私人信息或下载任何内容,以便在跟随链接时可以更快地获取链接内容。
  • preload:告诉浏览器下载资源,因为在当前导航期间稍后将需要该资源。
  • prerender:建议浏览器事先获取链接的资源,并建议将预取的内容显示在屏幕外,以便在需要时可以将其快速呈现给用户。
  • dns-prefetch:提示浏览器该资源需要在用户点击链接之前进行 DNS 查询和协议握手。

若需了解完整的链接类型,可以访问 MDN – Link Type

为了支持动态添加 link 元素设置该元素对应的 rel 属性,我们来定义一个 addPrefetch 方法,该方法用于实现预加载或预链接,具体实现如下:

static addPrefetch(kind: string, url: string, as?: string) {      if (LiteEmbed.prefetchUrlSet.has(url)) return // 避免创建重复的link元素      const linkElem = document.createElement('link')      linkElem.rel = kind      linkElem.href = url      if (as) {        (linkElem as any).as = as      }      linkElem.crossOrigin = 'true'      document.head.appendChild(linkElem)      LiteEmbed.prefetchUrlSet.add(url)  }

接着我们来介绍另一个知识点 —— 自定义元素的生命周期钩子。自定义元素可以定义特殊生命周期钩子,以便在其存续的特定时间内运行代码。 这称为自定义元素响应。目前自定义元素支持的生命周期钩子如下:

名称

调用时机

constructor

创建或升级元素的一个实例。用于初始化状态、设置事件侦听器或创建 Shadow DOM。参见规范,了解可在 constructor 中完成的操作的相关限制。

connectedCallback

元素每次插入到 DOM 时都会调用。用于运行安装代码,例如获取资源或渲染。一般来说,您应将工作延迟至合适时机执行。

disconnectedCallback

元素每次从 DOM 中移除时都会调用。用于运行清理代码(例如移除事件侦听器等)。

attributeChangedCallback(attrName, oldVal, newVal)

属性添加、移除、更新或替换。解析器创建元素时,或者升级时,也会调用它来获取初始值。Note: 仅 observedAttributes 属性中列出的特性才会收到此回调。

adoptedCallback()

自定义元素被移入新的 document(例如,有人调用了 document.adoptNode(el))。

下面我们将使用 constructor 和 connectedCallback 钩子,在 constructor 钩子中完成 LiteEmbed 类相关属性的初始化,在 connectedCallback 钩子中完成播放按钮的创建和设置相关的事件监听,相关的处理逻辑比较简单,我们直接上代码:

构造函数

class LiteEmbed extends HTMLElement {    constructor() {      super()      this.src = this.getAttribute('src') || ''      this.height = Number(this.getAttribute('height'))      this.posterUrl =        this.getAttribute('poster-url') || 'https://i.ytimg.com/vi/ogfYd705cRs/hqdefault.jpg'      this.embedOption = Matcher.matches(this.src)      LiteEmbed.addPrefetch('preload', this.posterUrl, 'image')    }  }

生命周期钩子

connectedCallback() {      if (this.embedOption != null) {        // 设置背景图片        this.style.backgroundImage = `url("${this.posterUrl}")`        this.style.height = this.getAttribute('height') || this.embedOption.height.toString()          // 创建播放按钮        const playBtn = document.createElement('div')        playBtn.classList.add('lte-playbtn')        this.appendChild(playBtn)          // 鼠标悬停时,预热(可能)要使用的TCP连接。    		// once: true 表示listener在添加之后最多只调用一次。如果是true,        // listener会在其被调用之后自动移除。        this.addEventListener(          'pointerover',          () => LiteEmbed.warmConnections(this.embedOption!.preconnects),          { once: true }        )        // 一旦用户点击,添加实际的iframe        this.addEventListener('click', e => this.addIframe())      }  }

在 connectedCallback 方法中,我们监听 pointerover 事件,在该事件触发后,我们调用 warmConnections 方法提前预热可能要使用的 TCP 链接,warmConnections 方法内部的逻辑也简单就是遍历预设的 preconnects 数组,然后动态创建 link 标签,相关的代码如下:

static warmConnections(preconnects: string[]) {      preconnects.forEach(preconnect =>        LiteEmbed.addPrefetch('preconnect', preconnect)      )  }

2.3 懒加载 iframe 内嵌网页

Lite-embed 组件要实现的最后一个功能就是懒加载 iframe 内嵌网页,即当用户点击海报或播放按钮的时候,才创建 iframe 元素进而开始加载内嵌网页。这里我们通过定义一个 addIframe 方法来实现该功能:

addIframe() {      if (this.embedOption != null) {        const finalEmbedOption = {          ...this.embedOption,          ...{ height: this.height, src: this.embedOption.embed }        }        const iframeHTML = this.embedOption.html.replace(          /{{(w*)}}/g,          (m: string, key: string) => {            return (finalEmbedOption as any)[key.toLowerCase()]          }        )        this.insertAdjacentHTML('beforeend', iframeHTML)        this.classList.add('lyt-activated')      }  }

至此 Lite-embed 的所有功能已经介绍完了,就差最后一步即定义 lite-embed 元素,代码很简单一行就搞定了:

customElements.define('lite-embed', LiteEmbed)

三、总结

本文详细介绍了如何利用 customElements Web Components 规范来开发 Lite-embed 组件,该组件虽然带了一些好处,比如提高嵌入页面的加载速度,但同时也存在一些问题,比如在点击视频封面或海报时,才开始动态加载 iframe,会造成需要二次点击才能正常播放嵌入的视频。对 Lite-embed 组件感兴趣的小伙伴可以访问 lite-embed,具体的项目地址如下:

https://github.com/semlinker/lite-embed

四、参考资源


欢迎小伙伴们订阅前端全栈修仙之路,及时阅读 Angular、TypeScript、Node.js/Java和Spring技术栈最新文章。