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技術棧最新文章。