轻松记录您
灵感和创意

如何自己开发个Markdown编辑器(1)

在 写文章体验很不好,文档样式少,代码样式难看,且代码经常丢样式,其他平台如知乎等体验也不好。如果想在多平台发文章就更痛苦,每个平台都要单独调整一遍样式,非常费时。市面上也找不到一款非常合适的编辑器,感觉都比较鸡肋,尤其是文章插入图片等媒体时,很难转移到其他平台。

所以我打算自己开发个Markdown编辑器,能够自定义样式,转到如 等平台时文中的图片等媒体可轻松上传。

所以主要目标:

可自定义元素样式 – 通过自己写css来控制元素样式

好看的代码样式 – 支持多种编程语言,且样式可选

轻松上传多家平台 – 先实现 ,后期再接入知乎、掘金等

技术难点

技术上主要难点有几方面

编辑器 – 好用且有高亮提示的编辑功能

Markdown解析 – 将Markdown内容转成html

代码高亮 – Markdown中的代码转成html后要有代码格式、关键字高亮

上传平台 – 如何将带有样式片等多媒体内容的文章上传(或粘贴)到不同平台

上传平台功能目前考虑使用复制粘贴方式,具体实现后面再详细设计,先具体实现编辑功能。编辑器与Markdown解析器都有优秀的开源项目,所以只要选择合适的开源库即可,感谢开源贡献者们。编辑器我们使用微软的monaco-editorMarkdown解析器我们使用marked;代码高亮用hightlight.js。搭建工程

我们先构建web形式的工程,实现基本的编辑功能,后期再考虑用Electron封装成本地。UI部分并不复杂,主要是相关功能的逻辑整合,所以我直接使用自己的工程模板来搭建。

# 克隆工程
git clone --depth=1 git@github.com:guofei0723/ltrtb.git stylemd
# 进入工程
cd stylemd
# 删除模板的git数据
rm -rf .git
# 初始化项目git
git init
# 安装工程依赖
yarn
# 安装项目需要的库
yarn add monaco-editor marked
# 启动工程
yarn start

工程可以启动,说明一切正常。下面来做基本的布局,核心区域为左右结构,左侧编辑,右侧预览。修改src/App.tsx

export default () => (
  <div className="h-screen w-screen flex bg-teal-900">
    <div className="main-left flex-1 h-full bg-gray-900 text-white">
      这边是编辑器
    </div>
    <div className="slider w-1 h-full" />
    <div className="main-right flex-1 h-full bg-white">
      这边是预览
    </div>
  </div>
)

左侧编辑器

现在来实例化monaco-editor,首先为编辑器单独创建一个组件,放在src/components/editor.tsx

import React from 'react'

interface EditorProps {

}

const Editor: React.FC<EditorProps> = () => (
  <div className="w-full h-full">
    我是编辑器
  </div>
)

export default Editor

再将页面左侧内容改为编辑器。

<div className="main-left flex-1 h-full bg-gray-900 text-white">
  <Editor />
</div>

monaco-editor使用了worker来处理代码内容,由于支持的语言很多所以对应的worker也很多。我们是基于webpack的工程,打包时可以仅包含我们需要的编程语言支持,monaco-editor提供了相应的webpack插件,以方便对其集成。先安装webpack插件

yarn add -D monaco-editor-webpack-plugin

在webpack配置中添加该插件

// ...
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
// ...
{
  // ...
  plugins: [
    new MonacoWebpackPlugin({
      languages: ['css', 'markdown'],
    }),

  ]
}

由于是在做Markdown编辑器,而且要支持通过css自定义样式,所以编程语言需要Markdown与css。重启工程,然后在组件中创建编辑器对象,Editor.tsx修改为以下内容

import React, { useEffect, useRef } from 'react'
import * as monaco from 'monaco-editor'

interface EditorProps {

}

const Editor: React.FC<EditorProps> = () => {
  // 外层div的ref
  const wrapperRef = useRef<HTMLDivElement>(null)
  // 编辑器实例
  const editorIns = useRef<monaco.editor.IStandaloneCodeEditor>()

  // 一些初始化
  useEffect(() => {
    // 创建编辑器实例
    editorIns.current = monaco.editor.create(wrapperRef.current as HTMLElement, {
      language: 'markdown',
      // 黑色风格
      theme: 'vs-dark',
    })
  }, [])

  return (
    <div className="w-full h-full" ref={wrapperRef} />
  )
}

