NitaiPage 浏览器扩展实现
目录结构
extension/
├── manifest.json # 清单
├── ext-bridge.js # 通信、地址校验
├── newtab-ext.html # 新标签页
├── newtab-ext.js # 连接与授权
├── offline-ext.html # 离线页
├── popup-ext.html
├── popup-ext.js # 收藏逻辑
├── icons/ # 图标
├── img/ # 离线页壁纸
└── _locales/ # 语言包所有浏览器扩展 API 统一从 ext-bridge.js 导出的 ext 访问,内部用 globalThis.browser ?? globalThis.chrome 选择实现
注意 Firefox 的 browser.* 返回 Promise,Chromium 的 chrome.* 是回调风格,桥接层需要统一 Promise 化,混用会得到 undefined
扩展 origin 不统一,禁止硬编码 chrome-extension:// ,应使用 ext.runtime.getURL('') 获取当前扩展根地址,或用协议正则 /^(chrome|moz|safari-web)-extension:$/ 判断
清单
json
{
"manifest_version": 3,
"permissions": ["activeTab", "storage"],
"optional_permissions": ["favicon"],
"host_permissions": [
"https://tab.nitai.cc/*",
"https://tab-dev.nitai.cc/*"
],
"optional_host_permissions": ["*://*/*"],
"action": { "default_popup": "popup-ext.html" },
"chrome_url_overrides": { "newtab": "newtab-ext.html" }
}通信
javascript
{
channel: 'nppext',
type: 'request' | 'response' | 'notice',
id: '...',
method: '...',
params: { }
}| 方向 | 方法 | 说明 |
|---|---|---|
| 站点 → 扩展 | ext.hello | 握手,返回{ version, capabilities },可用于 是否位于扩展环境 的检测 |
| 站点 → 扩展 | ext.requestPermission | 检查权限能否使用 |
| 扩展 → 站点 | site.addShortcut | 新增一条捷径,参数{ url, title, icon } |
| 扩展 → 站点 | site.setDataSource | 指定数据存到哪个自建服务,参数{ mode, url, token } |
| 通知 | site.ready | 站点已就绪 |
| 通知 | ext.permissionGranted | 用户完成了授权 |
扩展能力
capabilities 目前是 ['favicon'],扩展能力会随版本扩充。
插件可以通过通过 window.nppExt 请求:
javascript
if (window.nppExt?.isExtension) {
const { granted } = await window.nppExt.requestPermission('bookmarks')
if (!granted) {
// 用户拒绝或浏览器弹出授权窗口
window.nppExt.onPermissionGranted((permission) => {
// 授权完成后重试
})
}
}获取图标
用收藏时带的图标
site.addShortcut 的 icon 就是扩展从当前标签页取到的 favIconUrl,只允许 data: 与 http(s):
按 url 去重,已存在同地址时返回 created: false ,不改动原有记录
用扩展获取指定地址的 icon
javascript
// 未安装扩展 window.nppExt 是 undefined
if (window.nppExt?.isExtension) {
const { granted } = await window.nppExt.requestPermission('favicon')
if (granted) {
// 取不到则为 null
const icon = await window.nppExt.getFavicon('https://github.com', 64)
} else {
// 用户授权
const off = window.nppExt.onPermissionGranted((permission) => {
if (permission !== 'favicon') return
off()
retry()
})
}
}需要注意:
size在16–128之间,默认32,获取到的是data:- Firefox 不支持 favicon,
capabilities为空、getFavicon返回null - 空图标、超过 256KB 的响应返回
null
服务器地址
支持正式站点、开发站点、离线页(只展示离线页、不连接站点,页面上不显示状态条)、自定义地址。自定义地址有部分限制:
https:一律允许http:仅允许localhost、127.0.0.1、::1、私有网段10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16- 拒绝
javascript:、data:、blob:、file:等
保存时会自动调用 permissions.request({ origins: [...] }),获取授权,已授权时返回 true
自定义地址填入自建的 NitaiPage 数据库服务,或者仅填入前端页面,详见 自建服务端。自建服务如果开启 ONLY_SERVER ,页面里的数据则只能存入服务端数据库
数据服务
流程:
- 填数据服务地址(例如
https://example.com/api,只填域名会自动补 /api) - 点击 配对 后,自动发起配对申请,并显示配对码与要执行的命令
- 在服务器上执行
npm run pair -- <配对码> - 批准后将 地址和取得的凭证 写进 origin 的存储
安全
扩展和站点会经过校验:
- 扩展侧:
event.origin必须等于服务器地址的 origin、event.source必须来自 iframe - 站点侧:
event.origin必须是chrome-extension://或moz-extension://或safari-web-extension://开头、event.source必须是window.parent