Electron实现你自己的Markdown编辑软件

前言

公众号:【可乐前端】,期待关注交流,分享一些有意思的前端知识

在上一期我们已经实现了文件管理的功能,这篇文章主要会介绍文件顶部栏实现、Markdown编辑器的主体接入以及图床实现。

顶部栏

我们会实现一个顶部栏去管理当前打开的文件,这里主要用到的是antd的Tabs组件。整体的交互如下:

  • 点击左侧树的文件时:
    • 当前文件不在顶部栏内,打开比聚焦该文件
    • 如果已经存在,聚焦该文件
  • 点击顶部栏的tab,切换到改文件进行编辑
  • 点击关闭按钮,关闭对应的tab

点击左侧树的时候,可以如下实现:

js 复制代码
  const [tabs, setTabs] = useState([])
  const [activeKey, setActiveKey] = useState('')
  const [value, setValue] = useState('')
  const ipcRenderer = window.electron.ipcRenderer
  const handleSelect = (keys, { node }) => {
    const key = keys[0]
    if (!node.isLeaf) {
      return
    }
    const newTabs = [...tabs]
    const exist = newTabs.find((tab) => tab.key === key)
    if (!exist) {
      newTabs.push({ key, label: node.title })
      setTabs(newTabs)
    }
    setActiveKey(key)
  }
  // ...
   <Tabs
      hideAdd
      type="editable-card"
      activeKey={activeKey}
      onChange={(key) => setActiveKey(key)}
      onEdit={onEdit}
      items={tabs}
   />

这样点击的时候,对应的文件就会出现在顶部tab中,然后我们需要根据tab对应的文件路径取读取相应的文件内容。此时渲染进程可以向主进程发送一个事件,主进程读取到文件内容之后返回给渲染进程,渲染进程再交给编辑器处理。

js 复制代码
  useEffect(() => {
    if (!activeKey) {
      return
    }
    ipcRenderer.send(GET_FILE, activeKey)
  }, [activeKey])

监听到当前活跃的tab变更之后,向主进程发送事件,主进程接收到事件之后,就可以进行如下的读取文件操作:

js 复制代码
  ipcMain.on(GET_FILE, (event, key) => {
    try {
      const res = fs.readFileSync(key, { encoding: 'utf8' })
      event.sender.send(RECEIVE_FILE, res)
    } catch (error) {
      event.sender.send(COMMON_ERROR, '读取文件失败')
      event.sender.send(COMMON_ERROR_LOG, error)
    }
  })

这样渲染进程就可以获取到打开的文件内容。

然后来看一下移除标签页的逻辑,根据标签对应的key,从标签页数组中找到对应的项,检查是否要移除的标签页是当前活动标签页(activeKey),如果是的话,需要更新当前活跃的标签页。

如果移除的标签页不是最后一个标签页,就将当前活跃标签页设置为上一个标签页的key,否则设置为第一个标签页的key。最后,如果标签页数组为空,则把activeKey置空。

js 复制代码
  const onEdit = (targetKey) => {
    const remove = (targetKey) => {
      let newActiveKey = activeKey
      let lastIndex = -1
      tabs.forEach((item, i) => {
        if (item.key === targetKey) {
          lastIndex = i - 1
        }
      })
      const newPanes = tabs.filter((item) => item.key !== targetKey)
      if (newPanes.length && newActiveKey === targetKey) {
        if (lastIndex >= 0) {
          newActiveKey = newPanes[lastIndex].key
        } else {
          newActiveKey = newPanes[0].key
        }
      }
      if (newPanes.length === 0) {
        newActiveKey = ''
      }
      setTabs(newPanes)
      setActiveKey(newActiveKey)
    }
    remove(targetKey)
  }

顶部栏标签管理就实现到这里,下面我们来接入Markdown编辑器的主体。

编辑器主体

因为之前一直写文章用的都是掘金的Markdown编辑器,所以这里我也是直接接入了它的开源版本,由于我使用的是React技术栈,所以需要用到的包是@bytemd/react。

然后还可以安装一些常用的插件,比如@bytemd/plugin-gfm,它是专门处理 GitHub 风格的 Markdown 扩展语法(GitHub Flavored Markdown,简称 GFM)的插件。GFM 是 GitHub 对标准 Markdown 扩展的一种,引入了一些额外的功能,比如说任务列表、删除线、表格等;还有@bytemd/plugin-highlight,这是一个代码高亮的插件。

安装完对应的依赖之后,就可以通过十分简单的代码来使用这个组件了。

js 复制代码
import gfm from '@bytemd/plugin-gfm'
import highlight from '@bytemd/plugin-highlight'
import { Editor } from '@bytemd/react'
import 'bytemd/dist/index.css'
import zh from 'bytemd/locales/zh_Hans.json' //国际化json
import 'highlight.js/styles/default.css'
import './editor.css' // 额外的markdown主题样式
const plugins = [gfm(), highlight()]

