• JS如何实现书签导入导出?我是这么做的


    目录

    前言

    依赖

    概览

    功能实现

    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之外,案例的核心部分是两个类:
    FileSystemHTMLSystem,前者提供下载,文件读取的功能,后者实现了JSON和HTML互转的功能,除此之外其他的都是常见的布局及组件,所以文章重点描述这两大块

    功能实现

    FileSystem:

    • 读取文件功能,从element-ui的el-upload组件获取到数据后将结果转换成string格式
    • 下载文件功能,给定url下载静态资源
    • 本地文件转静态地址
    1. import type { UploadFile } from "element-plus/es/components/upload/src/upload.type";
    2. import { defer } from "utils-lib-js";
    3. export type readFileType = 'readAsArrayBuffer' | 'readAsBinaryString' | 'readAsDataURL' | 'readAsText'
    4. export declare interface IFileSystem {
    5. readFile: (file: UploadFile, type?: readFileType, encoding?: string) => Promise<ProgressEvent<FileReader>>
    6. downloadFile: (url: string, name?: string) => void
    7. stringToBlobURL: (fileString: string) => string
    8. }
    9. export class FileSystem implements IFileSystem {
    10. /**
    11. * @name:
    12. * @description: 读取前端上传的文件
    13. * @param {UploadFile} file 文件
    14. * @param {readFileType} type 文件类型
    15. * @param {string} encoding 解码方式
    16. * @return {Promise>}
    17. */
    18. readFile(file: UploadFile, type: readFileType = 'readAsText', encoding: string = 'utf-8') {
    19. const { promise, resolve, reject } = defer()
    20. const reader: FileReader = new FileReader();
    21. reader[type](file.raw, encoding)
    22. reader.onload = resolve
    23. reader.onerror = reject
    24. return <Promise<any>>promise
    25. }
    26. /**
    27. * @name:
    28. * @description: 下载文件
    29. * @param {string} url 资源目录/网址
    30. * @param {string} name 下载文件名
    31. * @return {*}
    32. */
    33. downloadFile(url: string, name: string = 'file.txt') {
    34. const link = document.createElement('a')
    35. link.href = url
    36. link.download = name
    37. const _evt = new MouseEvent('click')
    38. link.dispatchEvent(_evt)
    39. }
    40. /**
    41. * @name:
    42. * @description: 字符串转本地文件
    43. * @param {string} fileString 文件内容
    44. * @return {*}
    45. */
    46. stringToBlobURL(fileString: string) {
    47. return URL.createObjectURL(new Blob([fileString], { type: "application/octet-stream" }))
    48. }
    49. }

    HTMLSystem:

    • HTML转JSON函数,解析dom树,生成JSON数据
    • JSON转HTML函数,通过标准格式生成书签格式的HTML标签
    1. import { load, Cheerio, CheerioAPI, CheerioOptions } from 'cheerio'
    2. import {
    3. createHtmlFolder,
    4. createHtmlFile,
    5. createBaseTemp
    6. } from '@/config'
    7. import { File, Folder } from "@/layout/menu/types";
    8. export declare interface IHTMLSystemFolder | File, T = Cheerio<any>, I = CheerioAPI, FolderList = Array> {
    9. count: number
    10. resetCount: () => void
    11. initHTML: (html: string) => FolderList
    12. htmlToJson: (node: T, bookMarks: FolderList) => void
    13. addToBookMarks: (node: T, list: FolderList) => unknown
    14. getNodeTitle: (node: T) => void
    15. getNodeInfo: (node: T, info: File) => File
    16. createInitHtml: (temp: string, opt?: CheerioOptions, isDoc?: boolean) => I
    17. initJSON: (json: FolderList) => string
    18. jsonToHtml: (bookMarks: FolderList, node: I) => string
    19. createFolder: (folder: Folder, node: T) => I
    20. createFile: (file: File, node: T) => I
    21. createElemChild: (node: T) => (it: F, i: number) => void
    22. checkIsFileOrFolder: (item: F) => 'folder' | 'file' | 'none'
    23. }
    24. export class HTMLSystem implements IHTMLSystem {
    25. count = 0
    26. /**
    27. * @name:
    28. * @description: 重置id
    29. * @return {*}
    30. */
    31. resetCount = () => {
    32. this.count = 0;
    33. };
    34. /**
    35. * @name:
    36. * @description: 递增id
    37. * @return {*}
    38. */
    39. addCount = () => {
    40. return this.count++
    41. };
    42. /**
    43. * @name:
    44. * @description: 初始化html生成器
    45. * @param {string} html 预加载的html字符文件
    46. * @return {Array}
    47. */
    48. initHTML(html: string) {
    49. const $ = load(html);
    50. const dl = $("dl").first();
    51. const dt = dl.children("dt").eq(0);
    52. return this.htmlToJson(dt, []);
    53. }
    54. /**
    55. * @name:
    56. * @description: html转Json的递归函数
    57. * @param {Cheerio} node 根节点
    58. * @param {Array} bookMarks JSON数据源
    59. * @return {Array}
    60. */
    61. htmlToJson = (node: Cheerio<any>, bookMarks: Array = []) => {
    62. //下一级文件夹目录列表
    63. const childrenNodeDL = node.children("dl");
    64. const childrenNodeDT = childrenNodeDL.children("dt");
    65. const { item: dir, dirType } = this.addToBookMarks(node, bookMarks)
    66. childrenNodeDT.map((i) => {
    67. const it = childrenNodeDT.eq(i)
    68. dirType === 'file' && this.addToBookMarks(it, dir.children)
    69. this.htmlToJson(it, dir.children);
    70. });
    71. return bookMarks;
    72. };
    73. /**
    74. * @name:
    75. * @description: 将单个数据添加到JSON中
    76. * @param {Cheerio} node 父节点
    77. * @param {Array} list 书签JSON数据
    78. * @return {, Array, 'folder'|'file'}
    79. */
    80. addToBookMarks = (node: Cheerio<any>, list: Array = []) => {
    81. const item = this.getNodeTitle(node);
    82. const dirType = this.checkIsFileOrFolder(item)
    83. switch (dirType) {
    84. case "folder":
    85. item.children = [];
    86. case "file":
    87. item.id = this.addCount().toString()
    88. list.push(item)
    89. break;
    90. }
    91. return { item, list, dirType }
    92. }
    93. /**
    94. * @name:
    95. * @description: 判断单个数据是否是文件夹,并解析详细信息
    96. * @param {Cheerio} node 文件或文件夹所在的节点
    97. * @return {*}
    98. */
    99. getNodeTitle = (node: Cheerio<any>) => {
    100. const info: any = {};
    101. const title = node.children("h3");
    102. // 如果h3的length为0则不是文件夹,就获取网站名称和网址,否则是文件夹并赋值title, add_date,last_modified
    103. return title.length === 0 ? this.getNodeInfo(node, info) : {
    104. ...info,
    105. title: title.text(),
    106. add_date: title.attr("add_date"),
    107. last_modified: title.attr("last_modified")
    108. };
    109. };
    110. /**
    111. * @name:
    112. * @description: 解析书签文件详细信息
    113. * @param {Cheerio} node 文件所在的节点
    114. * @return {File}
    115. */
    116. getNodeInfo = (node: Cheerio<any>, info: File) => ({
    117. ...info,
    118. name: node.children("a").text(),
    119. href: node.children("a").attr("href") ?? '',
    120. icon: node.children("a").attr("icon") ?? '',
    121. add_date: node.children("a").attr("add_date")
    122. })
    123. /**
    124. * @name:
    125. * @description: 入口函数
    126. * @param {Array} json 上面生成的书签JSON文件
    127. * @return {string}
    128. */
    129. initJSON(json: Array) {
    130. return this.jsonToHtml(json);
    131. }
    132. /**
    133. * @name:
    134. * @description: 生成新标签的CheerioAPI
    135. * @param {string} temp 标签
    136. * @param {*} opt Cheerio 配置项
    137. * @param {*} isDoc 是否生成完整的html标签
    138. * @return {CheerioAPI}
    139. */
    140. createInitHtml = (temp: string, opt = { xml: true, xmlMode: true }, isDoc = false) => {
    141. const $ = load(temp, opt, isDoc);
    142. return $
    143. }
    144. /**
    145. * @name:
    146. * @description: JSON转书签的主函数
    147. * @param {Array} bookMarks 书签的JSON数据
    148. * @return {string}
    149. */
    150. jsonToHtml = (bookMarks: Array = []) => {
    151. const root = this.createInitHtml(`
      ${createBaseTemp()}
      `
      )("#root")
    152. bookMarks.forEach(this.createElemChild(root.children().first()))
    153. return root.children().toString()
    154. }
    155. /**
    156. * @name:
    157. * @description: 递归生成Dom树
    158. * @param {Cheerio} node 父节点
    159. * @return {void}
    160. */
    161. createElemChild = (node: Cheerio<any>) => (it: Folder | File) => {
    162. const type = this.checkIsFileOrFolder(it)
    163. switch (type) {
    164. case 'folder':
    165. const folder = this.createFolder(it as Folder)
    166. node.append(folder("*"))
    167. //每次都会获取最后一个标签,将子项放进去,防止标签重复遍历
    168. it.children.forEach(this.createElemChild(node.children("DL").last()))
    169. break
    170. case 'file':
    171. const file = this.createFile(it as File)
    172. node.append(file('*'))
    173. break
    174. case 'none':
    175. throw new Error('Item is not Folder or File')
    176. }
    177. }
    178. /**
    179. * @name:
    180. * @description: 生成文件夹标签
    181. * @param {Folder} folder 文件夹格式的单个数据
    182. * @return {CheerioAPI}
    183. */
    184. createFolder = (folder: Folder) => {
    185. const init = this.createInitHtml(createHtmlFolder(folder))
    186. return init
    187. }
    188. /**
    189. * @name:
    190. * @description: 生成文件标签
    191. * @param {File} file 文件格式的单个数据
    192. * @return {CheerioAPI}
    193. */
    194. createFile = (file: File) => {
    195. const init = this.createInitHtml(createHtmlFile(file))
    196. return init
    197. }
    198. /**
    199. * @name:
    200. * @description: 判断是文件还是文件夹格式的数据
    201. * @param {Folder} item 单个数据
    202. * @return {*}
    203. */
    204. checkIsFileOrFolder = (item: Folder | File) => item.title ? 'folder' : item.name ? 'file' : 'none'
    205. }

    html-config:

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

    1. import { File, Folder } from "@/layout/menu/types";
    2. /**
    3. * @name:
    4. * @description: 书签默认模板
    5. * @param {string} 书签名
    6. * @return {*}
    7. */
    8. export const createHtmlTemp = (name: string) => `
    9. <span class="hljs-subst">${name}</span>
    10. ${name}

    11. `
    12. /**
    13. * @name:
    14. * @description: 生成文件夹格式的Dom
    15. * @param {Folder} folder 文件夹格式数据
    16. * @return {*}
    17. */
    18. export const createHtmlFolder = (folder: Folder) => `
    19. ${folder.add_date}" LAST_MODIFIED="${folder.last_modified}">${folder.title}

    20. ${createBaseTemp()}
    21. `
    22. /**
    23. * @name:
    24. * @description: 生成文件格式的Dom
    25. * @param {File} file 文件格式数据
    26. * @return {*}
    27. */
    28. export const createHtmlFile = (file: File) => `
    29. `
    30. /**
    31. * @name:
    32. * @description: 列表格式的Dom
    33. * @return {*}
    34. */
    35. export const createBaseTemp = () => `
    36. `

    写在最后

    最终实现效果:BookMarks

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

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

  • 相关阅读:
    docker 安装 RabbitMQ & 安装 rabbitmq_delayed_message_exchange插件
    世界儿童日,周大福真诚关爱儿童成长
    Android高版本读取沙盒目录apk解析安装失败解决方案
    国考省考申论:归纳概括多个主体身上的优秀品质,透过动词现象(怎么做的),找到名词(精神品质)本质
    JavaScript之无题之让人烦躁的模块化
    什么是PUP?什么是PUP程序?
    汇编语言中的标志位:CF、PF、AF、ZF、SF、TF、IF、DF、OF
    【干货】浅谈如何给.net程序加多层壳达到1+1>2的效果
    智慧党建小程序源码系统+在线考试+缴费+学习 功能强大 带完整的前后端搭建教程
    Docker 安装运行命令解读安装Mysql
  • 原文地址:https://blog.csdn.net/time_____/article/details/128190825