[图片] 下一代的 Markdown 编辑器,为未来而构建 [图片] [图片] [图片] [图片] [图片] [图片] 简介 Vditor 是一款浏览器端的 Markdown 编辑器,使用 TypeScript 实现。支持原生 JavaScript、Vue、React、Angular。 ️ 背景 我们在开发 Sym 的 ..

一款浏览器端的 Markdown 编辑器

Vditor
下一代的 Markdown 编辑器,为未来而构建

npm bundle size


💡 简介

Vditor 是一款浏览器端的 Markdown 编辑器,使用 TypeScript 实现。支持原生 JavaScript、Vue、React、Angular。

📽️ 背景

我们在开发 Sym 的初期是直接使用 WYSIWYG 富文本编辑器的。那时候基于 HTML 的编辑器非常流行,项目中引用起来也很方便,也符合用户当时的使用习惯。

后来,Markdown 的崛起逐步改变了大家的排版方式。再加上我们其他几个项目都是面向程序员用户的,所以迁移到 md 上也是大势所趋。我们选择了 CodeMirror,这是一款优秀的编辑器,它对开发者提供了丰富的编程接口,对各种浏览器的兼容性也比较好。

再后来,随着我们项目业务需求方面的沉淀,使用 CodeMirror 有时候会感到比较“笨重”。比如要实现 @自动完成用户名列表、插入 Emoji、上传文件等就需要比较深入的二次开发,而这些业务需求恰恰是很多项目场景共有且必备的。

终于,我们决定开始在 Sym 中自己实现编辑器。随着几个版本的迭代,Sym 的编辑器也日趋成熟。在我们运营的社区黑客派上陆续有人问我们是否能将编辑器单独抽离出来提供给大家使用。与此同时,我们的前端主程 V 同学对于维护分散在各个项目中的编辑器也感到有点力不从心,外加对 TypeScript 的好感,所以就决定使用 ts 来实现一个全新的浏览器端 md 编辑器。

于是,Vditor 就这样诞生了。

✨ 特性

demo
render

🗃 案例

🛠️ 使用文档

CommonJS

npm install vditor --save
import Vditor from 'vditor'
import "~vditor/src/assets/scss/classic" // 或者使用 dark

const vditor = new Vditor(id, {options...})
vditor.focus()

HTML script

<!-- ⚠️生产环境请指定版本号,如 https://cdn.jsdelivr.net/npm/vditor@x.x.x/dist... -->
<!-- 可使用 index.dark.css 或 index.classic.css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vditor/dist/index.classic.css" />
<script src="https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js" defer></script>
const vditor = new Vditor(id, {options...})
vditor.focus()

示例代码

CSS

在 DOM 元素上添加 class="vditor-reset" 属性可对内容进行更为友好的展示。

API

options

说明 默认值
cache 是否使用 localStorage 进行缓存 true
height 编辑器总高度 'auto'
width 编辑器总宽度,支持 % 'auto'
placeholder 输入区域为空时的提示 ''
lang 多语言:en_US, zh_CN 'zh_CN'
counter 计数器 0
input 输入后触发 (value: string, previewElement?: HTMLElement): void -
focus 聚焦后触发 (value: string): void -
blur 失焦后触发 (value: string): void -
esc esc 按下后触发 (value: string): void -
ctrlEnter ⌘/ctrl+enter 按下后触发 (value: string): void -
select 编辑器中选中文字后触发 (value: string): void -
tab tab 键操作字符串,支持 \t 及任意字符串 -
typewriterMode 是否启用打字机模式 false

options.toolbar

