customElements 實戰之 Lite-embed

1、Lite-embed 簡介

Lite-embed 的靈感來源於 paulirish 大神的 lite-youtube-embed 項目:javascript

Provide videos with a supercharged focus on visual performance. This custom element renders just like the real thing but approximately 224X faster.

提供具備視覺效果的視頻。這個自定義元素的渲染方式與真實的效果同樣,可是速度提升了約 224 倍。html

Lite-embed 是基於 customElements Web Components 規範開發的組件,支持以 iframe 方式快速地嵌入第三方站點,如 BilibiliYoukuQQYoutubeVimeoCodepen 等。java

經過擴展 Lite-embed 項目中 services.ts 服務類的匹配規則,開發者能夠方便地內嵌其它支持 iframe 方式嵌入的站點,除此以外基於 services.ts 服務類,也可讓富文本編輯器支持自動解析剪貼板中的網址,自動以 iframe 的方式嵌入所指定的內容。這裏咱們以 B 站的某個視頻爲例,它的原始地址是:git

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

其對應的 iframe 內嵌代碼以下:github

<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 內嵌網頁懶加載的功能。typescript

2、Lite-embed 開發實戰

2.1 實現自動解析

前面咱們已經簡單介紹了 Lite-embed 的功能,下面咱們來介紹一下如何一步步實現 Lite-embed 組件。首先咱們先來定義 LiteEmbed 類,該類繼承於 HTMLElement 類,在 LiteEmbed 類中除了前面示例中使用的 src 和 height 屬性以外,咱們還定義了 posterUrl、prefetchUrlSet 和 embedOption 屬性。api

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 站爲例,它們之間的映射規則以下:瀏覽器

bilibili-url-mapping-wm.jpg

經過觀察上圖可知原始 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)

3、總結

本文詳細介紹瞭如何利用 customElements Web Components 規範來開發 Lite-embed 組件,該組件雖然帶了一些好處,好比提升嵌入頁面的加載速度,但同時也存在一些問題,好比在點擊視頻封面或海報時,纔開始動態加載 iframe,會形成須要二次點擊才能正常播放嵌入的視頻。對 Lite-embed 組件感興趣的小夥伴能夠訪問 lite-embed,具體的項目地址以下:

https://github.com/semlinker/...

4、參考資源

相關文章
相關標籤/搜索