简单记录下我如何给 Olinkics 添加导出书签功能。
生成文件内容
关于导出书签,第一个问题是要把书签以什么样的形式导出。
带着这个问题,我挨个尝试了几个浏览器以及书签管理应用的导出功能,发现它们基本上都提供一个导出为 .html
文件的功能。打开它们导出的文件,我发现它们基本上都长这样:
html
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file. Do not edit! -->
<HTML>
<HEAD>
<TITLE>Bookmarks</TITLE>
</HEAD>
<BODY>
<H1>Bookmarks</H1>
<DL><p>
<DT><A HREF="http://www.example.com" ADD_DATE="1624936262" LAST_MODIFIED="1624936262">Example Website</A>
<DT><A HREF="http://www.anotherexample.com" ADD_DATE="1624936300" LAST_MODIFIED="1624936300">Another Example</A>
<DT><H3>Folder</H3>
<DL><p>
<DT><A HREF="http://www.folderlink.com" ADD_DATE="1624936350">Folder Link</A>
</DL><p>
</DL><p>
</BODY>
</HTML>
这是一个称之为 NETSCAPE-Bookmark-file-1
的文件格式,主要用来导入和导出用户的浏览器书签。它最早由 Netscape 浏览器使用 (从名字里也可以看出来),而其他浏览器也基本支持这种格式。
<A HREF="URL">
用于表示一个网页书签,HREF
是书签链接的地址。ADD_DATE
和LAST_MODIFIED
是书签的时间戳,表示该书签何时添加和最后一次修改。
那我只要从数据库里拿到书签的数据,然后转换成这种形式就成,代码如下:
ts
type ExportedBookmark = {
url: string;
title: string | null;
tags: string[] | null;
note: string | null;
created_at: string;
updated_at: string;
};
export function generateNetscapeBookmarkFile(bookmarks: ExportedBookmark[]) {
let bookmarkFileContent = `
<!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file. Do not edit! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1>
<DL><p>
`;
bookmarks.forEach((bookmark) => {
const add = Math.floor(new Date(bookmark.created_at).getTime() / 1000);
const update = Math.floor(new Date(bookmark.updated_at).getTime() / 1000);
const tags = bookmark.tags?.join(",");
bookmarkFileContent += `
<DT><A HREF="${bookmark.url}" ADD_DATE="${add}" LAST_MODIFIED="${update}" TAGS="${tags}">${bookmark.title}</A>
`;
});
bookmarkFileContent += "</DL><p>";
return bookmarkFileContent;
}
下载文件
解决了文件内容的问题,下一个问题是:如何把这些内容写入一个文件,然后下载到用户的计算机里。
我问了下 ChatGPT,它给了我下面的代码:
ts
export function download(content: string, filename: string) {
const blob = new Blob([content], { type: "text/html" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
这里有三个关键点,一是 Blob
对象,二是 URL.createObjectURL()
方法,三是 <a>
标签。
Blob
对象,我们可以用它来存储字符串或二进制数据。
- 它的一个关键特性是,一般是做为一个原始的数据容器存在,通过链接或其他方式来访问。
URL.createObjectURL()
方法:
- 通过这个方法,浏览器可以将一个
Blob
对象转换成一个 URL。但要注意,这个 URL 是临时的,只在当前页面的生命周期内有效。 - 举个例子,
URL.createObjectURL(blob)
会生成一个类似于blob:http://example.com/abc123
这样的 URL,它实际上指向了内存中保存的Blob
对象。这意味着浏览器可以通过这个 URL 加载Blob
内容,就像访问普通的文件 URL 一样。
关于 <a>
标签,我们通常用它来定义超链接。通常来说,当我们点击一个超链接时,浏览器会打开这个超链接指向的网页或文件。然而, <a>
标签还有一个 download
属性,它告诉浏览器,当用户点击该链接,应该下载链接指向的资源,而不是在浏览器中打开它。
a.href = URL.createObjectURL(blob);
- 这行代码为<a>
标签设置了一个指向Blob
对象的 URL。a.download = filename;
- 这行代码为该链接指定了下载的文件名。a.click();
则模拟了用户点击这个下载链接的行为,从而触发文件的下载。
当执行 a.click()
时,浏览器会:
- 读取
href
指定的 URL。由于 URL 指向的是一个Blob
对象,浏览器会读取这个对象里的内容,也就是我们一开始传入的content
。 - 由于指定了
download
属性,所以浏览器会根据该属性指定的文件名下载文件,而不是打开它。
FIN
OK,到此难点全部搞定,可以开始写 UI 了。这里简单演示一下。
tsx
async fetchBookmarks() {
// ...
}
export function ExportButton() {
const handleExport = async () => {
const data = await fetchBookmarks();
const text = generateNetscapeBookmarkFile(data);
download(text, "bookmarks.html");
};
return <button onClick={handleExport}>Export</button>;
}