說在前面
平時(shí)大家都是怎么管理自己的瀏覽器書簽數(shù)據(jù)的呢?有沒有過公司和家里的電腦瀏覽器書簽不同步的情況?有沒有過電腦突然壞了但書簽數(shù)據(jù)沒有導(dǎo)出,導(dǎo)致書簽數(shù)據(jù)丟失了?解決這些問題的方法有很多,我選擇自己寫個(gè)chrome插件來做書簽同步。
實(shí)現(xiàn)方案
通過 gitee 來做存取
建一個(gè)私有倉庫來保存自己的書簽?zāi)夸浶畔?,需要同步的時(shí)候再獲取 gitee 倉庫的書簽?zāi)夸浀奖镜亍_@樣不用自己寫服務(wù)端對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ),減少了很多不必要的開發(fā)工作。
實(shí)現(xiàn)步驟
一、準(zhǔn)備工作
1、新建 gitee 倉庫
直接在gitee上新建倉庫即可。
我們不想要書簽信息公開,所以選擇勾選上私有:
創(chuàng)建完的初始倉庫是這樣的:
我們?cè)傩略鲆粋€(gè)目錄,用于存放和書簽相關(guān)的文件:
在該目錄下新增一個(gè)文件,用于保存書簽導(dǎo)出的數(shù)據(jù):
二、插件編寫
完成前面的準(zhǔn)備工作,新建完 gitee 倉庫之后,我們便可以正式開始進(jìn)行插件的編寫了。
1、插件模板
- 安裝依賴
jyeontu
npm i -g jyeontu
- 獲取模板
jyeontu create
- 生成模板
根據(jù)提示輸入相關(guān)信息即可
2、giteeAPI
我們可以通過 giteeAPI 來對(duì) gitee 倉庫進(jìn)行操作,下面是 giteeAPI 的操作文檔:
https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoStargazers?ex=no
獲取gitee指定文件的內(nèi)容
我們可以通過下面代碼來獲取到gitee指定倉庫指定文件的內(nèi)容:
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編碼的文件內(nèi)容
const decoder = new TextDecoder();
const decodedData = decoder.decode(
new Uint8Array([...decodedContent].map((char) => char.charCodeAt(0)))
);
return JSON.parse(decodedData);
}
修改指定文件的內(nèi)容數(shù)據(jù)
我們需要先獲取到文件,拿到文件的sha
值,后面通過sha
來對(duì)文件進(jìn)行編輯操作。btoa
函數(shù)只能處理Latin1字符范圍內(nèi)的字符串,對(duì)超出Latin1字符范圍的字符串進(jìn)行Base64編碼,我們需要進(jìn)行以下操作,使用TextEncoder
對(duì)象來將字符串轉(zhuǎn)換為字節(jié)數(shù)組,然后再進(jìn)行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編碼的文件內(nèi)容
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);
}
}
3、indexDb存取
我們不希望每次打開都需要去重新填寫gitee倉庫的相關(guān)信息,所以這里我們使用indexDb
來對(duì)gitee倉庫的相關(guān)信息做一個(gè)保存。
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"));
};
});
}
}
4、書簽存取
獲取chrome書簽
要獲取 Chrome 瀏覽器的書簽?zāi)夸洠覀兛梢允褂?Chrome 瀏覽器提供的 API——chrome.bookmarks。下面是一個(gè)示例代碼,演示如何使用chrome.bookmarks
API 獲取 Chrome 瀏覽器的書簽?zāi)夸洠?/p>
export const getBookmarks = () => {
return new Promise((resolve) => {
chrome.bookmarks.getTree(function (bookmarkTreeNodes) {
resolve(bookmarkTreeNodes);
});
});
};
在上述代碼中,我們首先使用chrome.bookmarks.getTree()
方法獲取 Chrome 瀏覽器的書簽?zāi)夸洏洹?/p>
請(qǐng)注意,要使用chrome.bookmarks
API,你需要在你的 Chrome 插件中聲明"bookmarks"
權(quán)限。具體來說,在插件清單文件(manifest.json)中添加以下內(nèi)容:
{
"manifest_version": 2,
"name": "你的插件名稱",
"version": "1.0",
"permissions": [
"bookmarks"
],
"background": {
"scripts": [
"bg.js"
]
}
}
在上述代碼中,我們?cè)?code>"permissions"字段中聲明了"bookmarks"
權(quán)限,以便我們可以使用chrome.bookmarks
API。同時(shí),在"background"
字段中指定了一個(gè)后臺(tái)腳本(bg.js),以便我們?cè)诤笈_(tái)執(zhí)行上述代碼。
刪除chrome瀏覽器書簽
導(dǎo)入書簽前我們需要先清除一下當(dāng)前瀏覽器的書簽,通過chrome.bookmarks.removeTree
可以刪除書簽節(jié)點(diǎn)。
export function removeBookmarks(bookmarkTreeNodes) {
// 遍歷書簽樹,刪除所有的書簽
function traverseBookmarks(bookmarkNodes) {
for (const node of bookmarkNodes) {
if (node.children) {
traverseBookmarks(node.children);
}
// 刪除書簽節(jié)點(diǎn)
chrome.bookmarks.removeTree(node.id);
}
}
traverseBookmarks(bookmarkTreeNodes);
}
導(dǎo)入書簽
使用chrome.bookmarks.create
來新建書簽。
export function importBookmarks(bookmarkTreeNodes) {
// 遍歷書簽樹
function traverseBookmarks(bookmarkNodes, parentId) {
for (const node of bookmarkNodes) {
// 如果節(jié)點(diǎn)是文件夾
if (node.children) {
// 創(chuàng)建一個(gè)新的文件夾節(jié)點(diǎn)
chrome.bookmarks.create(
{
parentId: parentId,
title: node.title,
},
function (newFolderNode) {
// 遞歸遍歷子節(jié)點(diǎn)
traverseBookmarks(node.children, newFolderNode.id);
}
);
}
// 如果節(jié)點(diǎn)是書簽
else {
// 創(chuàng)建一個(gè)新的書簽節(jié)點(diǎn)
chrome.bookmarks.create({
parentId: parentId,
title: node.title,
url: node.url,
});
}
}
}
// 從根節(jié)點(diǎn)開始遍歷書簽樹
traverseBookmarks(bookmarkTreeNodes[0].children, "1");
}
插件使用
1、插件下載
直接到gitee上下載源碼即可:
源碼地址:https://gitee.com/zheng_yongtao/chrome-plug-in.git
2、導(dǎo)入插件
書簽同步插件的目錄如下:
下載完后打開瀏覽器擴(kuò)展程序管理頁面(chrome://extensions/),選擇加載已解壓的擴(kuò)展程序:
選擇插件目錄導(dǎo)入即可:
導(dǎo)入成功后就可以看到下面這個(gè)插件了
可以勾選上下面這個(gè),勾選后插件就會(huì)顯示在導(dǎo)航欄上
3、補(bǔ)充gitee倉庫信息數(shù)據(jù)
導(dǎo)入插件后,我們點(diǎn)擊導(dǎo)航欄的插件圖標(biāo),可以看到這樣一個(gè)面板,其中有四個(gè)數(shù)據(jù)需要我們填寫:
獲取 token
進(jìn)入到giteeAPI文檔進(jìn)行授權(quán)獲取到返回填寫即可,具體步驟如下:
倉庫所屬空間地址(owner
)
就是個(gè)人主頁的一個(gè)空間地址,如下圖:
倉庫路徑(repo
)
前面新建倉庫的路徑(倉庫名),如下圖:
書簽文件路徑(filePath
)
新建用于保存書簽數(shù)據(jù)的文件,想保存多份不同的數(shù)據(jù)的話可以多件幾個(gè)不同的文件分別進(jìn)行存儲(chǔ),同步的時(shí)候選擇對(duì)應(yīng)的目錄即可,如下圖:
將對(duì)應(yīng)信息填寫上之后我們就可以開始進(jìn)行同步操作了:
4、同步方式
(1)覆蓋保存
使用當(dāng)前瀏覽器書簽數(shù)據(jù)覆蓋保存到gitee倉庫中。
(2)合并保存
將當(dāng)前瀏覽器書簽數(shù)據(jù)與gitee倉庫中的書簽數(shù)據(jù)合并好再進(jìn)行保存。
(3)覆蓋獲取
使用gitee倉庫中的書簽數(shù)據(jù)覆蓋掉本地的書簽數(shù)據(jù)。
(4)合并獲取
將gitee倉庫中的書簽數(shù)據(jù)和本地的書簽數(shù)據(jù)合并后再覆蓋掉本地的書簽數(shù)據(jù)。
(5)合并規(guī)則
同一層級(jí)并且同名的目錄我們會(huì)將其子節(jié)點(diǎn)合并到同一目錄下,同一層級(jí)下我們會(huì)根據(jù) 書簽名 + 書簽url 對(duì)該層級(jí)的書簽進(jìn)行去重。
源碼
1、gitee
gitee 地址:https://gitee.com/zheng_yongtao/chrome-plug-in/tree/master/chrome-bookmarks-manage
2、公眾號(hào)
關(guān)注公眾號(hào)『前端也能這么有趣』發(fā)送 chrome插件
即可獲取源碼。文章來源:http://www.zghlxwxcb.cn/news/detail-719679.html
說在后面
?? 這里是 JYeontu,現(xiàn)在是一名前端工程師,有空會(huì)刷刷算法題,平時(shí)喜歡打羽毛球 ?? ,平時(shí)也喜歡寫些東西,既為自己記錄 ??,也希望可以對(duì)大家有那么一丟丟的幫助,寫的不好望多多諒解 ??,寫錯(cuò)的地方望指出,定會(huì)認(rèn)真改進(jìn) ??,偶爾也會(huì)在自己的公眾號(hào)『前端也能這么有趣』發(fā)一些比較有趣的文章,有興趣的也可以關(guān)注下。在此謝謝大家的支持,我們下文再見 ??。文章來源地址http://www.zghlxwxcb.cn/news/detail-719679.html
到了這里,關(guān)于一鍵同步,無處不在的書簽體驗(yàn):探索多電腦Chrome書簽同步插件的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!