JS如何实现书签导入导出?我是这么做的
创始人
2024-03-21 12:31:43
0

目录

前言

依赖

概览

功能实现

FileSystem:

HTMLSystem:

html-config:

写在最后


前言

使用Node做过爬虫的人应该都知道Cheerio.js模块,其快速灵活的机制,使我们只需要了解JQ就可以轻松上手,是在使用node抓取网页数据的过程中不可或缺的一员。

了解了cheerio后,我突发奇想:干脆拿cheerio实现个书签的导入吧,正好可以熟悉一下它的用法,于是早些时候我使用cheerio+node实现了初版的书签导入功能,将浏览器导出的书签通过前端页面上传到服务端,服务端使用cheerio将html解析成JSON文件,通过接口将数据传递到前端。

然而,当时我并不满意,因为就为了一个接口开了一个node服务,是不是有点大材小用了,我能否靠本地缓存实现一个纯前端的书签预览和导入导出功能?

说干就干,导入书签我借助前端的FileReader类,读取HTML文件,然后再使用cheerio将Dom解析成JSON格式的数据,在前端展示成menu形式;导出书签同样使用cheerio根据JSON数据生成对应的Dom数据,通过URL.createObjectURL新建文件的本地url地址,最后使用a标签下载文件

下面我分享一下完整的实现过程及源码

依赖

  • utils-lib-js模块

  • cheerio模块

  • vite:3.1

  • vue:3.2

  • element-plus:2.0

概览

这个小案例是基于vite搭建的一个vue-3.0的项目,除了layout之外,案例的核心部分是两个类:
FileSystem和HTMLSystem,前者提供下载,文件读取的功能,后者实现了JSON和HTML互转的功能,除此之外其他的都是常见的布局及组件,所以文章重点描述这两大块

功能实现

FileSystem:

  • 读取文件功能,从element-ui的el-upload组件获取到数据后将结果转换成string格式
  • 下载文件功能,给定url下载静态资源
  • 本地文件转静态地址
import type { UploadFile } from "element-plus/es/components/upload/src/upload.type";
import { defer } from "utils-lib-js";
export type readFileType = 'readAsArrayBuffer' | 'readAsBinaryString' | 'readAsDataURL' | 'readAsText'
export declare interface IFileSystem {readFile: (file: UploadFile, type?: readFileType, encoding?: string) => Promise>downloadFile: (url: string, name?: string) => voidstringToBlobURL: (fileString: string) => string
}
export class FileSystem implements IFileSystem {/*** @name: * @description: 读取前端上传的文件* @param {UploadFile} file 文件* @param {readFileType} type 文件类型* @param {string} encoding 解码方式* @return {Promise>}*/readFile(file: UploadFile, type: readFileType = 'readAsText', encoding: string = 'utf-8') {const { promise, resolve, reject } = defer()const reader: FileReader = new FileReader();reader[type](file.raw, encoding)reader.onload = resolvereader.onerror = rejectreturn >promise}/*** @name: * @description: 下载文件* @param {string} url 资源目录/网址* @param {string} name 下载文件名* @return {*}*/downloadFile(url: string, name: string = 'file.txt') {const link = document.createElement('a')link.href = urllink.download = nameconst _evt = new MouseEvent('click')link.dispatchEvent(_evt)}/*** @name: * @description: 字符串转本地文件* @param {string} fileString 文件内容* @return {*}*/stringToBlobURL(fileString: string) {return URL.createObjectURL(new Blob([fileString], { type: "application/octet-stream" }))}
}

HTMLSystem:

  • HTML转JSON函数,解析dom树,生成JSON数据
  • JSON转HTML函数,通过标准格式生成书签格式的HTML标签

