安装、调试与发布
安装
- 打开插件管理页面 chrome://extensions
- 开启 Developer mode
- 导入文件夹(点击 Load Unpack 或者直接拖拽至管理面板)
调试
如果你已经安装的插件代码发生了变化 ,你可以在插件管理页面点击刷新图标手动刷新,或者在插件里调用下面的 API 重新加载插件:
window.chrome.runtime.reload();
发布
通过 Chrome 商店 发布插件是首选。如果插件未通过 Chrome 商店审核,则有被 Chrome 浏览器禁用戓删除的风险,通过启用开发者模式以文件夹安装的插件例外。首次发布时,Chrome 商店会为用 OpenSSL 为插件生成 RSA 私钥,再加密这个私钥生成 ID,做为插件的唯一标识符,以后更新时也会保持这个 ID 不变。
更新
API 概览
想要对插件支持的功能有一个概览,最直接的方式就是查看 manifest.json 文档:
{
"manifest_version": 2,
"name": "My Extension",
"version": "versionString",
"default_locale": "en",
"description": "A plain text description",
"icons": {...},
"browser_action": {...},
"page_action": {...},
"action": ...,
"author": ...,
"automation": ...,
"background": {
"persistent": false,
"service_worker":
},
"chrome_settings_overrides": {...},
"chrome_url_overrides": {...},
"commands": {...},
"content_capabilities": ...,
"content_scripts": [{...}],
"content_security_policy": "policyString",
"converted_from_user_script": ...,
"current_locale": ...,
"declarative_net_request": ...,
"devtools_page": "devtools.html",
"differential_fingerprint": ...,
"event_rules": [{...}],
"externally_connectable": {
"matches": ["*://*.example.com/*"]
},
"file_browser_handlers": [...],
"file_system_provider_capabilities": {
"configurable": true,
"multiple_mounts": true,
"source": "network"
},
"homepage_url": "http://path/to/homepage",
"host_permissions": ...,
"import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
"incognito": "spanning, split, or not_allowed",
"input_components": ...,
"key": "publicKey",
"minimum_chrome_version": "versionString",
"nacl_modules": [...],
"natively_connectable": ...,
"oauth2": ...,
"offline_enabled": true,
"omnibox": {
"keyword": "aString"
},
"optional_permissions": ["tabs"],
"options_page": "options.html",
"options_ui": {
"chrome_style": true,
"page": "options.html"
},
"permissions": ["tabs"],
"platforms": ...,
"replacement_web_app": ...,
"requirements": {...},
"sandbox": [...],
"short_name": "Short Name",
"signature": ...,
"spellcheck": ...,
"storage": {
"managed_schema": "schema.json"
},
"system_indicator": ...,
"tts_engine": {...},
"update_url": "http://path/to/updateInfo.xml",
"version_name": "aString",
"web_accessible_resources": [...]
}
由于大部分插件 API 都需在 manifest.json 中的 permissions 里声明,我们还可以通过 permissions 支持的选项来发现更多 API。Permissions 文档 里列出了 69 个选项,我们随机选取了一些做为示例:
|
"alarms"
|
Gives your extension access to the chrome.alarms API.
|
|
"bookmarks"
|
Gives your extension access to the chrome.bookmarks API.
|
|
"clipboardRead"
|
Required if the extension or app uses document.execCommand('paste').
|
|
"contextMenus"
|
Gives your extension access to the chrome.contextMenus API.
|
|
"cookies"
|
Gives your extension access to the chrome.cookies API.
|
|
"declarativeNetRequest"
|
Gives your extension access to the chrome.declarativeNetRequestAPI.
|
|
"desktopCapture"
|
Gives your extension access to the chrome.desktopCapture API.
|
|
"notifications"
|
Gives your extension access to the chrome.notifications API.
|
|
"sessions"
|
Gives your extension access to the chrome.sessions API.
|
|
"storage"
|
Gives your extension access to the chrome.storage API.
|
|
"system.cpu"
|
Gives your extension access to the chrome.system.cpu API.
|
|
"webRequest"
|
Gives your extension access to the chrome.webRequest API.
|
并非所有插件模块都能使用这些 API。如果 API 文档里没有明示 API 的使用范围,最实用的方法就是直接尝试。如果配了 permissions 却仍然不能使用 API,一般就是受使用范围的限制。Content script 就只能访问少量插件 API,但可以通过消息传递机制让父插件的 background 页去执行。
通信机制
消息传送
一次性消息
发送消息时,根据接收对象使用不同的 API:
// 发送消息至插件页面(不包括 Content script)
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
// 发送消息至当前 Web 页面的 Content script
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
console.log(response.farewell);
});
});
// 同步响应
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
});
// 异步响应,需要返回 true,否则 sendResponse 会被回收
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting == "hello") {
setTimeout(() => {
sendResponse({farewell: "goodbye"});
}, 200);
return true;
}
});
保持会话
插件中的消息实际上都是通过通道(Port)进行传输的,你可以调用 runtime.connect 来建立长时间的双向会话。如果给会话指定了 name,还能区分不同的会话。
建立会话:
var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question == "Who's there?")
port.postMessage({answer: "Madame"});
else if (msg.question == "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
});
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name == "knockknock");
port.onMessage.addListener(function(msg) {
if (msg.joke == "Knock knock")
port.postMessage({question: "Who's there?"});
else if (msg.answer == "Madame")
port.postMessage({question: "Madame who?"});
else if (msg.answer == "Madame... Bovary")
port.postMessage({question: "I don't get it."});
});
});
直接调用
同时运行在父插件进程中的页面,可以直接使用对方的全局变量,无需经过消息传递。获取父插件页面的引用,有以下两种方式:
- chrome.extension.getViews() —— 默认获取父插件中运行的所有页面,可指定页面类型
- chrome.extension.getBackgroundPage() —— 获取父插件的 background 页面
// background.js
const bgName = 'test';
let counter = 0;
const generateId = () => ++ counter;
// popup.js
const bg = chrome.extension.getBackgroundPage();
console.log(bg.bgName); // 输出 test
console.log(bg.generateId()); // 输出 1
实践总结
监控 web 页面 xhr 请求
// content_script.js
// 改写原生的 xhr 代码
const scriptContent = `(function() {
var proxied = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function() {
// 将请求参数传送给 content script
window.postMessage({type: 'openXhr', args: arguments}, '*');
return proxied.apply(this, [].slice.call(arguments));
};
})()`;
// 以 script 标签形式注入 web 页面
const head = document.getElementsByTagName('head')[0];
if (head) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.text = scriptContent;
head.appendChild(script);
head.removeChild(script); // 注入脚本后立即删除,以防 web 页面检测出来
}
// 监听 web 页面消息
window.addEventListener('message', e => {
if(e.data.type === 'openXhr') {
...
}
});
Try...catch 似乎不生效
// background.js
try {
chrome.tabs.get(tabId, (tab) => {
...
});
} catch (e) {
...
}
// background.js
chrome.tabs.get(tabId, (tab) => {
if(chrome.runtime.lastError) {
...
}
...
});
如何实现不重装也能自动更新插件
没有用户喜欢重新安装。如果是从 Chrome 商店安装的插件,浏览器会定期自动更新。如果你的插件没有发布在 Chrome 商店上,也能实现自动更新,而且无需审核,立即更新。 简单地说,就是把插件做为一个「壳子」,插件模块加载时,查询最近 webpack 打包时生成的 chunkMap, 以 xhr 请求的形式获取最新 JS 内容,然后用 eval 执行 JS 脚本。实现的时候,还应该考虑异常处理和缓存优化。 如果你更改了配置文件 manifest.json,只有手动重装或通过 Chrome 商店更新才能生效。