开篇废话
之前因为川普〇击案的新闻然后打开了 TikT〇k 。
突然看到弹窗了新功能 Floating Player
在点击试用后的我马上意识到现在的浏览器可以画中画里面可以插 HTML 了。
于是在想如何给自己的项目引入。
首先参考 Chrome developer 的文章查看大概原理,然后再来看我的实践:
正文
浏览器适配度
首先泼一盆冷水:
从 caniuse.com 来看,只有 Chromium 116+ 以上的版本支持。
并且 Firefox / Safari 目前还是不支持的。
因此目前只能当作实验特性,一定要用备用方案提供才行,否则用户将无法正常交互。
使用场景
画中画使用场景基本上是给播放器使用的。
非播放器的画中画目前只观察到 Android 里面的 HttpCanary
在抓包时候的 UI 了。
本文中将介绍我个人项目的一个使用场景并且展示如何将其引入。
原来的逻辑
我选中了一个项目的打开日志中心的功能来进行修改,之前的方案是 window.open
以 popup(弹窗)的方式打开个新网页,大概代码是这个样子的:
window.open(
"/logViewer.php",
"_status_1",
"width=1024,height=768,status=no,location=no,toolbar=no,menubar=no,scrollbars=no"
);
此网页,我希望用户可以同时打开多个日志页面并且不被干扰(原来页面也经常用得到,直接 _blank
打开又要切换回去)
因此最后使用 popup
弹窗来打开的方案,网页内弄个全屏 modal 或者新开个正常的标签页都被我否决了。
实现
触发PIP
首先可以知道是 documentPictureInPicture
的功能实现,于是我们的代码逻辑应该是这个样子:
if ('documentPictureInPicture' in window && !navigator.userAgent.includes('Android') && !navigator.userAgent.includes('Mobile')) {
const pipWindow = await documentPictureInPicture.requestWindow({
width:1024,
height:768,
});
} else {
window.open(
"/logViewer.php",
"_status_1",
"width=1024,height=768,status=no,location=no,toolbar=no,menubar=no,scrollbars=no"
);
}
在这个逻辑里,如果我们判断是 PC 设备(在移动端 PIP 感觉意义不明)然后浏览器支持 documentPictureInPicture
则启用,否则为 fallback 方案(window.open
):
可以看到在右下角弹了个 pip 窗口,不过画面是白的(毕竟我们还没开始给窗口写东西)
F12 可以看到,location.href
= about:blank
不过不用担心,请求的时候还是同域的,可以放心传曲奇之类的。
写入内容
一开始我想的是,直接 location.href = xxx
不就可以了,于是我试了一下:
pipWindow.location.href = '/logViewer.php';
然后 PIP 窗口就消失了。
Chrome developer 的教程是教你移动网页现有的元素到 DOM 窗口。
在下面的例子中,本处元素除了样式以外会被继承到 PIP 里面。
点击这几行的文字会弹窗,在 PIP 里面点击也会弹窗,但是文字从红色变成了黑色,也就是 css 样式默认不会继承。
然后点击「运行」后,此处描述文本将会移动到 PIP 窗口里面。
const example1 = document.querySelector("#example1");
const pipWindow = await documentPictureInPicture.requestWindow();
pipWindow.document.body.append(example1);
// 使用 CloneNode 将不会继承事件
// pipWindow.document.body.append(example1.cloneNode(true););
复制样式看个人喜好,如果懒的话窗口内直接再引用和主界面一样的 CSS 文件就可以,也可以先拷贝样式,参考官网教程即可,这里不再演示。
因此没有什么办法快速将 popup 弹窗改成 PIP
了吗?
在思考了一阵后,还是有的。
我们可以在PIP里面塞个 iframe 来实现最少的代码改动。
不过感觉是在套娃了,并且要求 iframe 里面的窗口是同域,或者用些奇怪的办法过 CORS (比如说鉴权部分用别的逻辑来处理)
最后代码变成了这个样子:
const pipWindow = await documentPictureInPicture.requestWindow();
pipWindow.document.body.innerHTML = `<style>body{margin:0;} iframe{border:0;width:100%;height:100%;}</style><iframe src="/"></iframe>`
sandbox 之类的策略请参考 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe
一下子迁移工作量就少了很多呢~
交互
这部分内容相信看 Chrome developer 的教程更详细,无非是把 document
变成了 pipWindow.document
(以此类推)
比如这是检测网页被导航至新的 url 的检测例子:
const pipWindow = await documentPictureInPicture.requestWindow();
pipWindow.document.body.innerHTML = `<style>body{margin:0;} iframe{border:0;width:100%;height:100%;}</style><iframe src="/"></iframe>`
pipWindow.document.querySelector('iframe').addEventListener('load', ()=>{
console.log('网页被导航至新 url');
});
// 当然也可以继续套娃,在 iframe 插点奇奇怪怪的东西:
pipWindow.document.querySelector('iframe').contentWindow.addEventListener('beforeunload', function(event) {
event.preventDefault();
event.returnValue = '';
});
其他交互看按照需求来做,总的来说比 popup
方便一点。
popup 只能用 postmessage 之类的来交互,写的好累啊(
总结
最终我放弃了 PIP 方案,因为有一些局限性:
- PIP 是独占模式,即最多只有 1 个 PIP 窗口,这对于我有同时打开多个窗口的需求还是差了点意思。
- 网页被关闭后PIP窗口也会被关闭,在播放器里面是非常合理的,不过在我的需求里感觉不太行
- 始终为在右下角打开,不能先指定位置,如果用户正好没注意到右下角可能会有疑惑
- 窗口有最大限制,只能 3/4 左右大小(能基本占满屏幕的话就成为新一代牛皮癣了)
完