Skip to content

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 ,页面里的数据则只能存入服务端数据库

数据服务 ​

流程:

  1. 填数据服务地址(例如 https://example.com/api,只填域名会自动补 /api)
  2. 点击 配对 后,自动发起配对申请,并显示配对码与要执行的命令
  3. 在服务器上执行 npm run pair -- <配对码>
  4. 批准后将 地址和取得的凭证 写进 origin 的存储

安全 ​

扩展和站点会经过校验:

  • 扩展侧:event.origin 必须等于服务器地址的 origin、 event.source 必须来自 iframe
  • 站点侧:event.origin 必须是 chrome-extension:// 或 moz-extension:// 或 safari-web-extension:// 开头、 event.source 必须是 window.parent

Released under the Apache-2.0 License.