瀏覽器有個實(shí)用的功能,但是可能用的頻率不高,就是書簽/收藏的導(dǎo)入和導(dǎo)出,因?yàn)楝F(xiàn)在一般瀏覽器都有云同步功能,所以這個功能存在感不強(qiáng)。
瀏覽器書簽是可以跨不同的瀏覽器導(dǎo)入的,所以意味著導(dǎo)出的文件肯定是有一個規(guī)范的,我簡單搜了一下沒有搜到,可能是各家約定俗成的規(guī)范,并沒有一個正式的標(biāo)準(zhǔn)。
通用的數(shù)據(jù)交換格式有很多,比如xml、json、yaml,json應(yīng)該是使用最廣泛的,因?yàn)橐子诮馕龊痛鎯?,尺寸也不大,所以很適合瀏覽器書簽的導(dǎo)出,但是,實(shí)際上現(xiàn)代瀏覽器導(dǎo)出的書簽文件是html文件。原因不詳,也沒有搜到相關(guān)信息,我猜測原因可能是html文件相對于json來說,普通用戶更為熟悉,其次,html文件可以直接使用瀏覽器打開,當(dāng)然,json文件也可以使用瀏覽器打開,但是可能直接點(diǎn)擊的時候默認(rèn)是用文本編輯器打開的,另外它們在瀏覽器的呈現(xiàn)方式也不一樣,html顯示的是一個普通的帶有一堆超鏈接的頁面,就是一個有點(diǎn)丑的網(wǎng)頁,而json打開有點(diǎn)類似源碼,不太友好,因?yàn)橐话阌脩魧?dǎo)出書簽就是為了在另一個瀏覽器導(dǎo)入,所以屏蔽細(xì)節(jié)并沒有什么問題。
html和xml是類似的,所以解析和傳輸也很簡單,接下來看一下實(shí)例:
基本結(jié)構(gòu)如上,每個文件夾下都有個書簽,導(dǎo)出的書簽源碼如下:
簡單分析一下:
1.標(biāo)簽字母都是大寫
2.DOCTYPE聲明和普通HTML頁面不同
3.使用DL和DT來組織書簽,DL代表一個文件夾的內(nèi)容列表,DT代表一個內(nèi)容,可能是書簽也可能是文件夾,文件夾的話會有一個H3標(biāo)簽來表示書簽的名字,書簽的話就是直接跟一個A標(biāo)簽,DL標(biāo)簽后都跟了一個小寫的p標(biāo)簽,有部分標(biāo)簽沒有閉合
4.H1標(biāo)簽之前的都和書簽內(nèi)容沒有什么關(guān)系
5.文件夾名稱H3標(biāo)簽和超鏈接A標(biāo)簽都有ADD_DATE和LAST_MODIFIED來保存時間信息,該屬性不存在也不影響
6.文件夾名稱H3標(biāo)簽的屬性PERSONAL_TOOLBAR_FOLDER來表示該文件夾下的內(nèi)容是否顯示到瀏覽器的工具欄,否則會默認(rèn)放到瀏覽器的其他文件夾里,但也不一定,有的瀏覽器會有自己的行為
7.網(wǎng)頁的標(biāo)題icon會轉(zhuǎn)換為base64格式放到ICON屬性上,這個屬性不存在也不影響
html其實(shí)就是普通字符串,所以可以手動生成,常見于一些導(dǎo)航網(wǎng)站和網(wǎng)址收藏工具的導(dǎo)出功能,如五花八門導(dǎo)航(http://lxqnsys.com/d),有一個需要注意的地方,就是html字符串必須格式化帶換行和縮進(jìn),下圖這種壓縮過的是不行的:
生成方式也很簡單,書簽是樹結(jié)構(gòu),所以遞歸循環(huán)拼接即可。
先看一下書簽數(shù)據(jù)的格式,忽略時間和icon:
let bookmarks = [
{
name: '',// 文件夾或書簽名字
toolbar: true,// 是否顯示到工具欄
folder: true,// 是否是文件夾
children: [
{
name: '',
folder: true,
children: []
},
{
name: '',// 書簽名稱
url: ''// 書簽url
}
]
}
]
使用ES6的話可以直接使用模板字符串``來帶換行的拼接,很方便:
function createBookmarksStr (bookmarks) {
let str = `
<!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>
`
let loop = (root) => {
let str = ''
root.forEach((item) => {
if (item.folder) {
str += `
<DT>
<H3 ${item.toolbar ? `PERSONAL_TOOLBAR_FOLDER="true"` : ''}>${item.name}</H3>
<DL>
<p>
`
str += loop(item.children)
str += `
</DL>
<p>
`
} else {
str += `
<DT><A HREF="${item.url}">${item.name}</A>
`
}
})
return str
}
str += loop(bookmarks)
str += `
</DL>
<p>
`
return str
}
ES6之前的就需要顯式的拼接上換行符:
function createBookmarksStr (bookmarks) {
var str = '<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is an automatically generated file.It will be read and overwritten.DO NOT EDIT! -->\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL>\n\t<p>\n\t\t<DT>\n\t\t\t<H3 ADD_DATE=\"1568796074\" LAST_MODIFIED=\"1601707819\" PERSONAL_TOOLBAR_FOLDER=\"true\">\u4E66\u7B7E\u680F</H3>\n\t\t\t<DL>\n\t\t\t\t<p>\n\t\t\t\t\t'
var loop = function (root) {
var str = ''
root.forEach(function (item) {
if (item.folder) {
str += '<DT>\n\t\t\t\t\t\t<H3 '+ (item.toolbar ? 'PERSONAL_TOOLBAR_FOLDER="true"' : '') +'>'+item.name+'</H3>\n\t\t\t\t\t\t<DL>\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t\t'
str += loop(item.children)
str += '</DL>\n\t\t\t\t\t\t<p>\n\t\t\t'
} else {
str += '<DT><A HREF=\"'+item.url+'\">'+item.name+'</A>\n\t\t\t\t\t\t\t\t'
}
})
return str
}
str += loop(bookmarks)
str += '</DL>\n\t\t\t<p>\n</DL>\n<p>'
return str
}
看完了如何生成,接下來看一下如何解析,解析和拼接類似,也是通過深度優(yōu)先進(jìn)行遍歷,只是會有一些特征判斷。字符串如何轉(zhuǎn)化成一棵樹,最簡單的肯定是先轉(zhuǎn)換為DOM元素,然后再通過操作DOM的api來進(jìn)行遍歷,有一些庫可以用來做這件事,不過這里直接用的是iframe:
function getBookmarksStrRootNode (str) {
// 創(chuàng)建iframe
let iframe = document.createElement('iframe')
document.body.appendChild(iframe)
iframe.style.display = 'none'
// 添加書簽dom字符串
iframe.contentWindow.document.documentElement.innerHTML = str
// 獲取書簽樹根節(jié)點(diǎn)
return iframe.contentWindow.document.querySelector('dl')
}
function analysisBookmarksStr(str) {
let root = getBookmarksStrRootNode(str)
}
看一下轉(zhuǎn)換的結(jié)果:
書簽DOM字符串:
轉(zhuǎn)換后的DOM節(jié)點(diǎn):
獲取到書簽樹的根節(jié)點(diǎn),接下來遞歸遍歷即可:
function walkBookmarksTree (root) {
let result = []
// 深度優(yōu)先遍歷
let walk = (node, list) => {
let els = node.children
if (els && els.length > 0) {
for (let i = 0; i < els.length; i++) {
let item = els[i]
// p標(biāo)簽或h3標(biāo)簽直接跳過
if (item.tagName === 'P' || item.tagName === 'H3') {
continue
}
// 文件夾不用創(chuàng)建元素
if (item.tagName === 'DL') {
walk(els[i], list)
} else {// DT節(jié)點(diǎn)
let child = null
// 判斷是否是文件夾
let children = item.children
let isDir = false
for(let j = 0; j < children.length; j++) {
if (children[j].tagName === 'H3' || children[j].tagName === 'DL') {
isDir = true
}
}
// 文件夾
if (isDir) {
child = {
name: item.tagName === 'DT' ? item.querySelector('h3') ? item.querySelector('h3').innerText : '' : '',
folder: true,
children: []
}
walk(els[i], child.children)
} else {// 書簽
let _item = item.querySelector('a')
child = {
name: _item.innerText,
url: _item.href
}
}
list.push(child)
}
}
}
}
walk(root, result)
return result
}
function analysisBookmarksStr(str) {
let root = getBookmarksStrRootNode(str)
let result = walkBookmarksTree(root)
}
最后解析的結(jié)果:
搞定收工,趕緊去試試吧。