{  
 hotkey: '⌘-⇧-f',  
 name: 'format',  
 tipPosition: 'ne',  
 tip: 'format',  
 icon: '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="768" height="768" viewBox="0 0 768 768"><path d="M342 426v-84h426v84h-426zM342 256v-86h426v86h-426zM0 0h768v86h-768v-86zM342 598v-86h426v86h-426zM0 214l170 170-170 170v-340zM0 768v-86h768v86h-768z"></path></svg>',  
 click: () => {  
   alert('custom toolbar')  
 },  
}  
说明 默认值
name 唯一标示 -
icon svg 图标 -
tip 提示 -
tipPosition 提示位置:ne, nw -
hotkey 快捷键,支持 ⌘/ctrl-key⌘/ctrl-⇧/shif-key 格式的配置 -
suffix 插入编辑器中的后缀 -
prefix 插入编辑器中的前缀 -
click 自定义按钮点击时触发的事件 ():viod -

options.preview

说明 默认值
delay 预览 debounce 毫秒间隔 1000
mode 显示模式:'both', 'editor', 'preview' 'both'
parse 预览回调 (element: HTMLElement): void -
url md 解析请求 -

options.preview.hljs

说明 默认值
enable 是否启用代码高亮 true
style 可选值参见 Chroma 'github'

options.hint

