Skip to content
On this page

用vue实现markdown编辑器的思路

以下均只考虑在chrome 64版本下

目录

  • 编辑器的实现方式及原理
  • 从剪切板上传图片遇到的坑

编辑器的实现方式及原理

1.实现方式

  • 扩展textarea
    • github的issue如此实现,本博客也是
    • 特点:涉及的api少且简单,能满足基本需求;编辑时样式不可调;无法做行号高亮等
  • 利用div设置contenteditable
    • 复杂的富文本基本都是基于此实现,例如github的readme编辑器
    • 涉及selectionrange对象

2.富文本需要实现的基本功能

  • 选中tab缩进(向后或向前)
  • 代码及代码块快捷键
  • 粘贴图片上传
  • 超链接

3.实现原理

**1.在textarea中 ** ,value就是一个字符串,所有的操作都是基于这个字符串去做切割与拼接。涉及到的api:

// 获取选中的字符
window.getSelection().toString()
// 获取选中字符的起始位置
dom.selectionStart
dom.selectionEnd
// 根据起始位置和方向设置字符选中
dom.setSelectionRange(start, end, selectionDirection) // 控制字符的选择的方向

在写技术型文章时,缩进是最常用的,那有了以上api,如何实现?
我们可以假设一段代码如下:

js
// \s代表空格 \n代表换行
var obj = {\n
\s\sname: 'jmingzi',\n
\s\sage: ''\n
}

首先我们要获取到选中的字符 ,即selectedString

当我们向后缩进这块代码时,实际上就是在每一行前加2个tab,利用replace

indent = '  '
indent + selectedString.replace(/\n/g, '\n' + indent)

当我们向前缩进时,实际上也是去掉每一行前的2个tab,同理

// 替换行首2个tab
result = selectedString.replace(/^\s{2}/, '')
// 替换剩余换行前的tab
result = result.replace(/\n\s{2}/g, '\n')

最后,我们需要保持选中的代码块,即可。

dom.setSelectionRange(
  selectionStart,
  selectionStart + result.length
)

需要注意的是,在vue中,利用:value绑定的textarea值,设置setSelectionRange时,需要等到视图更新后。

2.利用div,需要了解的知识点:

  • selection和range,利用selection可以获取当前选中的字符或range对象,利用range对象可以选取节点,增加选取的范围。
// selection很显然代表选中的范围,它是一个集合
// range代表html片段
// selection 对象可以包含多个range,因为在一个页面中可以选中多个range
  • execCommand

参考

3.还有一种方式,就是利用textarea和div重合,在textarea中编写文本,覆盖一层div,利用

pointer-event: none;

来使textarea可以继续编辑,在渲染后与textarea重合,从而达到高亮效果,唯一需要注意的是,滚动的时候需要同步。

从剪切板上传图片遇到的坑

有2种方式监听到paste粘贴事件:

  • 全局window上
  • 某一节点上documentbodydiv

事件中有一个clipboardData属性,在chrome中:

5b00ed2517d009726f213519

clipboardData包含

属性类型说明
dropEffectStringnone
effectAllowedStringuninitialized
filesFileList和input获取图片数据时的结构一样,为File对象的集合;当且仅当复制的为文件时才有值。
itemsDataTransferItemList复制的对象列表,包含了3个方法add remove clear
typesArray['Files', 'String']

File对象5b00eedc44d9041d3f1d677d

DataTransferItemList对象

你会发现在控制台打印

console.log(e.clipboardData.items)

看到的永远是空的,像这样

{
  length: 0,
  __proto__: DataTransferItemList
}

但是又能看到一些信息,比如这样

DataTransferItemList {0: DataTransferItem, 1: DataTransferItem, 2: DataTransferItem, length: 3}

说明在某种情况下控制台取不到该属性?所以我们得循环取值(注意它并不是iterator对象,不可直接遍历),每一个item由如下组成 5b00f27c9f54543b31afb3ec 其中每个item包含了3个方法,我们就可以对复制的内容作更改删除操作。

针对file,我们可以直接从FileList对象中获取,也可以用getAsFile来获取,都是得到的File对象。对于File对象,可以用FileReader对象来读取

js
const rdr = new FileReader()
rdr.onloadend = () => {
  this.upload(rdr.result)
}
rdr.readAsDataURL(File)

5b00f7d2ac502e5dedd8486e

针对string,有getAsString方法,返回的结果用回调来接收。

最后,利用vue的事件处理,我们可以很简单的组合按键事件

// 输入
@input="update"
// 粘贴
@paste="paste"
// tab键
@keydown.9.exact.prevent="tab($event, true)"
// tab缩进
@keydown.shift.exact.9.prevent="tab($event, false)"

这样,我们就可以在chrome下实现markdown编辑器的一系列基础功能。

参考

最后实现的编辑器看起来是这样的

5b00faad17d009726f21a13a