customElements 实战之 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 内嵌代码,再添加到目标页面中。Lite-embed 所实现的功能之一就是实现自动解析,即根据设置的地址,按照一定的匹配规则,最终生成对应的 iframe 内嵌代码。对于上述的需求,Lite-embed 使用起来也很简单,具体如下:
www.bilibili.com
当然如果只是实现上述功能的话,那么 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: ``, 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 ,具体的项目地址如下:
https://github.com/semlinker/lite-embed
全栈修仙之路,及时阅读 Angular、TypeScript、Node.js/Java和Spring技术栈最新文章。