说明 默认值
delay 提示 debounce 毫秒间隔 200
emoji 默认表情,可从 lute/emoji_map 中选取,也可自定义 { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' }
emojiTail 常用表情提示 -
emojiPath 表情图片地址 https://cdn.jsdelivr.net/npm/vditor/src/assets/emoji
at @用户回调,(value: string): Array,需同步返回数组[{value: '', html: ''}] -

options.upload

// POST data  
xhr.send(formData);  // formData = FormData.append("file[]", File)  
// return data  
{  
 "msg": "",  
 "code": 0,  
 "data": {  
 "errFiles": ['filename', 'filename2'],  
 "succMap": {  
   "filename3": "filepath3",  
   "filename3": "filepath3"  
   }  
 }  
}  
// POST data  
xhr.send(JSON.stringify({url: src})); // src 为站外图片地址  
// return data  
{  
 msg: '',  
 code: 0,  
 data : {  
   originalURL: '',  
   url: ''  
 }  
}  
说明 默认值
url 上传 url,查看规范 ''
max 上传文件最大 Byte 10 * 1024 * 1024
linkToImgUrl 剪切板中包含图片地址时,使用此 url 重新上传 ''
success 上传成功回调 (editor: HTMLPreElement, msg: string): void -
error 上传失败回调 (msg: string): void -
token CORS 上传验证,头为 X-Upload-Token -
filename 文件名安全处理 (name: string): string name => name.replace(/\W/g, '')
accept 文件上传类型,同 input accept -
validate 校验,成功时返回 true 否则返回错误信息 (files: File[]) => string | boolean -
handler 自定义上传,当发生错误时返回错误信息 (files: File[]) => string | null -
format 对服务端返回的数据进行转换,以满足内置的数据结构 (files: File[], responseText: string): string -

options.resize

说明 默认值
enable 是否支持大小拖拽 false
position 拖拽栏位置:top, bottom 'bottom'
after 拖拽结束的回调 (height: number): void -

options.classes

说明 默认值
preview 预览元素上的 className ''

options.keymap

说明 默认值
deleteLine 删除光标所在行或选中的行 ⌘-Backspace
duplicate 复制当前行或选中的内容 ⌘-d

methods

说明
getValue() 获取编辑器内容
getHTML() 获取预览区内容。该方法需使用异步编程
insertValue(value: string) 在焦点处插入内容
focus() 聚焦到编辑器
blur() 让编辑器失焦
disabled() 禁用编辑器
enable() 解除编辑器禁用
setSelection(start: number, end: number) 选中从 start 开始到 end 结束的字符串
getSelection():string 返回选中的字符串
setValue(value: string) 设置编辑器内容
renderPreview(value?: string) 设置预览区域内容
getCursorPosition():{top: number, left: number} 获取焦点位置
deleteValue() 删除选中内容
updateValue(value: string) 更新选中内容
isUploading() 上传是否还在进行中
clearCache() 清除缓存
disabledCache() 禁用缓存
enableCache() 启用缓存
html2md(value: string) HTML 转 md。该方法需使用异步编程
tip(text:string, time:number) 消息提示。time 为 0 将一直显示
setPreviewMode(mode: string) 设置预览模式。mode: 'both', 'editor', 'preview'

static methods

Vditor.mermaidRender(document)  
import VditorPreview from 'vditor/dist/method.min'  
VditorPreview.mermaidRender(document)  
element: HTMLTextAreaElement    // 为保证 Markdown 的完整性,请将其包裹在 textarea 或 pre 中  
options?: IPreviewOptions {  
 hljsStyle?: string;    // 高亮样式,默认为 'atom-one-light'  
 enableHighlight?: boolean;    // 是否需要代码高亮,默认为 true  
 customEmoji?: { [key: string]: string };    // 自定义 emoji,默认为 {}  
 lang?: (keyof II18nLang);    // 语言,默认为 'zh_CN'  
 emojiPath?: string;    // 表情图片路径  
}  
说明
mathRender(element: HTMLElement) 转换 element 中的文本为数学公式
mermaidRender(element: HTMLElement) 转换 element 中的文本为流程图/时序图/甘特图
codeRender(element: HTMLElement, lang: (keyof II18nLang) = "zh_CN") 为 element 中的代码块添加复制按钮
chartRender(element: (HTMLElement | Document) = document) 图表渲染
abcRender(element: (HTMLElement | Document) = document) 五线谱渲染
md2html(mdText: string, options?: IPreviewOptions): string markdown 文本转换为 HTML,该方法需使用异步编程
preview(element: HTMLTextAreaElement, options?: IPreviewOptions) 页面 Markdown 文章渲染
highlightRender(hljsStyle: string, enableHighlight: boolean, element?: HTMLElement | Document) 为 element 中的代码块进行高亮渲染
mediaRender(element: HTMLElement) 特定链接分别渲染为视频、音频、嵌入的 iframe
mathRenderByLute(element: HTMLElement) 对使用 Lute 渲染的数学公式进行渲染

升级

CHANGELOG

🏗 开发文档

  1. 安装 node LTS 版本
  2. 下载 最新代码并解压
  3. 根目录运行 npm install
  4. npm run start 启动本地服务器,打开 http://localhost:9000
  5. 修改代码
  6. npm run build 打包代码到 dist 目录

⚠️ 注意事项

🏘️ 社区

📄 授权

Vditor 使用 MIT 开源协议。

🙏 鸣谢

  • Vditor

    Vditor 是一款浏览器端的 Markdown 编辑器,使用 TypeScript 实现。

    27 引用 • 254 回帖 • 12 关注
  • Markdown

    Markdown 是一种可以使用普通文本编辑器编写的标记语言,通过类似 HTML 的标记语法,它可以使普通文本内容具有一定的格式,具体语法请参考 Markdown 教程

    89 引用 • 526 回帖 • 637 关注
  • 编辑器
    25 引用 • 202 回帖 • 1 关注
  • 开发指南
    5 引用 • 192 回帖
Vanessa 在 2019-06-19 15:47:05 更新了该帖
Vanessa 在 2019-08-28 11:17:51 更新了该帖
Vanessa 在 2019-08-28 11:48:41 更新了该帖
优质回帖
  • Vanessa 1

    前端是这样传送的

        const formData = new FormData();
        for (let i = 0, iMax = uploadFileList.length; i < iMax; i++) {
            formData.append("file[]", uploadFileList[i]);
        }
         xhr.send(formData);
    
  • haaid 1

    文字错误:接触编辑器禁用,「接触」->「解除」

  • gitors 1

    此链接 404

80 回帖
请输入回帖内容...
  • someone11732 1

    在焦点处插入内容应该是 insertValue 吧

    1 回复
  • Vanessa
    1 回复
  • someone11732

    哈哈,Vditor 很棒!

    1 回复
  • Vanessa

    谢谢支持

    1 回复
  • someone11732

    晚点也在我的项目上试试,现在的 markdown 编辑器不是功能不全就是 bug 多或者不维护

    1 回复
  • Vanessa

    本来早就想抽出来的,要不在 B3log 开源的几个项目中维护很头痛。以后只需要维护一个,然后升级以下就好了。

  • Today

    终于等到你😄

    1 回复
  • KevinBlandy

    编辑器现在markdownhtml的转换是由后端完成的吗?是否有计划实现让前端渲染,并且提供获取到渲染后 html 的接口。这样后端可以直接保存 html 数据。

    很不错的编辑器,用着舒服。

    2 回复
  • 88250

    暂时没有前端渲染 md 的计划,主要是前端渲染会有些问题:

    1. 有的业务逻辑是要后端查库的,比如渲染投票
    2. 安全校验需要后端来做,只靠前端会被绕过
    3. 后端渲染才能保证最终效果的一致

    但前端可以做一些格式方面的功能,比如 md 格式化、Lint、语法检查。我们正在用 golang 写一个 md 引擎(项目代号 Lute),打算通过 WASM 把这样的功能提供给 JS,在前端做文本格式方面的辅助,让用户可以更好地使用 Vditor。

    眼目前,做后端 Markdown 渲染 HTML 可以用 marked(或者用我们包装的 http-marked),这是我们目前测试下来渲染 md 最好的库。

    1 回复
  • Vanessa

    过年的时候都没给自己放假 😭

  • An

    大 V 都开始写编辑器了

    1 回复
  • Vanessa

    再开一个新坑。一个萝卜一个坑。

  • KevinBlandy

    在文本数据比较大的情况下,每一次的输入事件。都要把整个 markdown 文本提交给后台解析。这样是不是有点儿伤哦?其实我自己的想法非常简单。

    • 前端直接就完成了 markdownhtml 的转换,并且提供 API 获取到 html 数据。
    • 后端保存数据的时候,保存 html 以及 markdown 数据。
    • 查看文章的时候,从 DB 读取和渲染 html
    • 编辑文章的时候,从 DB 读取 markdown 渲染到编辑器。
    • Xss 安全问题,可以用 jsoup 来过滤 html 中的非法 标签属性 等。

    我这种想法有啥问题么?D 哥指教一下。

    1 回复
  • 88250

    1. 预览和最终渲染效果会不一致,比如前端有的标签、属性没过滤而后端过滤了
    2. 不安全的 HTML 一旦入库,后面要更新就比较麻烦了

    所以我觉得后端渲染更好,性能问题不用太过担心,目前黑客派上面我还没发现 md 渲染慢,即使是边打字边看实时预览的情况。这一点其实很好理解,现在连编程语言的自动完成都可以通过后端语言服务器来做,更何况 md 这样语法简单的文本。

  • q2484877

    为啥预览窗口没作用啊?是少了什么东西吗?

    png

  • Vanessa

    上传和 MD 解析的接口用的是第三方的。后期会内置 MD 解析。请关注 https://github.com/b3log/vditor/issues/9

  • q2484877

    后端渲染的时候,时序图和流程图没法渲染啊,你们是怎么解决的? @Vanessa @88250

    1 回复
  • 88250

    这部分是在前端做的,后端 Markdown 只会渲染为代码块。

    1 回复
  • q2484877

    能告知渲染代码在哪吗?实在是找不到😂

    1 回复
  • someone39364

    萌新问个问题,为什么我引入了 js 和 CSS,也初始化了,但是页面没有出来效果呢,是缺少什么依赖吗?

    1 回复
  • Vanessa 1

    看一下错误信息

    2 回复
  • someone39364

    显示未定义,image.png

  • someone39364

    谢谢。知道了,我的错,我没有给 id 加引号😂

  • Eddie

    好像渲染有点慢,是不是文本变化触发监听的事件不太对?

    1 回复
  • Vanessa

    是预览区的渲染,还是页面刷新后编辑器的渲染?

    1 回复
  • Eddie

    预览区啊,

    1 回复
  • Vanessa

    设置了 500 毫秒的延迟

    1 回复
  • Eddie

    这样啊,怪不得。。。

    1 回复
  • Vanessa

    默认是 1s。你的感知很灵敏呀

  • darren 2

    目前正打算使用 vditor,但是发现一些地方不顺手,所以应该算是提出一些建议吧:

    • 服务器渲染预览时请求头的 content-type 希望能设置成 application/json 当前是 text/plain 导致像 SpringMVC 无法直接使用 @RequestBody
    • 文件上传的选择框 允许添加限定选择的文件类型(accept 属性)
    • 希望能添加前端浏览器渲染的功能(这样在有流量限制或者离线断网的环境也能愉快的编辑,别问我为什么有离线使用的情况,那就要问为啥有缓存这个功能呢?哈哈哈)
    • 希望能开放一些 API 供开发者直接调用,如 HTML 转 markdown 的方法(目前只能粘贴的时候转换),这样方便读取页面上原始的一段 HTML,转为 markdown 后直接编辑
    • 编辑器当前是以 div 这 dom 容器作为入口的,所以如果假设原始 div 容器中已经有内容了,希望能直接将原始容器中的 HTML 自动转 markdown 并作为默认值初始化给编辑器
    1 回复
  • Vanessa
  • BrickCat 1 评论

    自定义 ToolBar 时报错了TypeError: this.options.toolbar.forEach is not a function

    toolbar:{
            name:['emoji', 'br', 'bold', '|', 'line']
          },
    
    请如下使用 toolbar: ['emoji', 'br', 'bold', '|', 'line'],
    Vanessa
  • 1412904892

    如果粘贴的时候是一张截图,应该如何处理?需要修改哪个地方

  • brandim

    iOS 移动端使用 WKWebView 或 Safari 无法显示编辑器,即:编辑器的位置是空白图片.png

    1 回复
  • Vanessa

    有没有什么报错?

  • Tracy4ever

    异次元空间是彩蛋吗?发着呆突然就进去了😂

    1 回复
  • Vanessa

    trollface 是的,惊吓到了没?

    2 回复
  • Tracy4ever

    有趣有趣,可惜再也进不去了😭

  • hkpqazwsxedc

    赞!!!!!!

  • Tracy4ever

    想问下图片上传后怎么在输入框显示呢?好像没看到实现拖拽进去显示及上传后显示的功能诶

    1 回复
  • Vanessa

    上传后在输入框显示的代码在 https://github.com/b3log/vditor/blob/master/src/ts/upload/index.ts#L193, 如果使用了 options.upload.success 就需要自己进行上传后的处理了

    2 回复
  • Tracy4ever

    好,我看一下,谢谢😄

  • Tracy4ever

    上传后图片找不到,是不是后台的返回数据格式不对啊?

    1 回复
  • Vanessa

    如果要用内置的上传功能,格式需要如下

    {
    	code: 0,
    	msg: '',
    	errFiles: ['filename', 'filename2']
    	succMap: {
    		'filename': 'filepath',
    		'filename2': 'filepath2',
    	}
    }
    
    1 回复
  • Tracy4ever

    好的啊,filepath 相对路径可以吗?

    1 回复
  • Vanessa

    相对路径比如在 /a/b/c 这样的页面里面会坏吧

  • bugelin

    上传的时候不需要给 input 指定个 name 吗 为啥我 springMVC 会报空指针获取不到 大佬求解

    1 回复
  • Vanessa 1

    前端是这样传送的

        const formData = new FormData();
        for (let i = 0, iMax = uploadFileList.length; i < iMax; i++) {
            formData.append("file[]", uploadFileList[i]);
        }
         xhr.send(formData);
    
请输入回帖内容 ...