当我们开发扩展程序的时候,一般不会意识到扩展程序在无痕模式下的表现,因为基本上不会遇到什么问题。但是,我在开发划词翻译的时候,却因为无痕模式遇到了两次 bug,最终促使我好好调查一下无痕模式到底会对扩展程序产生什么影响。
在这之前,我先简单描述一下背景。我为了让划词翻译能在 PDF 文件里使用,于是将 PDF.js 的 viewer(即https://mozilla.github.io/pdf.js/web/viewer.html)打包进了划词翻译当中。安装划词翻译之后,在地址栏内粘贴 chrome-extension://ikhdkkncnoglghljlkmcimlnlhkeamad/pdf-viewer/web/viewer.html 就能看到这个 PDF 阅读器。
我第一个遇到的无痕模式的问题就跟这个 PDF 阅读器有关。2016 年的时候,有用户反馈在无痕模式(当时的称呼是“隐私模式”)里打不开 PDF 阅读器(见 https://github.com/lmk123/crx-selection-translate/issues/164),在查阅了 Chrome 扩展程序的文档之后,我很快找到了解决方案:给 manifest.json 加一个新的属性 "incognito": "split"。
Chrome 扩展程序开发文档里对这个属性的说明文档地址是 https://developer.chrome.com/docs/extensions/mv2/manifest/incognito/。简单点说,扩展程序在无痕模式下有两种运行模式:
- 默认的模式是
spanning,这种情况下,在无痕模式里的内容脚本发送的消息都是发送到普通模式的扩展程序背景页当中的,收到的消息会用一个属性标识是否来自无痕模式。但是,这种模式不允许在无痕窗口里打开扩展程序里的 HTML,这也是划词翻译的 PDF 阅读器不能在无痕模式里使用的原因。 - 可以改为
split模式,这种情况下,无痕模式的窗口会单独生成一个无痕模式下的背景页,内容脚本发送的消息都是发送给无痕模式下的背景页,跟普通模式下的背景页完全分离开,互不干扰。使用这种模式就可以正常在无痕模式的窗口中打开扩展程序里的 HTML。
转眼时间过去了五年,我遇到了第二个跟无痕模式有关的问题。最近有一个用户反馈说,只要在无痕模式里使用划词翻译,就会导致划词翻译的账号掉线(见 https://github.com/lmk123/crx-selection-translate/issues/969)。
整个扩展程序里,跟无痕模式有关的配置就只有 manifest.json 里的 "incognito": "split"。在重新阅读了这个配置项的说明后,我明白了出现这个问题的过程:
- 首先,用户在普通模式下登录了划词翻译。划词翻译的服务器会在浏览器写入一个 cookie,而划词翻译会将用户的信息(用户名、用户 id 等)保存在扩展程序的 chrome.storage 里。
- 然后,当用户打开一个无痕模式的窗口的时候,由于我设置了
"incognito": "split",所以浏览器运行了一个新的背景页进程,但是,无痕模式下是没有第一步中的 cookie 的,所以当这个新的背景页进程访问了服务器的接口时,服务器发现没有 cookie,于是就返回了 401 给扩展程序。 - 当无痕模式里的背景页接收到服务器的 401 响应后,便删除了 chrome.storage 里保存的用户信息。然而,虽然普通模式和无痕模式的两个背景页进程之间是隔离的,但是它们的 chrome.storage 数据是相通的,所以此时,普通模式里的划词翻译的账号就掉线了。
明白了问题的过程,解决方案也就有了:我们只需要把用户的认证凭证从 cookie 里转到浏览器里的 chrome.storage 里就可以了——但是实施起来却遇到了问题。
用户在反馈这个问题的时候,Chrome 的版本是 88;当我实施解决方案的时候,Chrome 的版本是 90,而这个版本对扩展程序在无痕模式下的表现却发生了一些变化,而且,我不确定这是 feature 还是 bug(但我猜应该是 bug)。
这个变化就是: split 模式下,无痕模式里的扩展程序没有主机权限了。
发现这个变化的原因是,在无痕模式下划词翻译内置的 PDF 阅读器不能加载网上的 PDF 文件了,会报 CORS 错误——这是不应该出现的错误,因为划词翻译是有 <all_urls> 权限的。我尝试在无痕模式下的内容脚本、PDF 阅读器、背景页这三个地方执行 fetch('https://www.baidu.com'),发现只有内容脚本能正常发起请求,而其它两个地方都会报 CORS 错误。
我查找了 Chrome 90 的变更日志(见 https://chromereleases.googleblog.com/2021/04/stable-channel-update-for-desktop_20.html),但是并没有找到相关的说明,所以我猜测这应该是个 bug。
我又在之前为了测试兼容性问题而安装的 Chromium 65 里试了下,这时的表现就跟文档上描述一致:在无痕模式打开 PDF 阅读器时会直接显示“被拦截”。
但是,因为 Chrome 90 的这个变化,我需要重新考虑解决方案了。
无论 Chrome 90 的这个变化是不是一个 bug,但肯定会有用户在 Chrome 90 上使用划词翻译,而由于 CORS 的问题,用户压根就无法在无痕模式里正常使用划词翻译。
我还在 stack overflow 搜到一个相关的回答,意思是 split 模式对扩展程序而言不常用,而且总是有 bug;我还想起来,Firefox 是不支持 split 模式的,它在无痕模式下的运行方式只能是 spanning。
基于以上原因,我决定将划词翻译在无痕模式下的运行模式从 split 改回默认的 spanning,这需要对划词翻译做以下的变动:
- 当需要打开划词翻译的设置页、PDF 阅读器或任意扩展程序内部的 HTML 文件时,总是在普通模式的网页中打开;
- 如果用户自行在无痕模式下打开了扩展程序内部的 HTML 文件,最好是能检测到并转为在普通模式里打开;如果不能,也需要给个提示“xxx 页面在无痕模式下不可用”
- 在设置页或其它地方给用户提示,告诉用户无痕模式下的划词翻译会有一些限制。
当我将 "incognito": "split" 改为 "incognito": "spanning" 之后,我发现即使是在无痕模式下调用的 chrome.tabs.create() 方法,它也会改为在普通模式的窗口中打开,而不是像 "incognito": "split" 时会直接在无痕窗口中打开,这样,以上改造中的第一条其实就“自动”完成了。
对于第二条,其实几乎没有用户会通过直接在地址栏输入地址的方式打开内置 PDF 阅读器,所以这个情况可以忽略不计;但是,划词翻译为了添加“自动打开 PDF”功能,会使用 chrome,webRequest 将在线 pdf 的网址转到内置 PDF 阅读器,此时就需要判断在线 PDF 的网址是否是在无痕窗口加载的了,如果是的话就需要用 chrome.tabs.create() 方法在普通窗口下打开内置 PDF 阅读器,因为无痕模式下打开内置 PDF 阅读器会显示一个错误页面。
对于第三条,最后我没有在设置页加这个提示,而是当内置 PDF 加载的是来自无痕窗口的在线 PDF 时才会给出一个提示。因为,毕竟只有在无痕模式使用内置 PDF 时才需要提示用户知道,否则其他情况跟在普通窗口里使用其实没有区别。