【前端】使用新API实现一个在线Markdown编辑器

背景

最近在网上偶然看到一种在线编辑器,实现了用户的数据在本地,编辑器是一个在线web应用。这样就可以避免了用户写作文章需要上传第三方服务器带来的数据丢失风险。这样在数据进行有效保护的同时,编辑器生成的文章样式可以无差异的发布到第三方应用,例如:微信等。

思考片刻后,决定自己也实现一个类似的简单Demo,可以在线编辑Markdown文件,使用编辑器生成的内容样式会和实时的和Blog最终样式保持一致,这样可以实现所见即所得的博客文章。 这样可以极大提高自己写作的效率

最终的Demo地址:传送门

通过自己实现的编辑器进行写博客,很有成就感,也可以极大的提高生产力 💐。

核心实现方案

研究了一番,在线编辑器使用到了新的API showDirectoryPicker,下面介绍下核心实现方法。

  1. 检查浏览器是否支持此API
if (!('showOpenFilePicker' in window)) {
    errorDialog('当前浏览器不支持访问文件系统API,请使用 Chrome86 以上版本')
    setPickButtonDisabled(true)
}

点击查看 Window:showDirectoryPicker() 方法

  1. 弹出目录选择窗选择授权的目录
// Window 接口的showDirectoryPicker() 方法用于显示一个目录选择器,以允许用户选择一个目录。
const directoryHandle = await window?.showDirectoryPicker({
  mode: 'readwrite', // 设为 "readwrite" 可对目录进行读写访问。
  startIn: 'desktop' // 用于指定选择器的起始目录。
})
  1. 遍历目录并获得文件
const listAllFilesAndDirs = async (dirHandle: FileSystemDirectoryHandle, path: string): Promise<File> => {
  const files: File = { // 文件结构
    name: dirHandle.name,
    dir: path,
    kind: dirHandle.kind,
    children: [],
    title: dirHandle.name,
    key: path
  }
  for await (const [name, handle] of dirHandle) {
    if (handle.kind === 'directory') { // 目录类型要进行递归
      const newPath = `${path}/${name}`
      files.dir = newPath
      files.children.push(await listAllFilesAndDirs(handle, newPath))
    } else {// 文件类型
      const file = await handle.getFile()
      if (!file?.name.startsWith('.')) { // 不能是隐藏文件
        file.relativePath = `${path}/${file.name}`
        files.children.push({
          name: file.name, // 文件名
          dir: file.relativePath, // 路径
          children: [],
          kind: 'file',
          isLeaf: true,
          title: file.name,
          key: file.relativePath,
          meta: file,
          fileHandler: handle // 文件句柄要保存,后边查看和编辑要用到
        })
      }
    }
  }
  return files
}

const getAllFiles = async (directoryHandle: FileSystemDirectoryHandle) => {
  const { name } = directoryHandle
  const files = await listAllFilesAndDirs(directoryHandle, name)
  return files;
}

const files = getAllFiles(directoryHandle)
console.log(files); // 最终的文章列表
  1. 点击文件后实现文件内容读取
const onSelect: DirectoryTreeProps['onSelect'] = (keys, info) => {
  const file = info.node?.meta
  if (!file) { // 点击的是文件夹
    return
  }
  if (file?.name && !file.name.endsWith('.md')) {
    infoDialog('仅支持 MarkDown 文件打开!')
    return
  }

  // 尝试通过handler读取文件内容
  const reader = new FileReader()
  reader.onerror = (event) => {
    console.log('文件读取读取错误:', event)
  }
  reader.onload = (e) => {
    const value = e.target?.result ?? ''
    if (typeof value === 'string') {
      setMdConentHtml(value); // 读取markdown文件,然后会解析为html文件放在右侧进行展示。
      setOpenFile(info.node?.fileHandler ?? null) // 保存当前编辑的文件句柄,保存要用
    } else {
      infoDialog('文件内容格式不正确!')
    }
  }
  reader.readAsText(file)
  // 文件读取结束
}
  1. 编辑完成后对文件进行保存
const saveFile = async () => {
    try {
      setSaveLoading(true) 
      const writableStream = await openFile?.createWritable({ keepExistingData: true })
      await writableStream.write(code) // 将文件写入
      await writableStream.close()
      successDialog(`保存成功: ${openFile?.name}`)
      directoryHandle && getAllFiles(directoryHandle) // 使用目录句柄再次刷新列表,否则保存后的文件无法再次进行查看和编辑了。
    } catch (e) {
      errorDialog(`保存失败:${e}`)
    } finally {
      setSaveLoading(false)
    }
  }

遇到的问题

  1. Next.js 中使用Antd5后会出现样式丢失问题。
    解决方法可以参考这个链接, 传送门

  2. 文件保存后,文件无法再次被编辑和查看。
    解决方法是,用户选择好授权目录后,将授权的句柄保存下来,每次保存,都会重新去重新读取此句柄遍历获得新的文件句柄。

关于我
loading