前言
一键上传选中的网页内容,实现知识快速收藏。如飞书剪存,有道云剪报,MrDoc速记。早在2008年,我参考了有道云一键上传,实现了一个简单的浏览器插件,能方便保存网页内容到个人网站。这些插件目前都很难兼容全部的浏览器,Chrome支持好一些。
时移势迁,光阴荏苒,这一小插件已经使用了十多年。之前作为网页书签存在,现在已经变成浏览器插件了。但之前的插件版本只支持Chrome和Opera,不支持firefox,而我平常用Chrome访问国外站点,firefox访问国内站点和个人开发网站。所以近期终于得闲开发了firefox版本。
个人使用,所以也没美化。
实现原理
调用window.getSelection()获得选中的网页内容,然后发给popup脚本,再在popup里调用fetch()函数上传数据。
网页剪报功能简述
以飞书剪存为例,它是一个兼容于各大浏览器的扩展程序。它可以将网页正文保存至飞书文档,且自动剥离广告。
一键保存网页正文,告别手动复制粘贴: 浏览到喜欢的网页,点击飞书剪存,即可将网页内容保存至你的飞书文档中。
智能剥离网页广告,告别无用干扰信息: 飞书剪存可以智能识别并去除悬浮或嵌在网页中的广告,还你干净、无噪的网页内容。
插件定义
v2插件定义
这个是飞书的:
{
"name": "__MSG_appName__",
"description": "__MSG_CreationDoc_Operation_AppStoreBrief__",
"version": "1.0.26",
"manifest_version": 2,
"default_locale": "zh_CN",
"browser_action": {
"default_icon": {
"16": "assets/icons/48.png",
"24": "assets/icons/48.png",
"32": "assets/icons/48.png"
},
"default_title": "__MSG_appName__"
},
"icons": {
"16": "assets/icons/48.png",
"32": "assets/icons/48.png",
"48": "assets/icons/48.png",
"128": "assets/icons/128.png"
},
"background": {
"persistent": false,
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"run_at": "document_idle",
"js": [
"content.js"
]
}
],
"permissions": [
"tabs",
"activeTab",
"contextMenus",
"cookies",
"storage",
"*://*/*"
],
"web_accessible_resources": [
"assets/*",
"app.html",
"app.js",
"page-post-frame-id.html",
"page-post-frame-id.js"
],
"incognito": "not_allowed",
"content_security_policy": "script-src 'self' 'unsafe-eval' https://*.bytegoofy.com blob: https://*.ibytedapm.com ; object-src 'self'",
"homepage_url": "https://www.feishu.cn/hc/zh-CN/articles/606278856233"
}
v3版本的插件定义
{
"manifest_version": 3,
"name": "CMS一键上传8.0",
"version": "8.0",
"homepage_url": "http://www.icodelib.cn",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["jquery-1.8.3.js","content-script.js", "inject.js"]
}
],
// 浏览器右上角图标设置,browser_action、page_action、app必须三选一
"action": {
"default_title": "一键上传",
"default_popup": "popup.html",
"default_icon": {
"16": "icons/16x16.png",
"48": "icons/48x48.png",
"128": "icons/128x128.png"
}
},
"host_permissions": ["<all_urls>"],
"web_accessible_resources": [{"resources": ["inject.js", "popup.js"], "matches": ["<all_urls>"]}],
"permissions":
[
"contextMenus", // 右键菜单
"tabs", // 标签
"activeTab",
"scripting",
"notifications", // 通知
"webRequest", // web请求
"webRequestBlocking",
"webNavigation",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequest",
"declarativeNetRequestFeedback",
"storage"
]
// "content_security_policy": "script-src 'self' 'unsafe-eval'"
}
实现
在content-script里获得选中内容,然后发送:
// 发送消息给后台
async function sendMessageToBackground(html) {
try {
await browser.runtime.sendMessage({
src: window.location.href,
title: document.title,
content: html
})
// showMsg('上传完成')
} catch (err) {
console.error(err);
// showMsg('上传失败')
}
}
在popup.js里监听消息,然后获得数据后,调用后端API:
browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
const formData = new FormData();
formData.append('src', message.src);
formData.append('title', message.title);
formData.append('content', message.content);
try {
const response = await fetch(target_site, {
method: 'POST',
mode: "cors",
body: formData,
})
const result = await response.json();
debug(result)
} catch (err) {
debug(err)
}
// sendResponse({success: 'ok'})
return true;
});
火狐需要用户选择接受外部访问
const permissions = {
// This origin is listed in host_permissions:
origins: ["<all_urls>"],
};
const checkbox_host_permission = document.getElementById("checkbox_host_permission");
checkbox_host_permission.onchange = async () => {
if (checkbox_host_permission.checked) {
let granted = await browser.permissions.request(permissions);
if (!granted) {
// Permission request was denied by the user.
checkbox_host_permission.checked = false;
}
} else {
try {
await browser.permissions.remove(permissions);
} catch (e) {
// While Chrome allows granting of host_permissions that have manually
// been revoked by the user, it fails when revoking them, with
// "Error: You cannot remove required permissions."
console.error(e);
checkbox_host_permission.checked = true;
}
}
};
browser.permissions.contains(permissions).then(granted => {
checkbox_host_permission.checked = granted;
});
发布
对于Firefox而言,插件开发完后,需要上传到https://addons.mozilla.org/签名才能安装。
无品牌的firefox构建版本可以安装不用签名的插件。在about:config里将xpinstall.signatures.required设为false即可。此方法能装上,但功能有点问题,所以我还是选择了正式途径。
注意事项
- 后端API需要使用https
当使用manifest_version: 3时,fetch()函数会报错,TypeError: NetworkError when attacking to fetch resource,网上查了半天,发现v2和v3的诸多不同,且Content Security Policy (CSP) 的限制,需要以https或wss协议访问外部才行。 - firefox需要插件有一个addon ID
链接
-Firefox插件开发文档
-火狐Unbranded_Builds