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 方式快速地嵌入第三方站點,如 Bilibili、Youku、QQ、Youtube、Vimeo 和 Codepen 等。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
前面咱們已經簡單介紹了 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 站爲例,它們之間的映射規則以下:瀏覽器
經過觀察上圖可知原始 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 連接。
在介紹如何預熱 TCP 連接前,咱們須要瞭解一些前置知識,如 HTML link 標籤 rel 屬性的一些特殊用途和自定義元素的生命週期鉤子。
在實際開發中能夠經過設置 link 標籤 rel 屬性來提高網頁的渲染速度(有兼容性問題),常見的類型以下:
crossorigin
屬性的值,從而能夠進行匿名預取。若需瞭解完整的連接類型,能夠訪問 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) ) }
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/...