我熬夜开发了一款简约实用、支持多平台的Markdown在线编辑器(开源)

开发 开发工具
之前,一直想开发一款属于自己的Markdown编辑器,主要是自己平常写文章可以更加灵活操作,另外扩宽自己的视野也是非常不错的选择啊!所以在周末就决定玩耍一番。首先我调研了很多线上热门的md编辑器,都很优秀。不为超过他们,主要自己用着舒服点。这篇文章主要是记录下我是如何从0到1是完成一款还算拿得出手的Markdown编辑器。

[[385871]]

 前言

之前,一直想开发一款属于自己的Markdown编辑器,主要是自己平常写文章可以更加灵活操作,另外扩宽自己的视野也是非常不错的选择啊!所以在周末就决定玩耍一番。首先我调研了很多线上热门的md编辑器,都很优秀。不为超过他们,主要自己用着舒服点。这篇文章主要是记录下我是如何从0到1是完成一款还算拿得出手的Markdown编辑器。

完成项目一览



调研Markdown编辑器

国内、国外关于Markdown编辑器有很多。

editor.md

网址:https://pandao.github.io/editor.md/

是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。这个组件好像是国内开发的,个人之前用着还可以。

typora

网址:https://www.typora.io/

Typora是一款免费的轻量级Markdown编辑器,它没有Mou,Haroopad等Markdown编辑器那么大名鼎鼎,算是较为小众的一款产品。凭良心说话,我用过的Markdown编辑器也有好几款,其中包括:小书匠,Haroopad,Atom等,但Typora是最合我心意的一款编辑器了,其轻量、快速、易于上手,使用起来简直不要太舒服!!

tui-editor

网址:https://ui.toast.com/tui-editor

这是一款Markdown组件,通过调研决定用它。为什么?确认过眼神~

技术栈

  • Vue.js
  • tui-editor

实战

确定好技术栈之后,我们就得脚踏实地地干活了。

1. 搭建Vue脚手架

我们会使用VueCLI搭建一个最基础的项目,这里暂时不需要Vue-router、Vuex这些插件,所以尽可能轻装。

2. 创建编辑器组件

我们会在components文件目录下创建一个Editor.vue文件,这个文件也就是我们的主战场,大部分操作都会在这个文件。

3. 配置编辑器组件

在配置编辑器时,有以下几点使我非常困惑,以致于花费了大量时间。

  1. 代码没有被高亮
  2. 语言不是中文
  3. 编辑器样式有问题

以上这几个问题通过以下措施才得以解决:

  1. 通过阅读文档:https://nhn.github.io/tui.editor/latest/
  2. 访问Github网站:https://github.com/nhn/tui.editor

Editor.vue

  1. <template> 
  2.   <div class="main"
  3.     <div id="editor"></div> 
  4.   </div> 
  5. </template> 
  6. <script> 
  7. import Editor from "@toast-ui/editor"
  8. import hljs from "highlight.js"
  9. import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight"
  10. import '@toast-ui/editor/dist/i18n/zh-cn.js'
  11.  
  12. import "highlight.js/styles/github.css"
  13. import "codemirror/lib/codemirror.css"; // Editor's Dependency Style 
  14. import "@toast-ui/editor/dist/toastui-editor.css"; // Editor's Style 
  15. import "@/styles/index.css"
  16. export default { 
  17.   components: {}, 
  18.   data() { 
  19.     return { 
  20.       editor: null 
  21.     }; 
  22.   }, 
  23.   mounted() { 
  24.     this.editor = new Editor({ 
  25.       el: document.getElementById("editor"), 
  26.       plugins: [[codeSyntaxHighlight, {hljs}]], 
  27.       previewStyle: "vertical"
  28.       height: "100vh"
  29.       initialEditType: "markdown"
  30.       minHeight: "200px"
  31.       initialValue: ""
  32.       placeholder: "你想写点什么..."
  33.       language:'zh-CN'
  34.       useCommandShortcut: true
  35.       useDefaultHTMLSanitizer: true
  36.       usageStatistics: false
  37.       hideModeSwitch: false
  38.       viewer: true
  39.       toolbarItems: [ 
  40.         "heading"
  41.         "bold"
  42.         "italic"
  43.         "strike"
  44.         "divider"
  45.         "hr"
  46.         "quote"
  47.         "divider"
  48.         "ul"
  49.         "ol"
  50.         "task"
  51.         "indent"
  52.         "outdent"
  53.         "divider"
  54.         "table"
  55.         "image"
  56.         "link"
  57.         "divider"
  58.         "code"
  59.         "codeblock"
  60.       ], 
  61.     }); 
  62.     this.editor.getUI().getToolbar().removeItem("21"); 
  63.   }, 
  64. }; 
  65. </script> 

看似上面几行代码,但是也是很费劲才得以完成。

增加功能

