平时大家都是怎么管理自己的浏览器书签数据的呢?有没有过公司和家里的电脑浏览器书签不同步的情况?有没有过电脑突然坏了但书签数据没有导出,导致书签数据丢失了?解决这些问题的方法有很多,我选择自己写个chrome插件来做书签同步。
建一个私有仓库来保存自己的书签目录信息,需要同步的时候再获取 gitee 仓库的书签目录到本地。这样不用自己写服务端对数据进行存储,减少了很多不必要的开发工作。
直接在gitee上新建仓库即可。
完成前面的准备工作,新建完 gitee 仓库之后,我们便可以正式开始进行插件的编写了。
jyeontu
npm i -g jyeontu
jyeontu create
根据提示输入相关信息即可
我们可以通过 giteeAPI 来对 gitee 仓库进行操作,下面是 giteeAPI 的操作文档:
https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoStargazers?ex=no
我们可以通过下面代码来获取到gitee指定仓库指定文件的内容:
async function fetchFileContent(apiUrl, accessToken) {
const response = await fetch(apiUrl, {
headers: {
Authorization: "token " + accessToken,
},
});
const fileData = await response.json();
return fileData.content;
}
export async function getFile(gitInfo) {
const accessToken = gitInfo.token;
const apiUrl =
"https://gitee.com/api/v5/repos/" +
gitInfo.owner +
"/" +
gitInfo.repo +
"/contents/" +
gitInfo.filePath;
const fileContent = await fetchFileContent(apiUrl, accessToken);
const decodedContent = atob(fileContent); // 解码Base64编码的文件内容
const decoder = new TextDecoder();
const decodedData = decoder.decode(
new Uint8Array([...decodedContent].map((char) => char.charCodeAt(0)))
);
return JSON.parse(decodedData);
}
我们需要先获取到文件,拿到文件的sha
值,后面通过sha
来对文件进行编辑操作。
btoa
函数只能处理Latin1字符范围内的字符串,对超出Latin1字符范围的字符串进行Base64编码,我们需要进行以下操作,使用TextEncoder
对象来将字符串转换为字节数组,然后再进行Base64编码。
async function fetchFileContent(apiUrl, accessToken) {
const response = await fetch(apiUrl, {
headers: {
Authorization: "token " + accessToken,
},
});
const fileData = await response.json();
return fileData.content;
}
async function getDecodedContent(content) {
const decodedContent = atob(content); // 解码Base64编码的文件内容
const decoder = new TextDecoder();
const decodedData = decoder.decode(
new Uint8Array([...decodedContent].map((char) => char.charCodeAt(0)))
);
return JSON.parse(decodedData);
}
async function putFileContent(apiUrl, accessToken, encodedContent, sha) {
const commitData = {
access_token: accessToken,
content: encodedContent,
message: "Modified file",
sha: sha,
};
const putResponse = await fetch(apiUrl, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "token " + accessToken,
},
body: JSON.stringify(commitData),
});
if (putResponse.ok) {
console.log("File modified successfully.");
} else {
console.error("Failed to modify file.");
}
}
export async function modifyFile(gitInfo, modifiedContent) {
const accessToken = gitInfo.token;
const apiUrl =
"https://gitee.com/api/v5/repos/" +
gitInfo.owner +
"/" +
gitInfo.repo +
"/contents/" +
gitInfo.filePath;
try {
const fileContent = await fetchFileContent(apiUrl, accessToken);
const content = await getDecodedContent(fileContent);
modifiedContent = mergeBookmarks(content, modifiedContent);
modifiedContent = JSON.stringify(modifiedContent);
const encoder = new TextEncoder();
const data = encoder.encode(modifiedContent);
const encodedContent = btoa(
String.fromCharCode.apply(null, new Uint8Array(data))
);
await putFileContent(apiUrl, accessToken, encodedContent, fileContent.sha);
} catch (error) {
console.error("An error occurred:", error);
}
}
我们不希望每次打开都需要去重新填写gitee仓库的相关信息,所以这里我们使用indexDb
来对gitee仓库的相关信息做一个保存。
export class IndexedDB {
constructor(databaseName, storeName) {
this.databaseName = databaseName;
this.storeName = storeName;
this.db = null;
}
open() {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(this.databaseName);
request.onerror = () => {
reject(new Error("Failed to open database"));
};
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
this.db = event.target.result;
if (!this.db.objectStoreNames.contains(this.storeName)) {
this.db.createObjectStore(this.storeName, {
keyPath: "id",
autoIncrement: true,
});
}
};
});
}
createDatabase() {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(this.databaseName);
request.onerror = () => {
reject(new Error("Failed to create database"));
};
request.onsuccess = () => {
this.db = request.result;
this.db.close();
resolve();
};
request.onupgradeneeded = (event) => {
this.db = event.target.result;
if (!this.db.objectStoreNames.contains(this.storeName)) {
this.db.createObjectStore(this.storeName, {
keyPath: "id",
autoIncrement: true,
});
}
this.db.close();
resolve();
};
});
}
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
add(data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.add(data);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(new Error("Failed to add data"));
};
});
}
getAll() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readonly");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(new Error("Failed to get data"));
};
});
}
getById(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readonly");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(new Error("Failed to get data"));
};
});
}
delete(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(new Error("Failed to delete data"));
};
});
}
update(id, newData) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(this.storeName, "readwrite");
const objectStore = transaction.objectStore(this.storeName);
const getRequest = objectStore.get(id);
getRequest.onsuccess = () => {
const oldData = getRequest.result;
if (!oldData) {
const addRequest = objectStore.add({ ...newData, id });
addRequest.onsuccess = () => {
resolve({ ...newData, id });
};
addRequest.onerror = () => {
reject(new Error("Failed to add data"));
};
} else {
const mergedData = { ...oldData, ...newData };
const putRequest = objectStore.put(mergedData);
putRequest.onsuccess = () => {
resolve(mergedData);
};
putRequest.onerror = () => {
reject(new Error("Failed to update data"));
};
}
};
getRequest.onerror = () => {
reject(new Error("Failed to get data"));
};
});
}
}
要获取 Chrome 浏览器的书签目录,我们可以使用 Chrome 浏览器提供的 API——chrome.bookmarks。下面是一个示例代码,演示如何使用chrome.bookmarks
API 获取 Chrome 浏览器的书签目录:
export const getBookmarks = () => {
return new Promise((resolve) => {
chrome.bookmarks.getTree(function (bookmarkTreeNodes) {
resolve(bookmarkTreeNodes);
});
});
};
在上述代码中,我们首先使用chrome.bookmarks.getTree()
方法获取 Chrome 浏览器的书签目录树。
请注意,要使用chrome.bookmarks
API,你需要在你的 Chrome 插件中声明"bookmarks"
权限。具体来说,在插件清单文件(manifest.json)中添加以下内容:
{
"manifest_version": 2,
"name": "你的插件名称",
"version": "1.0",
"permissions": [
"bookmarks"
],
"background": {
"scripts": [
"bg.js"
]
}
}
在上述代码中,我们在"permissions"
字段中声明了"bookmarks"
权限,以便我们可以使用chrome.bookmarks
API。同时,在"background"
字段中指定了一个后台脚本(bg.js),以便我们在后台执行上述代码。
导入书签前我们需要先清除一下当前浏览器的书签,通过chrome.bookmarks.removeTree
可以删除书签节点。
export function removeBookmarks(bookmarkTreeNodes) {
// 遍历书签树,删除所有的书签
function traverseBookmarks(bookmarkNodes) {
for (const node of bookmarkNodes) {
if (node.children) {
traverseBookmarks(node.children);
}
// 删除书签节点
chrome.bookmarks.removeTree(node.id);
}
}
traverseBookmarks(bookmarkTreeNodes);
}
使用chrome.bookmarks.create
来新建书签。
export function importBookmarks(bookmarkTreeNodes) {
// 遍历书签树
function traverseBookmarks(bookmarkNodes, parentId) {
for (const node of bookmarkNodes) {
// 如果节点是文件夹
if (node.children) {
// 创建一个新的文件夹节点
chrome.bookmarks.create(
{
parentId: parentId,
title: node.title,
},
function (newFolderNode) {
// 递归遍历子节点
traverseBookmarks(node.children, newFolderNode.id);
}
);
}
// 如果节点是书签
else {
// 创建一个新的书签节点
chrome.bookmarks.create({
parentId: parentId,
title: node.title,
url: node.url,
});
}
}
}
// 从根节点开始遍历书签树
traverseBookmarks(bookmarkTreeNodes[0].children, "1");
}
直接到gitee上下载源码即可:
源码地址:https://gitee.com/zheng_yongtao/chrome-plug-in.git
下载完后打开浏览器扩展程序管理页面(chrome://extensions/),选择加载已解压的扩展程序:
选择插件目录导入即可:
导入插件后,我们点击导航栏的插件图标,可以看到这样一个面板,其中有四个数据需要我们填写:
进入到giteeAPI文档进行授权获取到返回填写即可,具体步骤如下:
owner
)repo
)filePath
)新建用于保存书签数据的文件,想保存多份不同的数据的话可以多件几个不同的文件分别进行存储,同步的时候选择对应的目录即可,如下图:
使用当前浏览器书签数据覆盖保存到gitee仓库中。
将当前浏览器书签数据与gitee仓库中的书签数据合并好再进行保存。
使用gitee仓库中的书签数据覆盖掉本地的书签数据。
将gitee仓库中的书签数据和本地的书签数据合并后再覆盖掉本地的书签数据。
同一层级并且同名的目录我们会将其子节点合并到同一目录下,同一层级下我们会根据 书签名 + 书签url 对该层级的书签进行去重。
gitee 地址:https://gitee.com/zheng_yongtao/chrome-plug-in/tree/master/chrome-bookmarks-manage
关注公众号『前端也能这么有趣』发送 chrome插件
即可获取源码。
这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 ,平时也喜欢写些东西,既为自己记录 ,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 ,写错的地方望指出,定会认真改进 ,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 。