前言
之前,一直想开发一款属于自己的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. 配置编辑器组件
在配置编辑器时,有以下几点使我非常困惑,以致于花费了大量时间。
- 代码没有被高亮
- 语言不是中文
- 编辑器样式有问题
以上这几个问题通过以下措施才得以解决:
- 通过阅读文档:
https://nhn.github.io/tui.editor/latest/
- 访问Github网站:
https://github.com/nhn/tui.editor
Editor.vue
代码语言:javascript复制<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>
看似上面几行代码,但是也是很费劲才得以完成。
增加功能
首先,我开发这个程序的初衷是更好地方便自己写文章,所以,我定下了这几个需求:
- 可复制HTML格式文本,方便复制到微信公众号
- 可复制Markdown文本,方便可以复制到稀土掘金、csdn这些博客网站上发布
- 可下载Markdown文件,更加方便保存和移动
因篇幅原因,先奉上主要逻辑代码。这里我使用了clipboard
这个将文本复制到剪贴板的插件。网址:https://clipboardjs.com/
。
另外,downloadBlobAsFile
方法主要是创建Blob
对象,然后通过a标签的download
属性进行下载。
downloadBlobAsFile.js
代码语言:javascript复制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', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(e);
}
Editor.vue
代码语言:javascript复制<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>
针对微信公众号进行样式优化
::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;
}
线上体验
代码语言:javascript复制https://www.maomin.club/site/mdeditor/
结语
谢谢阅读,希望没有浪费你的时间。
源码地址:
代码语言:javascript复制https://github.com/maomincoding/simpleMdEditor
如果对你有帮助,欢迎Star~