qrcode

马上订阅,开启修仙之旅

customElements 实战之 Lite-embed

一、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 站的某个视频为例,它的原始地址是:

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

其对应的 iframe 内嵌代码如下:

1
2
<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 使用起来也很简单,具体如下:

1
2
3
4
5
<!--  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 属性。

1
2
3
4
5
6
7
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 接口定义:

1
2
3
4
5
6
7
8
export interface EmbedOption {
site: string
height: number
source: string
embed: string
html: string
preconnects: string[]
}

接着我们来介绍如何实现自动解析,要实现自动解析的前提是原始 url 地址和 iframe 内嵌地址这两个地址之间存在一定的映射规则。以 B 站为例,它们之间的映射规则如下:

bilibili-url-mapping

通过观察上图可知原始 url 地址上的 av 字符串之后的序列号对应 iframe src 地址中 aId 参数的值。所以我们可以利用正则表达式来实现地址的映射,具体如下:

1
2
3
4
5
6
7
8
9
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 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 方法,该方法用于实现预加载或预链接,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
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 钩子中完成播放按钮的创建和设置相关的事件监听,相关的处理逻辑比较简单,我们直接上代码:

构造函数

1
2
3
4
5
6
7
8
9
10
11
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')
}
}

生命周期钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 标签,相关的代码如下:

1
2
3
4
5
static warmConnections(preconnects: string[]) {
preconnects.forEach(preconnect =>
LiteEmbed.addPrefetch('preconnect', preconnect)
)
}

2.3 懒加载 iframe 内嵌网页

Lite-embed 组件要实现的最后一个功能就是懒加载 iframe 内嵌网页,即当用户点击海报或播放按钮的时候,才创建 iframe 元素进而开始加载内嵌网页。这里我们通过定义一个 addIframe 方法来实现该功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 元素,代码很简单一行就搞定了:

1
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技术栈最新文章。

qrcode