我熬夜开发了一款简约实用、支持多平台的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

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

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

增加功能

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

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

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

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

downloadBlobAsFile.js

export default function downloadBlobAsFile(data, filename) { 
    const contentType = 'application/octet-stream'
    if (!data) { 
        console.error(' No data'); 
        return
    } 
 
    if (!filename) { 
        filename = 'filetodonwload.txt'
    } 
 
    if (typeof data === 'object') { 
        data = JSON.stringify(data, undefined, 4); 
    } 
 
    let blob = new Blob([data], {type: contentType}); 
    let e = document.createEvent('MouseEvents'); 
    let a = document.createElement('a'); 
 
    a.download = filename; 
    a.href = URL.createObjectURL(blob); 
    a.dataset.downloadurl = [contentType, a.download, a.href].join(':'); 
    e.initMouseEvent('click'truefalse, window, 0, 0, 0, 0, 0, falsefalsefalsefalse, 0, null); 
    a.dispatchEvent(e); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.

Editor.vue

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

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

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

::v-deep ul li { 
  list-style-type: disc !important; 

 
::v-deep ol li { 
  list-style-type: decimal !important; 

 
::v-deep ul li::before, ::v-deep ol li::before { 
  content: none; 

::v-deep .tui-editor-contents p>code{ 
  background-color: #fff5f5; 
  color: #ff502c; 

::v-deep .tui-editor-contents pre { 
  width: 100%; 
  overflow: auto; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

线上体验

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

结语

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

源码地址:

https://github.com/maomincoding/simpleMdEditor

 

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

2023-06-20 00:04:18

框架开发UMD

2020-09-18 06:00:51

开源Markdown编辑器

2021-11-24 09:12:11

Markdown编辑器Linux

2020-09-16 10:27:50

MarkDown编辑器编程

2024-03-06 08:26:29

2017-05-23 19:19:16

开源Markdown编辑器

2022-08-31 08:32:22

数据可视化项目nocode

2021-08-26 05:15:22

图片编辑器 H5-DooringMitu-Doorin

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编辑器开源

2022-01-10 18:16:24

编辑器Typora Markdown

2023-09-10 23:22:33

Zettlr笔记编辑器

2022-09-05 13:16:42

MicroVim编辑器

2021-12-23 10:59:30

开源技术 软件

2021-04-12 08:31:53

PC-Dooring项目PC端搭建

2014-09-05 09:45:46

2021-04-04 08:16:09

NewsFlash阅读器开源
点赞
收藏

51CTO技术栈公众号