首先,我开发这个程序的初衷是更好地方便自己写文章,所以,我定下了这几个需求:

  1. 可复制HTML格式文本,方便复制到微信公众号
  2. 可复制Markdown文本,方便可以复制到稀土掘金、csdn这些博客网站上发布
  3. 可下载Markdown文件,更加方便保存和移动

因篇幅原因,先奉上主要逻辑代码。这里我使用了clipboard这个将文本复制到剪贴板的插件。网址:https://clipboardjs.com/。

另外,downloadBlobAsFile方法主要是创建Blob对象,然后通过a标签的download属性进行下载。

downloadBlobAsFile.js

  1. export default function downloadBlobAsFile(data, filename) { 
  2.     const contentType = 'application/octet-stream'
  3.     if (!data) { 
  4.         console.error(' No data'); 
  5.         return
  6.     } 
  7.  
  8.     if (!filename) { 
  9.         filename = 'filetodonwload.txt'
  10.     } 
  11.  
  12.     if (typeof data === 'object') { 
  13.         data = JSON.stringify(data, undefined, 4); 
  14.     } 
  15.  
  16.     let blob = new Blob([data], {type: contentType}); 
  17.     let e = document.createEvent('MouseEvents'); 
  18.     let a = document.createElement('a'); 
  19.  
  20.     a.download = filename; 
  21.     a.href = URL.createObjectURL(blob); 
  22.     a.dataset.downloadurl = [contentType, a.download, a.href].join(':'); 
  23.     e.initMouseEvent('click'truefalse, window, 0, 0, 0, 0, 0, falsefalsefalsefalse, 0, null); 
  24.     a.dispatchEvent(e); 

Editor.vue

  1. <template> 
  2.   <div class="main"
  3.     <div class="tools"
  4.       <el-button 
  5.           size="mini" 
  6.           type="primary" 
  7.           @click="drawer = true" 
  8.       >工具</el-button> 
  9.       <el-button 
  10.           size="mini" 
  11.           type="primary" 
  12.           @click="aboutView = true" 
  13.       >关于</el-button> 
  14.       <el-dialog 
  15.           :title="'工具'" 
  16.           :visible.sync="drawer" 
  17.           :append-to-body="true" 
  18.       > 
  19.         <div class="tool-innter"
  20.           <el-button type="primary" @click="getHtml" class="htmlbtn" 
  21.           >复制HTML 
  22.           </el-button 
  23.           > 
  24.           <el-button type="primary" @click="getMd" class="mdbtn" 
  25.           >复制MarkDown 
  26.           </el-button 
  27.           > 
  28.           <el-button type="primary" @click="downloadMd" class="downloadbtn" 
  29.           >下载MarkDown 
  30.           </el-button 
  31.           > 
  32.         </div> 
  33.       </el-dialog> 
  34.       <el-dialog 
  35.           :title="'关于'" 
  36.           :visible.sync="aboutView" 
  37.           :append-to-body="true" 
  38.       > 
  39.         <h3>Simple·MarkDown编辑器</h3> 
  40.         <ul class="functionList"
  41.           <li v-for="(item,index) in functionList" :key="index"
  42.             {{item}} 
  43.           </li> 
  44.         </ul> 
  45.         <h3>作者</h3> 
  46.         <ul class="functionList"
  47.           <li v-for="(item,index) in authorList" :key="index">{{item}}</li> 
  48.         </ul> 
  49.         <div class="wxcode"
  50.           <img src="../assets/wxcode.jpeg" alt=""
  51.         </div> 
  52.       </el-dialog> 
  53.     </div> 
  54.     <div id="editor"></div> 
  55.   </div> 
  56. </template> 
  57. <script> 
  58. import Editor from "@toast-ui/editor"
  59. import Clipboard from "clipboard"
  60. import hljs from "highlight.js"
  61. import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight"
  62. import '@toast-ui/editor/dist/i18n/zh-cn.js'
  63. import downloadBlobAsFile from "../utils/download"
  64.  
  65. import "highlight.js/styles/github.css"; //https://github.com/highlightjs/highlight.js/tree/master/src/styles 
  66. import "codemirror/lib/codemirror.css"; // Editor's Dependency Style 
  67. import "@toast-ui/editor/dist/toastui-editor.css"; // Editor's Style 
  68. import "@/styles/index.css"
  69. export default { 
  70.   components: {}, 
  71.   data() { 
  72.     return { 
  73.       editor: null
  74.       drawer: false
  75.       aboutView: false
  76.       functionList:['页面简约','功能实用','支持稀土掘金、CSDN、微信公众号、知乎','可复制HTML、MarkDown','可下载MarkDown文件'], 
  77.       authorList:['作者:Vam的金豆之路','欢迎关注我的公众号:前端历劫之路','我创建了一个技术交流、文章分享群,群里有很多大厂的前端大佬,关注公众号后,点击下方菜单了解更多即可加我微信,期待你的加入'
  78.     }; 
  79.   }, 
  80.   methods: { 
  81.     // 复制HTML 
  82.     getHtml() { 
  83.       const clipboard = new Clipboard(".htmlbtn", { 
  84.         target: () => this.editor.preview.el, 
  85.       }); 
  86.       clipboard.on("success", () => { 
  87.         this.$message({ 
  88.           message: "复制成功"
  89.           type: "success"
  90.         }); 
  91.         clipboard.destroy(); 
  92.       }); 
  93.       clipboard.on("error", () => { 
  94.         this.$message.error("复制失败"); 
  95.         clipboard.destroy(); 
  96.       }); 
  97.     }, 
  98.     // 复制Markdown 
  99.     getMd() { 
  100.       const clipboard = new Clipboard(".mdbtn", { 
  101.         text: () => this.editor.getMarkdown(), 
  102.       }); 
  103.       clipboard.on("success", () => { 
  104.         this.$message({ 
  105.           message: "复制成功"
  106.           type: "success"
  107.         }); 
  108.         clipboard.destroy(); 
  109.       }); 
  110.       clipboard.on("error", () => { 
  111.         this.$message.error("复制失败"); 
  112.         clipboard.destroy(); 
  113.       }); 
  114.     }, 
  115.     // 下载Markdown 
  116.     downloadMd() { 
  117.       if (this.editor.getMarkdown().trim()) { 
  118.         downloadBlobAsFile(this.editor.getMarkdown(), "unnamed.md"); 
  119.       } else { 
  120.         this.$message.error("下载失败"); 
  121.       } 
  122.     }, 
  123.   }, 
  124.   mounted() { 
  125.     this.editor = new Editor({ 
  126.       el: document.getElementById("editor"), 
  127.       plugins: [[codeSyntaxHighlight, {hljs}]], 
  128.       previewStyle: "vertical"
  129.       height: "100vh"
  130.       initialEditType: "markdown"
  131.       minHeight: "200px"
  132.       initialValue: ""
  133.       placeholder: "你想写点什么..."
  134.       language:'zh-CN'
  135.       useCommandShortcut: true
  136.       useDefaultHTMLSanitizer: true
  137.       usageStatistics: false
  138.       hideModeSwitch: false
  139.       viewer: true
  140.       toolbarItems: [ 
  141.         "heading"
  142.         "bold"
  143.         "italic"
  144.         "strike"
  145.         "divider"
  146.         "hr"
  147.         "quote"
  148.         "divider"
  149.         "ul"
  150.         "ol"
  151.         "task"
  152.         "indent"
  153.         "outdent"
  154.         "divider"
  155.         "table"
  156.         "image"
  157.         "link"
  158.         "divider"
  159.         "code"
  160.         "codeblock"
  161.       ], 
  162.     }); 
  163.     this.editor.getUI().getToolbar().removeItem("21"); 
  164.   }, 
  165. }; 
  166. </script> 

针对微信公众号进行样式优化

::v-deep是深度作用选择器,主要是为了覆盖原有的样式所用。

  1. ::v-deep ul li { 
  2.   list-style-type: disc !important; 
  3.  
  4. ::v-deep ol li { 
  5.   list-style-type: decimal !important; 
  6.  
  7. ::v-deep ul li::before, ::v-deep ol li::before { 
  8.   content: none; 
  9. ::v-deep .tui-editor-contents p>code{ 
  10.   background-color: #fff5f5; 
  11.   color: #ff502c; 
  12. ::v-deep .tui-editor-contents pre { 
  13.   width: 100%; 
  14.   overflow: auto; 

线上体验

https://www.maomin.club/site/mdeditor/

结语

谢谢阅读,希望没有浪费你的时间。

源码地址:

https://github.com/maomincoding/simpleMdEditor

 

责任编辑:姜华 来源: 前端历劫之路
相关推荐

2020-09-18 06:00:51

开源Markdown编辑器

2023-06-20 00:04:18

框架开发UMD

2021-11-24 09:12:11

Markdown编辑器Linux

2020-09-16 10:27:50

MarkDown编辑器编程

2024-03-06 08:26:29

2021-08-26 05:15:22

图片编辑器 H5-DooringMitu-Doorin

2017-05-23 19:19:16

开源Markdown编辑器

2022-08-31 08:32:22

数据可视化项目nocode

2021-10-27 14:55:57

Mark TextMarkdown编辑器

2021-10-21 10:58:03

Markdown编辑器

2022-04-27 08:42:20

Markdown编辑神器

2021-04-08 14:58:59

开发前端编辑器

2021-06-23 06:12:38

Subtitld编辑器开源

2014-09-05 09:45:46

2023-09-10 23:22:33

Zettlr笔记编辑器

2022-01-10 18:16:24

编辑器Typora Markdown

2021-04-04 08:16:09

NewsFlash阅读器开源

2021-12-23 10:59:30

开源技术 软件

2022-09-05 13:16:42

MicroVim编辑器

2015-06-01 07:37:42

开发产品
点赞
收藏

51CTO技术栈公众号