什么是 Chrome书签
Chrome书签是Google Chrome浏览器中的一项功能,它允许用户保存他们经常访问或感兴趣的网页链接。用户可以将这些网页链接存储在书签栏、其他文件夹或使用关键词进行搜索。
以下是一个典型的Chrome书签HTML文件的示例:
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
<DT><H3 ADD_DATE="1700000000" LAST_MODIFIED="1700000000" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Bar</H3>
<DL><p>
<DT><A HREF="https://www.google.com" ADD_DATE="1700000001">Google</A>
<DT><A HREF="https://www.example.com" ADD_DATE="1700000002">Example</A>
<DT><H3 ADD_DATE="1700000003" LAST_MODIFIED="1700000003">Folder</H3>
<DL><p>
<DT><A HREF="https://www.example.org" ADD_DATE="1700000004">Example Org</A>
</DL><p>
</DL><p>
</DL><p>
各个部分的说明:
-
文档头部
<!DOCTYPE NETSCAPE-Bookmark-file-1>:说明这是一个兼容Netscape的书签文件格式,许多浏览器都使用这种格式。<META>标签:定义字符编码(一般是UTF-8)。<TITLE>和<H1>:文档标题,通常是 "Bookmarks"。
-
根层级
<DL>标签<DL>:定义了书签目录的层级(类似于HTML的有序列表)。<DT>:定义每个项目或文件夹。
-
书签项
<A>标签HREF:书签链接的目标网址。ADD_DATE:书签添加的时间戳(Unix时间戳格式)。LAST_MODIFIED:书签最后修改的时间戳。
-
文件夹
<H3>标签<H3>:文件夹名称。PERSONAL_TOOLBAR_FOLDER:如果存在该属性,表示这是浏览器工具栏上的书签文件夹。
-
嵌套
<DL>- 文件夹内部可以包含一个嵌套的
<DL>,用来存储该文件夹下的书签或子文件夹。
- 文件夹内部可以包含一个嵌套的
使用js 生成 Chrome 书签html
数据结构可以根据自身的业务进行调整。
前端数据结构
const bookmarksdata = [
{
key:'1',
label:'类目A',
children:[
{
menuName: '谷歌',
menuIcon: '', // Chrome 书签文件中并无图标信息
menuUrl: 'https://google.com',
menuCode: 'xx'
},
{
menuName: 'baidu',
menuIcon: '', // Chrome 书签文件中并无图标信息
menuUrl: 'https://baidu.com',
menuCode: 'xx'
}
]
},
{
key:'2',
label:'类目B',
children:[
{
menuName: '网址名称',
menuIcon: '', // Chrome 书签文件中并无图标信息
menuUrl: 'https://a.com',
menuCode: ''
}
]
}
]
生成函数:
// 生成 Chrome 书签 HTML 格式
function generateBookmarkHTML(bookmarks) {
let html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file.
It will be read and overwritten.
DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>`;
bookmarks.forEach(group => {
html += `
<DT><H3>${group.label}</H3>
<DL><p>`;
group.children.forEach(item => {
html += `
<DT><A HREF="${item.menuUrl}" ADD_DATE="${Date.now()}">${item.menuName}</A>`;
});
html += `
</DL><p>`;
});
html += `
</DL><p>`;
return html;
}
// 导出为 HTML 文件
export function exportBookmarks(bookmarksData = []) {
const bookmarkHTML = generateBookmarkHTML(bookmarksData);
const blob = new Blob([bookmarkHTML], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'bookmarks.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
使用js解析 Chrome书签文件
处理带有属性PERSONAL_TOOLBAR_FOLDER="true"的H3标签进行忽略,判断书签栏的类目是网址还是单个网址,然后递归处理,生成想要的数据结构。
/**
* 解析书签 html 文件
* @param {*} file
* @returns
*/
function parseBookmarksHTML(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (event) {
try {
const htmlContent = event.target.result;
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
// 找到最外层 DL
const rootDL = doc.querySelector('DL');
if (!rootDL) {
throw new Error('未找到 <DL>,文件可能不是标准 Chrome 书签!');
}
// 解析获得所有文件夹,并顺带收集"无文件夹"链接
const { noFolderLinks, folderList } = parseRootDL(rootDL);
// 把"无文件夹"链接打包进一个文件夹结构
const result = [];
if (noFolderLinks.length > 0) {
result.push({
key: generateUUID(),
label: '未分类',
children: noFolderLinks,
});
}
// 把真正的文件夹们也放到最外层数组
result.push(...folderList);
resolve(result);
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error('文件读取失败'));
reader.readAsText(file);
});
}
/**
* 解析最外层 <DL>,把顶层没有文件夹的 <A> 放进 noFolderLinks,
* 顶层有文件夹的 <H3> + <DL> 放进 folderList
*
* @param {HTMLElement} dlElement
* @returns {{ noFolderLinks: Array, folderList: Array }}
*/
function parseRootDL(dlElement) {
const dtElements = dlElement.querySelectorAll(':scope > DT');
const noFolderLinks = [];
const folderList = [];
dtElements.forEach((dt) => {
const h3 = dt.querySelector(':scope > H3');
const aTag = dt.querySelector(':scope > A');
if (h3) {
// 如果 h3 带有 PERSONAL_TOOLBAR_FOLDER="true" 就跳过"自己",但要继续递归解析它的子 <DL>
if (h3.getAttribute('PERSONAL_TOOLBAR_FOLDER') === 'true') {
const childDL = dt.querySelector(':scope > DL');
if (childDL) {
// 子DL 里会返回一个数组,既可能含子文件夹({ key, label, children }) 也可能是书签({ menuName, ... })
const childItems = parseBookmarkDL(childDL);
// 我们需要把这些子items分拆到 noFolderLinks 或 folderList
// parseBookmarkDL 里返回的结构是:
// - 如果是文件夹: { key, label, children }
// - 如果是书签: { menuName, menuUrl, ... }
childItems.forEach((item) => {
if (item.menuUrl) {
// 这是个链接
noFolderLinks.push(item);
} else {
// 这是个文件夹
folderList.push(item);
}
});
}
// 然后跳过"自己"(不 push 任何东西)
return;
}
// --- 如果不是 PERSONAL_TOOLBAR_FOLDER="true",正常处理文件夹 ---
const folderName = h3.innerText.trim() || '未命名文件夹';
const childDL = dt.querySelector(':scope > DL');
folderList.push({
key: generateUUID(),
label: folderName,
children: childDL ? parseBookmarkDL(childDL) : [],
});
}
else if (aTag) {
// 顶层直接的链接
noFolderLinks.push({
menuName: aTag.innerText.trim() || '未命名书签',
menuIcon: '',
menuUrl: aTag.href || '',
menuCode: generateUUID(),
});
}
});
return { noFolderLinks, folderList };
}
/**
* 递归解析子文件夹/链接
* 这里和之前差不多,只是用于处理子文件夹
*
* @param {HTMLElement} dlElement
* @returns {Array} - 返回子层级的数组(文件夹、链接混合)
*/
function parseBookmarkDL(dlElement) {
const results = [];
const dtElements = dlElement.querySelectorAll(':scope > DT');
dtElements.forEach((dt) => {
const h3 = dt.querySelector(':scope > H3');
const aTag = dt.querySelector(':scope > A');
if (h3) {
// 如果是 "PERSONAL_TOOLBAR_FOLDER" 那就跳过"自己",把子DL 继续解析合并到当前 results
if (h3.getAttribute('PERSONAL_TOOLBAR_FOLDER') === 'true') {
const childDL = dt.querySelector(':scope > DL');
if (childDL) {
const childItems = parseBookmarkDL(childDL);
childItems.forEach((item) => results.push(item));
}
return;
}
// 正常子文件夹处理
const folderName = h3.innerText.trim() || '未命名文件夹';
const childDL = dt.querySelector(':scope > DL');
results.push({
key: generateUUID(),
label: folderName,
children: childDL ? parseBookmarkDL(childDL) : []
});
}
else if (aTag) {
// 书签
const linkName = aTag.innerText.trim() || '未命名书签';
const linkUrl = aTag.href || '';
const iconData = aTag.getAttribute('ICON') || ''; // 如果取不到,默认空字符串
results.push({
menuName: linkName,
menuIcon: iconData,
menuUrl: linkUrl,
menuCode: generateUUID()
});
}
});
return results;
}
/** 简易 UUID 生成 */
function generateUUID() {
return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 处理文件上传
async function handleFileUpload(event) {
return new Promise(async (resolve, reject) => {
try {
const file = event.target.files[0];
if (!file) {
throw new Error('请选择一个书签 HTML 文件');
}
// 检查文件类型
if (!file.name.toLowerCase().endsWith('.html')) {
throw new Error('请选择正确的 HTML 格式文件');
}
// 检查文件大小(例如限制为 10MB)
if (file.size > 10 * 1024 * 1024) {
throw new Error('文件大小超过限制(最大10MB)');
}
const bookmarks = await parseBookmarksHTML(file);
resolve(bookmarks);
} catch (error) {
reject(error);
}
});
}
// 创建文件上传按钮
export function importBookmarks(cb) {
const uploadInput = document.createElement('input');
uploadInput.type = 'file';
uploadInput.accept = '.html';
uploadInput.addEventListener('change', async (e) => {
try {
const data = await handleFileUpload(e);
if (cb) {
cb(data);
} else {
console.warn('未提供回调函数处理导入结果');
}
} catch (error) {
console.error('导入书签失败:', error.message);
// 这里可以添加UI提示,比如使用 alert 或其他提示组件
alert(error.message);
} finally {
uploadInput.remove();
}
});
uploadInput.click();
}
使用方法,直接调用importBookmarks函数,并传入回调函数即可。
总结
Chrome 浏览器和 Edge浏览器名称有所不同,Chrome书签叫书签栏,Edge浏览器叫收藏夹,区别在于导出的这一行:
- Chrome
<DT><H3 ADD_DATE="1700000000" LAST_MODIFIED="1700000000" PERSONAL_TOOLBAR_FOLDER="true">书签栏</H3>
- Edge
<DT><H3 ADD_DATE="1700000000" LAST_MODIFIED="1700000000" PERSONAL_TOOLBAR_FOLDER="true">收藏夹栏</H3>
上面的generateBookmarkHTML 函数并没有加入这一行,如果加入这行,当把书签文件导入到 Chrome时会在根目录多生成两个文件夹来嵌套生成的文件夹: 已导入->书签栏,在Edge浏览器中则导入正常,如果有已存在的文件夹则进行合并数据,并不会在书签根级多生成一个文件夹。