import { load, Cheerio, CheerioAPI, CheerioOptions } from 'cheerio'
import {createHtmlFolder,createHtmlFile,createBaseTemp
} from '@/config'
import { File, Folder } from "@/layout/menu/types";
export declare interface IHTMLSystem, I = CheerioAPI, FolderList = Array> {count: numberresetCount: () => voidinitHTML: (html: string) => FolderListhtmlToJson: (node: T, bookMarks: FolderList) => voidaddToBookMarks: (node: T, list: FolderList) => unknowngetNodeTitle: (node: T) => voidgetNodeInfo: (node: T, info: File) => FilecreateInitHtml: (temp: string, opt?: CheerioOptions, isDoc?: boolean) => IinitJSON: (json: FolderList) => stringjsonToHtml: (bookMarks: FolderList, node: I) => stringcreateFolder: (folder: Folder, node: T) => IcreateFile: (file: File, node: T) => IcreateElemChild: (node: T) => (it: F, i: number) => voidcheckIsFileOrFolder: (item: F) => 'folder' | 'file' | 'none'
}
export class HTMLSystem implements IHTMLSystem {count = 0/*** @name: * @description: 重置id* @return {*}*/resetCount = () => {this.count = 0;};/*** @name: * @description: 递增id* @return {*}*/addCount = () => {return this.count++};/*** @name: * @description: 初始化html生成器* @param {string} html 预加载的html字符文件* @return {Array}*/initHTML(html: string) {const $ = load(html);const dl = $("dl").first();const dt = dl.children("dt").eq(0);return this.htmlToJson(dt, []);}/*** @name: * @description: html转Json的递归函数* @param {Cheerio} node 根节点* @param {Array} bookMarks JSON数据源* @return {Array}*/htmlToJson = (node: Cheerio, bookMarks: Array = []) => {//下一级文件夹目录列表const childrenNodeDL = node.children("dl");const childrenNodeDT = childrenNodeDL.children("dt");const { item: dir, dirType } = this.addToBookMarks(node, bookMarks)childrenNodeDT.map((i) => {const it = childrenNodeDT.eq(i)dirType === 'file' && this.addToBookMarks(it, dir.children)this.htmlToJson(it, dir.children);});return bookMarks;};/*** @name: * @description: 将单个数据添加到JSON中* @param {Cheerio} node 父节点* @param {Array} list 书签JSON数据* @return {, Array, 'folder'|'file'} */addToBookMarks = (node: Cheerio, list: Array = []) => {const item = this.getNodeTitle(node);const dirType = this.checkIsFileOrFolder(item)switch (dirType) {case "folder":item.children = [];case "file":item.id = this.addCount().toString()list.push(item)break;}return { item, list, dirType }}/*** @name: * @description: 判断单个数据是否是文件夹,并解析详细信息* @param {Cheerio} node 文件或文件夹所在的节点* @return {*}*/getNodeTitle = (node: Cheerio) => {const info: any = {};const title = node.children("h3");// 如果h3的length为0则不是文件夹,就获取网站名称和网址,否则是文件夹并赋值title, add_date,last_modifiedreturn title.length === 0 ? this.getNodeInfo(node, info) : {...info,title: title.text(),add_date: title.attr("add_date"),last_modified: title.attr("last_modified")};};/*** @name: * @description: 解析书签文件详细信息* @param {Cheerio} node 文件所在的节点* @return {File}*/getNodeInfo = (node: Cheerio, info: File) => ({...info,name: node.children("a").text(),href: node.children("a").attr("href") ?? '',icon: node.children("a").attr("icon") ?? '',add_date: node.children("a").attr("add_date")})/*** @name: * @description: 入口函数* @param {Array} json 上面生成的书签JSON文件* @return {string}*/initJSON(json: Array) {return this.jsonToHtml(json);}/*** @name: * @description: 生成新标签的CheerioAPI* @param {string} temp 标签* @param {*} opt Cheerio 配置项* @param {*} isDoc 是否生成完整的html标签* @return {CheerioAPI}*/createInitHtml = (temp: string, opt = { xml: true, xmlMode: true }, isDoc = false) => {const $ = load(temp, opt, isDoc);return $}/*** @name: * @description: JSON转书签的主函数* @param {Array} bookMarks 书签的JSON数据* @return {string}*/jsonToHtml = (bookMarks: Array = []) => {const root = this.createInitHtml(`
${createBaseTemp()}
`)("#root")bookMarks.forEach(this.createElemChild(root.children().first()))return root.children().toString()}/*** @name: * @description: 递归生成Dom树* @param {Cheerio} node 父节点* @return {void}*/createElemChild = (node: Cheerio) => (it: Folder | File) => {const type = this.checkIsFileOrFolder(it)switch (type) {case 'folder':const folder = this.createFolder(it as Folder)node.append(folder("*"))//每次都会获取最后一个标签,将子项放进去,防止标签重复遍历it.children.forEach(this.createElemChild(node.children("DL").last()))breakcase 'file':const file = this.createFile(it as File)node.append(file('*'))breakcase 'none':throw new Error('Item is not Folder or File')}}/*** @name: * @description: 生成文件夹标签* @param {Folder} folder 文件夹格式的单个数据* @return {CheerioAPI}*/createFolder = (folder: Folder) => {const init = this.createInitHtml(createHtmlFolder(folder))return init}/*** @name: * @description: 生成文件标签* @param {File} file 文件格式的单个数据* @return {CheerioAPI}*/createFile = (file: File) => {const init = this.createInitHtml(createHtmlFile(file))return init}/*** @name: * @description: 判断是文件还是文件夹格式的数据* @param {Folder} item 单个数据* @return {*}*/checkIsFileOrFolder = (item: Folder | File) => item.title ? 'folder' : item.name ? 'file' : 'none'}

html-config:

此外,生成HTML时,需要一些模板函数


import { File, Folder } from "@/layout/menu/types";
/*** @name: * @description: 书签默认模板* @param {string} 书签名* @return {*}*/
export const createHtmlTemp = (name: string) => `


${name}

${name}

` /*** @name: * @description: 生成文件夹格式的Dom* @param {Folder} folder 文件夹格式数据* @return {*}*/ export const createHtmlFolder = (folder: Folder) => `

${folder.title}

${createBaseTemp()} ` /*** @name: * @description: 生成文件格式的Dom* @param {File} file 文件格式数据* @return {*}*/ export const createHtmlFile = (file: File) => `
${file.name} ` /*** @name: * @description: 列表格式的Dom* @return {*}*/ export const createBaseTemp = () => `

`

写在最后

最终实现效果:BookMarks

源码:book_mark: 纯前端导入导出html书签,生成书签导航

最后,感谢你看到这里,如果文章有帮助到你,还请支持一下博主!

相关内容

热门资讯

实干笃行多措并举奋力夺取“开门... 转自:沈阳日报  铆足干劲开新局,奋楫争先创佳绩。今年一季度,市自然资源局紧扣服务经济社会高质量发展...
澳洲公会:内地小微企业2024... 本报记者 杜丽娟 北京报道小微企业作为国民经济的组成部分,在促进就业、驱动经济增长、推动科技创新及维...
今年开展职业技能培训和岗位练兵... 转自:辽宁日报 本报讯 记者徐铁英报道 “技能照亮前程·匠才助推振兴”2025年度专项培训行动目前正...
用中国创新赋能全球市场 转自:中国经营网张硕在前不久刚闭幕的2025上海车展期间,“中国汽车技术创新赋能全球发展”成为热议话...
新青两地艺术家青海湖畔联袂献艺 5月9日,新疆民间艺术季优秀节目巡演青海站活动走进青海湖二郎剑景区。 本报讯(西海新闻记者 吴梦婷 ...
“民法典宣传月·送法进企业宣传... 转自:辽宁日报 本报讯 记者黄岩报道 今年5月是第五个“民法典宣传月”,连日来,全省各地各部门积极投...
行走百个社区 叩开万户家门 转自:沈阳日报  “以前咨询涉及残疾人的法律法规时无从下手,现在法律服务直接‘送上门’!”坐在轮椅上...
美国财长贝森特:避免债务触及上...   美国财政部长贝森特致信国会议员,称财政部动用特别会计手段以避免突破联邦债务上限的能力,最早可能在...
“人设”:视觉时代创作新手法 转自:光明网  【面面观·“人设”与新媒介文艺创作】  作者:李皓颖(北京大学中文系博士研究生)  ...
HAI乐园超级运动公园项目正式... 转自:沈阳日报  本报讯(沈阳日报、沈报全媒体记者张晶)5月9日,以运动为主题特色,搭配餐饮休闲、二...
中国品牌汽车亮相2025马来西... 这是5月8日在马来西亚雪兰莪州沙登拍摄的2025马来西亚车展媒体预览会现场。 2025年马来西...
引秋生手里 藏月入怀中——评《... 转自:光明网  作者:袁 坤(浙江省博物馆馆员)  《缮扇——传统纸扇装裱修复及保护》一书,近来由岭...
国办印发《关于进一步加强 困境... 新华社北京5月9日电  日前,国务院办公厅印发《关于进一步加强困境儿童福利保障工作的意见》(以下简称...
西宁市城中区 开展杨柳絮清理行... 本报讯(西海新闻记者 燕卓)每年5月,杨柳絮随风漫天飞舞,给市民的生活带来烦恼。为最大限度减少飘絮问...
说说泰山封禅大典那些事儿   “五一”期间,由泰安文旅集团打造的《中华泰山·封禅大典》2025升级改造版迎来首演。作为泰山封禅...
为中俄关系发展注入新的动能   新华社北京5月9日电 应俄罗斯联邦总统普京邀请,国家主席习近平于5月7日至10日对俄罗斯进行国事...
成功研制牦牛心肝肺营养肽产品 本报海北讯 (记者 王晶) 5月8日,记者从海北藏族自治州刚察县委宣传部获悉,刚察县与相关单位合作,...
我省重点交通项目捷报频传 积石峡黄河特大桥主拱肋合龙。 同赛项目西卜沙隆务河特大桥施工现场。 西宁曹家堡机场东进场路施工...
国际巨头,确认大裁员! 5月9日,松下集团宣布,计划于2025年度至2026年度在全球范围内裁员1万人,其及5000名日本员...
深入贯彻中央八项规定精神学习教... 本报讯(记者 莫昌伟)按照中央党的建设工作领导小组统一部署,5月9日,深入贯彻中央八项规定精神学习教...