前端开发经常会用到富文本编辑器,比如CKEditor,动不动一个库几十M的代码量,其中涉及许多你可能用不到的功能特性和相关设置,CKEditor最新版本的代码仓库就有接近2000个JS文件,300,000行代码。
可是如果你只需要一个简易版的编辑器,真的值得引入这么一个庞大的库吗?
今天我们从实现一个简易版的编辑器带大家了解一下其背后涉及到的原理。
开始
这个编辑器将要使用到markdown
:一个简洁语法并且自带样式的语言,而且远比纯HTML的输入输出要安全得多。
首先,我们需要一些依赖包。 @ts-stack/markdown 和 turndown,@ts-stack/markdown
是用来将markdown语法转化为HTML代码显示用的,而turndown
是将HTML代码转化为markdown语言。
接下来,创建一个基础的Vue组件,命名为WysiwygEditor.vue
,在组件中添加一个div元素,并且将它的contenteditable
属性设置为true
,然后添加一些Tailwind样式去美化一下。
<!-- WysiwygEditor.vue --> <template> <div> <div @input="onInput" v-html="innerValue" contenteditable="true" class="wysiwyg-output outline-none border-2 p-4 rounded-lg border-gray-300 focus:border-green-300" /> </div> </template> <script> export default { name: 'WysiwygEditor', props: ['value'], data() { return { innerValue: this.value } }, methods: { onInput(event) { this.$emit('input', event.target.innerHTML) } } } </script>
然后使用该组件:
<!-- Some other component --> <template> <!-- ... --> <wysiwyg-editor v-model="someText" /> <!-- ... --> </template> <!-- ... -->
看起来像这样
现在这个div元素的样式看起来像textarea
标签的效果了。
让文本变为富文本
在编辑器的上面会有一些带有bold,italic,underlined,headings,lists等文本的编辑按钮。并且上面会有对应功能的图标。可以通过安装fontawesome icon来实现。然后对按钮进行一些样式设置。
.button { @apply border-2; @apply border-gray-300; @apply rounded-lg; @apply px-3 py-1; @apply mb-3 mr-3; } .button:hover { @apply border-green-300; }
先将这些按钮添加鼠标点击后的监听方法,后面我们会去实现每一个方法里的具体执行。
<!-- WysiwygEditor.vue --> <template> <!-- ... --> <div class="flex flex-wrap"> <button @click="applyBold" class="button"> <font-awesome-icon :icon="['fas', 'bold']" /> </button> <button @click="applyItalic" class="button"> <font-awesome-icon :icon="['fas', 'italic']" /> </button> <button @click="applyHeading" class="button"> <font-awesome-icon :icon="['fas', 'heading']" /> </button> <button @click="applyUl" class="button"> <font-awesome-icon :icon="['fas', 'list-ul']" /> </button> <button @click="applyOl" class="button"> <font-awesome-icon :icon="['fas', 'list-ol']" /> </button> <button @click="undo" class="button"> <font-awesome-icon :icon="['fas', 'undo']" /> </button> <button @click="redo" class="button"> <font-awesome-icon :icon="['fas', 'redo']" /> </button> </div> <!-- ... --> </template> <!-- ... -->
编辑器现在看起来是这样了
现在看起来是不是越来越接近了。还缺少按钮动作的执行方法。这里要用到document.execCommand
,虽然MDN已经宣称将废弃该特性,但是大部分浏览器仍然支持。我们暂且还是使用它。
让我们通过它来实现applyBold方法
methods: { // ... applyBold() { document.execCommand('bold') }, // ... }
非常简洁明了,同样,我们来实现其它方法
// ... applyItalic() { document.execCommand('italic') }, applyHeading() { document.execCommand('formatBlock', false, '<h1>') }, applyUl() { document.execCommand('insertUnorderedList') }, applyOl() { document.execCommand('insertOrderedList') }, undo() { document.execCommand('undo') }, redo() { document.execCommand('redo') } // ...
这里唯一需要说明的是applyHeading
,因为我明确需要在此处指定所需的元素。使用这些命令后,可以预先对输出的元素标签进行一些样式设置
.wysiwyg-output h1 { @apply text-2xl; @apply font-bold; @apply pb-4; } .wysiwyg-output p { @apply pb-4; } .wysiwyg-output p { @apply pb-4; } .wysiwyg-output ul { @apply ml-6; @apply list-disc; } .wysiwyg-output ol { @apply ml-6; @apply list-decimal; }
有了一定样式后,在输入框中输入一些内容
为了使得更美观一点,把空行用空的段落标签代替,以回车结束的内容归为一个段落
// ... data() { return { innerValue: this.value || '<p><br></p>' } }, mounted() { document.execCommand('defaultParagraphSeparator', false, 'p') }, // ...
添加markdown支持
如果我想直接在编辑器里写markdown语法,暂时还不支持
# Hello, world! **Lorem ipsum dolor** _sit amet_ * Some * Unordered * List 1. Some 1. Ordered 1. List
结果看起来是这样
完全没有任何样式。别忘了,前面我们安装了@ts-stack/markdown
库,现在可以使用了
import { Marked } from '@ts-stack/markdown' export default { name: 'WysiwygEditor', props: ['value'], data() { return { innerValue: Marked.parse(this.value) || '<p><br></p>' } }, // ...
我们把输入的内容markdown语法转化为HTML代码之后,看起就正常了
同时还需要在组件传出本文编辑器数据的时候,进行转化,这里要用到前面安装的turndown
import TurndownService from 'turndown' export default { // ... methods: { onInput(event) { const turndown = new TurndownService({ emDelimiter: '_', linkStyle: 'inlined', headingStyle: 'atx' }) this.$emit('input', turndown.turndown(event.target.innerHTML)) }, // ...
让我们把编辑器中输入的markdown语法文本在页面中通过模板输出后的效果
<!-- Some other component --> <template> <!-- ... --> <wysiwyg-editor v-model="someText" /> <pre class="p-4 bg-gray-300 mt-12">{{ someText }}</pre> <!-- ... --> </template>
同步输入输出,内容是一致的,没有任何问题
看起来一切正常,达到了我们想要的效果。下面是全部的代码
<template> <div> <div class="flex flex-wrap"> <button @click="applyBold" class="button"> <font-awesome-icon :icon="['fas', 'bold']" /> </button> <button @click="applyItalic" class="button"> <font-awesome-icon :icon="['fas', 'italic']" /> </button> <button @click="applyHeading" class="button"> <font-awesome-icon :icon="['fas', 'heading']" /> </button> <button @click="applyUl" class="button"> <font-awesome-icon :icon="['fas', 'list-ul']" /> </button> <button @click="applyOl" class="button"> <font-awesome-icon :icon="['fas', 'list-ol']" /> </button> <button @click="undo" class="button"> <font-awesome-icon :icon="['fas', 'undo']" /> </button> <button @click="redo" class="button"> <font-awesome-icon :icon="['fas', 'redo']" /> </button> </div> <div @input="onInput" v-html="innerValue" contenteditable="true" class="wysiwyg-output outline-none border-2 p-4 rounded-lg border-gray-300 focus:border-green-300" /> </div> </template> <script> import { Marked } from '@ts-stack/markdown' import TurndownService from 'turndown' export default { name: 'WysiwygEditor', props: ['value'], data() { return { innerValue: Marked.parse(this.value) || '<p><br></p>' } }, mounted() { document.execCommand('defaultParagraphSeparator', false, 'p') }, methods: { onInput(event) { const turndown = new TurndownService({ emDelimiter: '_', linkStyle: 'inlined', headingStyle: 'atx' }) this.$emit('input', turndown.turndown(event.target.innerHTML)) }, applyBold() { document.execCommand('bold') }, applyItalic() { document.execCommand('italic') }, applyHeading() { document.execCommand('formatBlock', false, '<h1>') }, applyUl() { document.execCommand('insertUnorderedList') }, applyOl() { document.execCommand('insertOrderedList') }, undo() { document.execCommand('undo') }, redo() { document.execCommand('redo') } } } </script>
结论
只需要87行代码便实现了一个简易的富文本编辑器。虽然功能还是太简单,但是最起码我们知道了实现一个富文本编辑器后面的原理。后面需要增加功能就不是什么难事了。