为 Vue 构建自己的所见即所得 Markdown 编辑器 📝👀 入门 将“富文本”放入“富文本” 添加 Markdown 总结想法

2025-05-28

为 Vue 构建你自己的所见即所得 Markdown 编辑器 📝👀

入门

将“富”融入“富文本”

添加 markdown




总结

HTML5 和现代 JavaScript 让很多事情比过去简单得多。复杂的事情不再需要大量的 hack,很多功能都是现成的。

市面上有很多现成的所见即所得 (WYSIWYG,所见即所得,又称“富文本”)编辑器,比如CKEditor。它们提供了大量的功能以及各种框架的指南、特性和插件,但它们的代码库通常非常庞大。我的意思是,CKEditor 5 的代码库里有大约 2000 个 JS 文件,总计约 30 万行代码——这真是令人难以置信,不是吗?

而且可能没有必要:大多数用例不需要 PDF 或 Word 导出、实时协作、数学和化学集成、修订、自动创建参考书目或功能齐全的 Excel 克隆。当您只需要一些基本的文本编辑功能时,为什么不一次性构建自己的所见即所得编辑器呢?

在这篇文章中,我将解释如何为 Vue 创建自己的所见即所得的 markdown 编辑器!

入门

该编辑器将使用 markdown:它是一种简单的语法,可以按照我想要的方式设置样式,并且比纯 HTML 更安全地保存和再次输出。

首先,我需要几个包,分别是@ts-stack/markdownturndown,它们分别@ts-stack/markdown用来将 Markdown 显示为 HTML 和turndown将 HTML 转换回 Markdown。

接下来,我创建一个支持 的基本 Vue 组件v-model并将其命名为WysiwygEditor.vue。我已经可以在这里使用<div>带有 属性 的 了contenteditable。我还添加了一些 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>


Enter fullscreen mode Exit fullscreen mode

现在可以像这样使用该组件:



<!-- Some other component -->
<template>
  <!-- ... -->
  <wysiwyg-editor v-model="someText" />
  <!-- ... -->
</template>
<!-- ... -->


Enter fullscreen mode Exit fullscreen mode

这看起来像这样:

所见即所得的编辑器,采用 Tailwind 风格

现在,div 的行为基本上与 a 类似,textarea但有一点不同:它会生成 HTML。

将“富”融入“富文本”

你可能知道一些按钮,它们可以用来给文本加粗、斜体或加下划线,以及在 Google Docs 或 Word 等程序中添加列表、标题等。接下来,我们来添加这些按钮。为此,我安装了Fontawesome 图标,并将这些按钮添加到 textarea-div 的正上方。但首先:一些样式:



.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;
}


Enter fullscreen mode Exit fullscreen mode

我稍后会添加点击监听器并实现所使用的方法。



<!-- 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>
<!-- ... -->


Enter fullscreen mode Exit fullscreen mode

编辑器现在看起来像这样:

所见即所得编辑器,现在带有按钮。

太棒了!现在我需要给这个东西添加实际功能。为此,我将使用document.execCommand,它或多或少是为创建所见即所得的编辑器而设计的。尽管 MDN 声明此功能已被弃用,但大多数浏览器仍然提供一些支持,因此对于最基本的功能来说,它应该仍然可以使用。

让我们实现这个applyBold方法:



methods: {
  // ...

  applyBold() {
    document.execCommand('bold')
  },

  // ...
}


Enter fullscreen mode Exit fullscreen mode

好的,这很简单。现在剩下的:



  // ...

  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')
  }

  // ...


Enter fullscreen mode Exit fullscreen mode

这里唯一弹出的方法是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;
}


Enter fullscreen mode Exit fullscreen mode

完成的编辑器(包含一些示例内容,如下所示:

带有示例内容的编辑器

为了让事情表现得更好一些,我还需要将空段落设置为空内容的默认值,并将默认的“换行符”也设置为段落:



  // ...
  data() {
    return {
      innerValue: this.value || '<p><br></p>'
    }
  },

  mounted() {
    document.execCommand('defaultParagraphSeparator', false, 'p')
  },
  // ...


Enter fullscreen mode Exit fullscreen mode

添加 markdown

所以,我想把 Markdown放进编辑器,然后再从编辑器里取出 Markdown 。我先定义一些 Markdown 字符串,看看会发生什么:



# Hello, world!

**Lorem ipsum dolor** _sit amet_

* Some
* Unordered
* List


1. Some
1. Ordered
1. List


Enter fullscreen mode Exit fullscreen mode

所见即所得的编辑器,带有无格式的 Markdown

是的,什么也没发生。还记得@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>'
    }
  },

// ...


Enter fullscreen mode Exit fullscreen mode

现在输入将被呈现为 HTML:

在所见即所得编辑器中格式化 Markdown

太棒了!现在,为了从组件中获取 Markdown 我使用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))
    },
// ...


Enter fullscreen mode Exit fullscreen mode

让我们通过在预先格式化的 div 中输出我们收到的 markdown 来查看它是否有效:



<!-- Some other component -->
<template>
  <!-- ... -->
  <wysiwyg-editor v-model="someText" />

  <pre class="p-4 bg-gray-300 mt-12">{{ someText }}</pre>
  <!-- ... -->
</template>


Enter fullscreen mode Exit fullscreen mode

太棒了!完成了!我们来测试一下:

添加更多内容并生成 markdown 输出。

Markdown 编辑器的动画版本。

似乎有效!

作为参考,这里是整个组件:



<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>

<span class="nt">&lt;div</span>
  <span class="err">@</span><span class="na">input=</span><span class="s">"onInput"</span>
  <span class="na">v-html=</span><span class="s">"innerValue"</span>
  <span class="na">contenteditable=</span><span class="s">"true"</span>
  <span class="na">class=</span><span class="s">"wysiwyg-output outline-none border-2 p-4 rounded-lg border-gray-300 focus:border-green-300"</span>
<span class="nt">/&gt;</span>
Enter fullscreen mode Exit fullscreen mode

</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>

Enter fullscreen mode Exit fullscreen mode




总结

这很有趣。87 行 Vue 代码就能实现一个所见即所得的编辑器,相当小巧。组件的行为类似于输入框,v-model这更加方便。在我看来,对于一个业余项目来说,这个编辑器足以应付内容不多的小规模情况。

不过,在客户项目中,我更倾向于使用现成的解决方案,因为它更易于维护,功能更强大,支持也更好。不过,构建这个项目本身就是一次很棒的学习机会!


希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下❤️🦄 !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。

如果您想支持我的努力, 请给我买杯咖啡 在 Twitter 上关注我🐦

给我买个咖啡按钮

文章来源:https://dev.to/thormeier/build-your-own-wysiwyg-markdown-editor-for-vue-318j
PREV
⚠️ 不要在家尝试这个:CSS 作为后端 - 引入级联服务器表!
NEXT
如何在几分钟内用 Vue 构建一个桌面应用程序🛠️ 安装💻请求 API🌟 就是这样!Desktop Météo