const [value, setValue] = useState('')
<Editor
  uploadImages={handleUpload}
  mode="split"
  locale={zh}
  value={value}
  plugins={plugins}
  onChange={(v) => {
    setValue(v)
    updateFile(v)
  }}
/>

在内容更新的时候会触发一个onChange事件,这跟平时一般的input组件表现一致,这个时候我们需要更新组件的value以及更新对应文件的内容。

js 复制代码
  const updateFile = useCallback(
    debounce((value) => {
      ipcRenderer.send(UPDATE_FILE, activeKey, value)
    }, 300),
    [activeKey]
  )

实现一个updateFile来更新文件的内容,这里我加了一个防抖函数,让IO不要太过频繁。同样也是发送一个事件给主进程,让主进程去写文件。

js 复制代码
  ipcMain.on(UPDATE_FILE, (event, key, value) => {
    try {
      console.log('更新文件内容')
      fs.writeFileSync(key, value, { encoding: 'utf8' })
    } catch (error) {
      event.sender.send(COMMON_ERROR, '更新文件失败')
      event.sender.send(COMMON_ERROR_LOG, error)
    }
  })

这样我们的编辑器主体接入就完成了,可以开始快乐的写文章啦。

图床

写文章的时候怎么能不配图呢?配图怎么能少的了图床呢?所以这一小节是基于GitHub仓库来搭建了一个图床。

首先打开你的GitHub点击新建仓库(这里我由于已经创建过了所以显示仓库已存在。),然后打开GitHub token管理页面,新建一个token。

新建的时候把这里钩上

然后点击生成,token就新建好了,请注意保管好。

然后我们就可以开始尝试把文件上传到GitHub了:

js 复制代码
import axios from 'axios'
export const generateRandomFileName = (file) => {
  return `${window.crypto.randomUUID()}.${file.type.split('/')[1]}`
}

function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()

    reader.onload = () => {
      resolve(reader.result.split(',')[1])
    }

    reader.onerror = (error) => {
      reject(error)
    }

    reader.readAsDataURL(file)
  })
}

const token = '你的token'
const owner = '你的用户名'
const repo = '仓库名'
const commitMessage = 'Upload image to GitHub'
export const uploadImageToGitHub = async (file) => {
  try {
    const path = generateRandomFileName(file)
    // 构造请求头
    const headers = {
      Authorization: `token ${token}`,
      'Content-Type': 'application/json'
    }
    const imageContent = await fileToBase64(file)
    // 构造请求体
    const requestData = {
      message: commitMessage,
      content: imageContent,
      path: path
    }

    // 发送 HTTP 请求
    const response = await axios.put(
      `https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
      requestData,
      { headers }
    )

    // 输出上传结果
    console.log('Image uploaded successfully:', response.data)
    return `https://cdn.jsdelivr.net/gh/${owner}/${repo}@main/${response.data.content.path}`
  } catch (error) {
    console.error('Failed to upload image to GitHub:', error.response.data)
    return ''
  }
}

让我们一起看看上面的代码做了什么:

  • generateRandomFileName 函数:接收一个文件对象 file,通过使用 window.crypto.randomUUID() 生成一个随机的文件名,保证文件名的唯一性,使用文件的类型(file.type)作为文件扩展名。

  • fileToBase64 函数:将文件对象转换为 base64 编码的字符串。

  • uploadImageToGitHub 函数:接收一个文件对象 file,通过调用前面两个函数,生成随机的文件名和将文件转换为 base64 编码的字符串。然后,使用 Axios 库发送一个 PUT 请求到 GitHub API 的 contents 端点,以上传文件。

上传成功之后,可以通过jsdelivr的CDN服务包裹一下我们的图片链接,让我们的图片资源访问的更快:https://cdn.jsdelivr.net/gh/${owner}/${repo}@main/${response.data.content.path}

到这里我们就已经实现了将File对象上传到GitHub的功能,剩下需要做的就是在编辑器组件中接入这个上传功能。

bytemd暴露了uploadImages这个属性,当我们在编辑器中上传、粘贴图片时会触发这个方法,我们可以在这里拿到我们在本地上传的图片,然后调用上传到GitHub的接口,这样就实现了在编辑器中上传图片。

js 复制代码
  const handleUpload = async (files) => {
    const urls = await Promise.all(
      files.map(async (file) => {
        const url = await uploadImageToGitHub(file)
        return {
          url
        }
      })
    )
    return urls
  }

最后

到这里我们就已经实现了自己的 Markdown编辑软件,这篇文章就是在这个软件下写出来的。大体功能没有什么问题,就是还有一些小的交互细节可以持续去优化一下。如果你觉得有意思的话,点点关注点点赞吧~

相关推荐
卸任11 小时前
Vibe coding工具使用心得
前端·ai编程
brien912 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance12 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M12 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写12 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李12 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易13 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding13 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马13 小时前
前沿视界
前端
kyriewen13 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员