html text编辑器 仅预览_自己开发个Markdown编辑器(一)

d2c7f5748e063af124345cccfe49aeb5.png

在微信公众号写文章体验很不好,文档样式少,代码样式难看,且代码经常丢样式,其他平台如知乎等体验也不好。如果想在多平台发文章就更痛苦,每个平台都要单独调整一遍样式,非常费时。市面上也找不到一款非常合适的编辑器,感觉都比较鸡肋,尤其是文章插入图片等媒体时,很难转移到其他平台。所以我打算自己开发个Markdown编辑器,能够自定义样式,转到如微信公众号等平台时文中的图片等媒体可轻松上传。所以主要目标如下:
  • 可自定义元素样式 - 通过自己写css来控制元素样式

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

  • 轻松上传多家平台 - 先实现微信公众号,后期再接入知乎、掘金等

技术难点

技术上主要难点有如下几方面
  • 编辑器 - 好用且有高亮提示的编辑功能

  • markdown解析 - 将markdown内容转成html

  • 代码高亮 - markdown中的代码转成html后要有代码格式、关键字高亮

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

上传平台功能目前考虑使用复制粘贴方式,具体实现后面再详细设计,先具体实现编辑功能。编辑器与markdown解析器都有优秀的开源项目,所以只要选择合适的开源库即可,感谢开源贡献者们。 编辑器我们使用微软的 monaco-editor ;markdown解析器我们使用marked;代码高亮用hightlight.js。 搭建工程我们先构建web形式的工程,实现基本的编辑功能,后期再考虑用 Electron封装成本地。UI部分并不复杂,主要是相关功能的逻辑整合,所以我直接使用自己的工程模板来搭建。
# 克隆工程git clone --depth=1 git@github.com:guofei0723/ltrtb.git stylemd# 进入工程cd stylemd# 删除模板的git数据rm -rf .git# 初始化项目gitgit init# 安装工程依赖yarn# 安装项目需要的库yarn add monaco-editor marked# 启动工程yarn start
c28ca77347f697084a4e47c231c4c14b.png工程可以启动,说明一切正常。下面来做基本的布局,核心区域为左右结构,左侧编辑,右侧预览。修改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>)

a0926ba15bf7d28de011e7220127d216.png

左侧编辑器现在来实例化monaco-editor,首先为编辑器单独创建一个组件,放在src/components/editor.tsx
import React from 'react'interface EditorProps {}const Editor: React.FC = () => (  <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>

03f0239a97a9a45a21906324b4dcb52d.png

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 = () => {  // 外层div的ref  const wrapperRef = useRef(null)  // 编辑器实例  const editorIns = useRef()  // 一些初始化  useEffect(() => {    // 创建编辑器实例    editorIns.current = monaco.editor.create(wrapperRef.current as HTMLElement, {      language: 'markdown',      // 黑色风格      theme: 'vs-dark',    })  }, [])  return (    
"w-full h-full" ref={wrapperRef} /> )}export default Editor目前效果如下

2f3df9a3cee1ebd7f43ec3a0dbd008f1.png

现在可以正常输入内容了,但还不能预览,下面来做预览功能。预览功能首先要处理的是把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\`\`\`javascriptconsole.log('Hello, World!');\`\`\`bye`const Previewer: React.FC = () => {  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>
效果如下

ec3328fa0a826e0e03a54ead717489f2.png

因为我们现在是直接输出的内容,并没有作为元素插入页面,所以现在看到的是html源码。现在说明marked是正常工作了,下面来把解析结果插入页面,修改previewer.tsx
// ...return (  // eslint-disable-next-line react/no-danger  
__html: content }} />)// ...效果如下

8018b06f9d4668b00d6fed0aea65244c.png

现在的确是被作为元素插入页面,但却没有任何样式,这是因为受当前页面全局css的影响。由于工程使用的是tailwindcss,会清除所有默认样式,所以这些插入的元素也没有任何默认样式。为了不让预览的样式受页面样式的影响,我们把预览内容放到iframe中,修改previewer.tsx如下
// ...return (  <iframe    title="previewer"    srcDoc={content}    className="w-full h-full"  />)// ...

f330479febe6e2b84b04abdf82b6b58f.png

好的,预览内容独立出来后可以看到浏览器的默认样式了。现在考虑动态解析左侧编辑器中的内容。数据共享由于左侧编辑器与右侧预览器是相互独立的,所以考虑使用context将编辑器中的内容共享出来。新建store/docdata.tsx文件
import React, { useContext, useState } from 'react'const DocContext = React.createContext  string,  React.Dispatch>,] | 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])// ...

98fa52f6fa0ddae4fde473ca570b78d0.gif

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

monaco-editor

https://github.com/microsoft/monaco-editor

marked

https://github.com/markedjs/marked

highlight.js

https://highlightjs.org/

~ End ~

分享前端开发知识、所见所闻

欢迎关注公众号 小小前猿

9d32bfbc4da2aaf1409245dd48e32eb4.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值