MarkPane--简洁美观的md编辑器
MarkPane--简洁美观的md编辑器
项目介绍
项目名称:markPane。
简介:一个简洁的网页端markdown编辑器,可以实时预览,多文件切换,保存下载文件等。且内容可以长期保持,采用localstorage存储。
项目背景:学完vue3的课程后,我开始在github上面找项目,先打算从简单做起。平常都是用typora编辑器来写博客或者知识总结,就想着搜一下markdown编辑器。然后我看到了一个项目:markCook,看到界面和功能啥的都挺好的,简洁又美观,果断star,想着复刻一下。原项目是2018年的,基于vue2和vuex等,所以我准备使用vue3和pinia等新的工具来重构这个项目。整个过程大概一周,我也学过vue2,所以阅读源码不是很困难,在写代码的过程中,我也体会到前端更新真快,越来越多的工具或插件使得开发过程更加容易。
项目地址: markPane 在线体验: MarkPane 3.0 | A smart and beautiful markdown editor 参照了原作者项目: markCook
组件结构
整个界面分为4个区域,分别为左侧的侧边栏,右上的工具栏,右侧的输入区域和渲染区域。 以下讲述其实现过程。
存储结构
采用pinia工具存储,搭配piniaPluginPersistedstate插件,实现本地存储,数据具有持久性。 本项目中主要存储用户书写的文件articleList,每个文件有唯一的id,有一个currentId,表示当前预览的文件的id。
具体实现
侧边栏asideMenu实现
外观上html和css的实现都比较简单,logo加上一个显示文件列表的v-for(li),再加上几个button。\
保存文件后面再介绍
实现输入区域inputArea
将文章的内容显示在屏幕上,容易想到的是直接用v-model关联store和template,为了方便控制我使用了v-model的原本实现方式,即分为:value和@input,前者将store内容绑定到屏幕上,后者用户编辑时将store内容也更新。
实现渲染区域prewviewArea
实现md文本转html:
利用marked、markedHighlight和highlight.js,参考了markedjs/marked-highlight: Add code highlighting to marked (github.com)
markedjs的作用是将md转换成html元素,例如# merrick
,转换成<h1></h1>
,
转换代码块,
int a;
cout<<"hello world!"<<endl
转换为,
<pre><code class="hljs language-cpp"><span class="hljs-type">int</span> a;
cout<<<span class="hljs-string">"hello world!"</span><<endl
</code></pre>
从上述代码中,可以看出mardedjs会为我们创建标签如<h1>、<pre>、<code>
等。但是我们也注意到标签上还有class如hljs language-cpp hljs-string
等,使得代码高亮,这就是markedHighlght和highlight的作用。除此之外,我们还需引入相应的代码css样式,有不同的主题,比如vscode中常用的atom-one-dark等。可以在highlight.js (v11.7.0)| BootCDN 中引入。
markedjs的配置如下:
// 用于md到html的转换,同时配置好代码高亮样式
marked.use(
{
mangle: false,
},
{
headerIds: false,
},
markedHighlight({
langPrefix: "hljs language-",
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : "plaintext";
return hljs.highlight(code, { language }).value;
},
})
);
使用:marked.prase(value)
// 渲染文本,.md --》 html
const previewContent = computed(() => {
return marked.parse(rawContent.value);
});
template中绑定
<template>
<div ref="previewer" class="preview" v-html="previewContent"></div>
</template>
同步滚动
用户上下滚动输入区域的同时,渲染区域也会随之滚动。 因为两个组件属于兄弟组件,所以涉及到兄弟组件之间的通信。 之前学js的时候,我们可以通过dom方法从dom tree上直接获取那个元素,但是在vue中我们改用了模板引用ref。 这里需要在inputArea中获取到previewArea这个元素,我这里使用的方法是将store作为中转站引用previewArea,这样inpurArea就可以通过store获取到previewArea元素。再通过以下方法实现同步滚动:
//同步滚动
const syncScroll = (e) => {
store.previewer.scrollTop = e.target.scrollTop;
};
数据持久存储localstorage
使用了piniaPluginPersistedstate插件,好处是无需手写存入与获取localstorage的代码,非常方便,通过path属性可以指定需要保存的数据,代码如下所示:
{
// localstorage:使用piniaPluginPersistedstate插件
persist: {
paths: ["articleList", "currentId"],
},
}
存储数据

工具栏功能实现
工具栏主要用于快捷添加文本,例如代码块、表格等,同时还可以改变原样式,如给文本加粗、斜体、强调等。实际上就是两种类型,一种是在光标处插入新的内容,另一种是给选中的文本添加诸如加粗、链接等,实现起来需要考虑光标的位置。
-
通过
store.inputer.selectionStart
和..selectionEnd可以获取到当前光标的位置,当两者相等时,表示直接原地插入内容;当两者不等时,表示需要插入内容来修饰选中的内容。 -
插入内容后,有时需要选中可编辑的文本内容,如插入一个链接后,自动选中url内容,方便用户快速编辑。
这时需要我们主动设置好光标的选中方位,使用setSelectionRange(start,end)
,需要注意的时,设置之前需要使元素聚焦focus。了解清楚这些,就可以用代码将其实现了,分两种情况,每种情况中不同插入内容实现方式差不多。
文件保存
保存为两种格式(.html)和(.md)。
使用a标签下载,利用Blob将文章内容转换为Blob
对象,再利用URL.createObjectURL()
为其创建url,绑定到href属性上,download属性为下载文件的名称 = title + 后缀.html/.md
<a
:href="htmlDataUrl"
:download="titleHtml"
@mouseenter="createUrl('html')"
>
<i class="fa iconfont icon-HTML-fill"></i>
<span>Save as .html</span>
</a>
const createUrl = (mode) => {
if (mode === "md") {
//下载.md文件
const val = store.rawContent;
const blobObj = new Blob([val]);
const objectURL = URL.createObjectURL(blobObj);
mdDataUrl.value = objectURL;
} else {
//下载.html文件
const val = store.previewContent;
const blobObj = new Blob([val]);
const objectURL = URL.createObjectURL(blobObj);
htmlDataUrl.value = objectURL;
}
};
文件拖动打开
拖拽事件:'drop',当文件拖动到inputArea时触发,此时需要读取文件内容 通过event.dataTransfer.files获取拖入文件列表。 FileReader对象:读取dataTransfer文件,并在读取完成后(onload)覆盖界面内容。
读取文件内容后,首先判断文件数量(至多1个),再判断文件类型(只能为md文件),若成功则覆盖inputArea的内容。不成功时使用elementPlus组件库中的message发出提醒。 代码实现:
// 文件拖动
const dragging = (e) => {
const fileData = e.dataTransfer.files;
// console.log(e.dataTransfer.files);
if (fileData.length > 1) {
ElMessage({
message: "请一次拖入1个文件",
type: "warning",
});
} else if (fileData[0].name.slice(-3) !== ".md") {
ElMessage({
message: "文件类型不匹配, 应为(*.md)",
type: "warning",
});
} else {
ElMessage({
message: "导入成功",
type: "success",
});
const fileReader = new FileReader();
fileReader.readAsText(fileData[0], "UTF-8");
fileReader.onloadend = (e) => {
const newContent = e.target.result;
store.contentChange(newContent);
};
}
};
项目部署
后续可能
做完项目之后,收获还是挺大的,在github上面第一次实现其他人的项目还是很有挑战的,后续也会继续学习,多多实践!🦀🦀
转载自:https://juejin.cn/post/7242237897993109563