Chrome 插件开发概论
开发插件需要的技术:HTML、CSS、JavaScript
插件构成
manifest.json
:必要。插件的基本信息,包括名称、版本号、图标、脚本入口等。
background script
:用于调用 Chrome 插件 API,是插件跨域请求、网页截屏、弹出 Chrome 通知消息的承担者。可以视为一个隐藏的浏览器页面,在插件管理中可打开background.html
进行调试。
content script
:插件注入到页面的脚本,不会显式地出现在页面 DOM 结构中。可以操作 DOM,但与页面本身的脚本是隔离的,无法访问页面本身的变量或函数,相当于独立的沙盒运行。可以调用有限的 Chrome 插件 API,但无法进行跨域请求。
功能页面
:包括插件图标的弹出页面popup
和插件配置页面options
。
通信架构
1 2 3 4 5 6 7 8 9 10 11
| graph TD background.js -.-> popup.js popup.js -.-> background.js popup.html-.->popup.js popup.js-.->popup.html 窗口-.->contentscript.js contentscript.js-.->窗口 background.js-->contentscript.js contentscript.js-->background.js 窗口-->background.js background.js-->窗口
|
浏览器中的作用范围
功能页面是每个 window 一份,但是每个 tab 都会注入 content script
脚本注入
声明式注入
1 2 3 4 5 6 7 8 9
| { "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], "run_at": "document_idle", "js": ["content.js"] } ] }
|
- matches 表示页面 url 匹配时才加载
run_at
表示在什么时机加载,一般是 document_idle
,避免 content_scripts 影响页面加载性能。
需要注意的是,如果用户已经打开了 N 个页面,然后再安装插件,这 N 个页面除非重新刷新,否则是不会加载 manifest 声明的 content_scripts。
安装插件之后新打开的页面是可以加载 content_scripts 的。
所以需要在用户点击插件图标时,探测页面中的 content_scripts 是否存在(发送消息是否有响应/出错),再提示用户刷新页面。
程序注入
使用程序动态注入脚本
1 2 3
| chrome.tabs.executeScript({ file: "content.js", });
|
如用户点击插件图标时执行注入脚本,则无需刷新页面
1 2 3 4 5 6
| chrome.browserAction.onClicked.addListener(() => { chrome.tabs.executeScript({ file: 'content.js', }); });
|
content.js 的执行会影响其所在的沙盒。
1 2 3 4 5 6 7 8
|
if (!window.contentLoaded) { window.contentLoaded = true; }
console.log(window.contentLoaded);
|
使用沙盒内的全局变量则可以避免 content.js 重复执行带来的问题。
声明式只会注入一次,缺点是可能需要刷新页面。程序式不需要刷新页面,缺点是可能会注入多次。
permissions
该字段是一个字符串数组,用来声明插件需要的权限,这样才能调用某些 chrome API
tabs
activeTab
contextMenus:网页右键菜单,browser_action 右键菜单
cookies:操作 cookie,和用户登录态相关的功能可能会用到该权限
storage:插件存储,不是 localStorage
web_accessible_resources:网页能访问的插件内部资源,比如插件提供 SDK 给页面使用,如 ethereum 的 metamask 钱包插件。或者是修改 DOM 结构用到了插件的样式、图片、字体等资源。
background script
它是在一个隐藏的 tab 中执行,所在的页面域名为空
比如 background 需要和 a.com 通信。首先应该把 *://*.a.com/*
加入到 manifest 的 permissions 数组中。
当发送网络请求时,浏览器会自动带上 a.com 的 cookie,服务器的 set-cookie 也会对浏览器生效。这是符合预期的。
background 设置 document.cookie 时,不能指定域名,否则会设置失败。
1 2 3 4 5
| document.cookie = `session=xxxxxxx; domain=a.com; max-age=9999999999; path=/`;
document.cookie = `session=xxxxxxx; max-age=9999999999`;
|
background 使用 tabs 接口操作浏览器的 tab 窗口
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| async function open(url: string): Promise<number> { return new Promise((resolve) => { chrome.tabs.create( { url, }, (tab) => resolve(tab.id!) ); }); }
async function getActiveTab(): Promise<chrome.tabs.Tab | null> { return new Promise((resolve) => { chrome.tabs.query( { active: true, currentWindow: true, }, (tabs) => { if (tabs.length > 0) { resolve(tabs[0]); } else { resolve(null); } } ); }); }
async function activate( tabId?: number, url?: string ): Promise<number | undefined> { if (typeof tabId === "undefined") { return tabId; }
const options: chrome.tabs.UpdateProperties = IS_FIREFOX ? { active: true } : { selected: true }; if (url) { options.url = url; }
return new Promise((resolve) => { chrome.tabs.update(tabId, options, () => resolve(tabId)); }); }
async function openOrActivate(url: string): Promise<number> { const pattern = getUrlPattern(url); return new Promise<number>((resolve) => { chrome.tabs.query( { url: pattern, }, (tabs) => { if (tabs.length > 0 && tabs[0].id) { return Tabs.activate(tabs[0].id); } else { this.open(url).then((id) => resolve(id)); } } ); }); }
|
content script
它只能使用有限的 chrome API
由于 content 可以访问 DOM,可以用它来选择、修改、删除、增加网页元素。
但是 content 是运行在隔离的空间(类似沙盒),所以如果需要和页面的其他脚本通信,需要采用 window.postMessage
的方式。
资源注入
content 可以向页面中注入 <script>
,由此给页面提供 SDK 等功能,注入的脚本和页面自己的脚本一样,都无法和 content 直接通信。
注意:注入的资源要先在 menifest 的 web_accessible_resources
字段中声明。
1 2 3 4
| const script = document.createElement("script"); script.src = chrome.runtime.getURL("sdk.js"); document.body.appendChild(script);
|
1 2 3 4 5
| window.jsbridge = { version: "1.0.1", };
|
注入的 sdk.js 脚本是可以被页面内其他脚本访问到的
常用的通信 API 是 chrome.runtime.sendMessage
。
Reference
chrome 插件开发指南 - 掘金 (juejin.cn)