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 方式快速地嵌入第三方站點,如 Bilibili、Youku、QQ、Youtube、Vimeo 和 Codepen 等。
通過擴展 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 還支援 Youku、QQ、Youtube、Vimeo 和 Codepen 等站點,為了統一處理映射規則並方便後期擴展,我們來新增一個 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,具體的項目地址如下:
四、參考資源
歡迎小夥伴們訂閱前端全棧修仙之路,及時閱讀 Angular、TypeScript、Node.js/Java和Spring技術棧最新文章。