export default Editor

目前效果

现在可以正常输入内容了,但还不能预览,下面来做预览功能。

预览功能

首先要处理的是把Markdown解析成html,我们通过marked来实现。创建预览器组件previewer.tsx

import React, { useEffect, useState } from 'react'
import marked from 'marked'

interface PreviewerProps {

}

const demoMarkdown = 
# 这里当作是标题
## here should be h2
------
* list item 1
* list item 2

> hello
> Nice to meet you

\\\
console.log('Hello, World!');
\\\

bye

const Previewer: React.FC<PreviewerProps> = () => {
  const [content, setContent] = useState('')

  useEffect(() => {
    setContent(marked(demoMarkdown))
  }, [])

  return (
    <div>
      {content}
    </div>
  )
}

export default Previewer

这里我们先硬编码了一段Markdown内容用于测试,功能没问题后再动态解析左侧输入的内容。将页面右侧改为预览器组件

<div className="main-right flex-1 h-full bg-white">
  <Previewer />
</div>

效果

因为我们现在是直接输出的内容,并没有作为元素插入页面,所以现在看到的是html源码。现在说明marked是正常工作了,下面来把解析结果插入页面,修改previewer.tsx

// ...
return (
  // eslint-disable-next-line react/no-danger
  <div dangerouslySetInnerHTML={{ __html: content }} />
)
// ...

效果

现在的确是被作为元素插入页面,但却没有任何样式,这是因为受当前页面全局css的影响。由于工程使用的是tailwindcss,会清除所有默认样式,所以这些插入的元素也没有任何默认样式。为了不让预览的样式受页面样式的影响,我们把预览内容放到iframe中,修改previewer.tsx

// ...
return (
  <iframe
    title="previewer"
    srcDoc={content}
    className="w-full h-full"
  />
)
// ...

好的,预览内容独立出来后可以看到浏览器的默认样式了。现在考虑动态解析左侧编辑器中的内容。

数据共享

由于左侧编辑器与右侧预览器是相互独立的,所以考虑使用context将编辑器中的内容共享出来。

新建store/docdata.tsx文件

import React, { useContext, useState } from 'react'

const DocContext = React.createContext<[
  string,
  React.Dispatch<React.SetStateAction<string>>,
] | null>(null)

export function DocProvider({ children } : React.PropsWithChildren<{}>) {
  const docState = useState('')

  return (
    <DocContext.Provider value={docState}>
      {children}
    </DocContext.Provider>
  )
}

export const useDocState = () => useContext(DocContext) as DocState

使用DocProvider包装整个app,修改src/App.tsx

// ...
export default () => (
  <DocProvider>
    {/* ... */}
  </DocProvider>
)

修改editor.tsx,使其共享出编辑的文档内容

// ...
import { useDocState } from '@/store/docdata'
// ...
// 拿到设置共享doc的方法
const [, setDoc] = useDocState()
// 一些无需react监听的状态数据
const status = useRef({
  // 更新内容的计时器
  updateTimer: -1,
}).current

// 一些初始化
useEffect(() => {
  // 创建编辑器实例
  editorIns.current = monaco.editor.create(wrapperRef.current as HTMLElement, {
    language: 'markdown',
    // 黑色风格
    theme: 'vs-dark',
  })

  const { current: edt } = editorIns
  // 监听内容变化
  edt.onDidChangeModelContent(() => {
    // 如果当前没有倒计时更新,则5秒后更新
    if (status.updateTimer < 0) {
      status.updateTimer = window.setTimeout(() => {
        // 标识已经完成更新
        status.updateTimer = -1
        // 更新内容
        setDoc(edt.getValue())
      }, 5000)
    }
  })
}, [])

修改previewer.tsx,当内容变化时自动解析

// ...
import { useDocState } from '@/store/docdata'
// ...
// 获取编辑的内容
const [doc] = useDocState()

// 文档内容变化时更新
useEffect(() => {
  setContent(marked(doc))
}, [doc])
// ...

自动解析已经可用,后面重点就是实现样式与插入图片了。今天时间已经不早,姑且先到这

未经允许不得转载:坚果之云 Markdown » 如何自己开发个Markdown编辑器(1)
分享到: 更多 (0)

坚果云Markdown轻松记录您 灵感和创意

坚果云Markdown下载坚果云Markdown介绍