实现一个 Markdown 编辑器组件:Vue 3 + Vite + Highlight.js

news2025/4/18 8:41:08

文章目录

  • 一、项目背景与需求分析
  • 二、搭建基础项目
    • 1. 初始化 Vue 3 项目
    • 2. 安装依赖
  • 三、实现 Markdown 编辑器组件
    • 1. 创建 Markdown 编辑器组件
    • 2. 组件说明
  • 四、优化与拓展
    • 1. 自动保存功能
    • 2. 文件上传功能
  • 五、总结

一、项目背景与需求分析

在现代前端开发中,Markdown 编辑器广泛应用于博客、文档、Wiki、代码注释等场景。一个优秀的 Markdown 编辑器需要具备以下功能:

  1. 实时预览:用户输入时,能够看到实时的渲染效果。
  2. 语法高亮:支持 Markdown 语法的实时高亮显示。
  3. 导出功能:支持将编辑的内容导出为 Markdown 或 HTML 格式,方便分享和存档。
  4. 自动保存:避免用户丢失未保存内容。
  5. 文件上传:支持上传文件(如图片),并在 Markdown 内容中嵌入显示。

本文将使用 Vue 3 构建一个简单的 Markdown 编辑器组件,并实现上述基本功能。

二、搭建基础项目

1. 初始化 Vue 3 项目

我们使用 Vite 来初始化 Vue 3 项目,并选择 TypeScript 模板:

npm create vite@latest markdown-editor --template vue-ts
cd markdown-editor

2. 安装依赖

接下来,我们安装实现 Markdown 渲染和语法高亮所需要的依赖:

  • marked:用于将 Markdown 转换为 HTML。
  • highlight.js:用于提供 Markdown 语法的高亮显示。
npm install marked highlight.js

三、实现 Markdown 编辑器组件

1. 创建 Markdown 编辑器组件

src/components 目录下创建 MarkdownEditor.vue 文件,封装 Markdown 编辑器功能。

<template>
  <div class="markdown-editor">
    <textarea
      v-model="content"
      class="editor-textarea"
      placeholder="请输入 Markdown 内容..."
      @input="saveToLocalStorage"
    ></textarea>
    <div class="preview">
      <div v-html="renderedContent" class="preview-content"></div>
    </div>
    <div class="export-buttons">
      <button @click="exportMarkdown">导出 Markdown</button>
      <button @click="exportHTML">导出 HTML</button>
    </div>
    <div class="file-upload">
      <input type="file" @change="handleFileUpload" />
      <p>上传图片并在 Markdown 中显示</p>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue';
import { marked } from 'marked';
import hljs from 'highlight.js';

export default defineComponent({
  name: 'MarkdownEditor',
  setup() {
    const content = ref('');
    const fileUrl = ref<string | null>(null);

    // 从本地存储加载内容
    onMounted(() => {
      const savedContent = localStorage.getItem('markdown-content');
      if (savedContent) {
        content.value = savedContent;
      }
    });

    // 将 Markdown 转换为 HTML 并加上语法高亮
    const renderedContent = computed(() => {
      marked.setOptions({
        highlight: (code: string) => {
          return hljs.highlightAuto(code).value;
        },
      });
      return marked(content.value);
    });

    // 保存内容到本地存储(自动保存)
    const saveToLocalStorage = () => {
      localStorage.setItem('markdown-content', content.value);
    };

    // 导出 Markdown 内容
    const exportMarkdown = () => {
      const blob = new Blob([content.value], { type: 'text/markdown' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = 'document.md';
      link.click();
    };

    // 导出 HTML 内容
    const exportHTML = () => {
      const blob = new Blob([renderedContent.value], { type: 'text/html' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = 'document.html';
      link.click();
    };

    // 文件上传功能
    const handleFileUpload = (event: Event) => {
      const fileInput = event.target as HTMLInputElement;
      const file = fileInput.files ? fileInput.files[0] : null;
      if (file) {
        const reader = new FileReader();
        reader.onload = () => {
          const imageUrl = reader.result as string;
          fileUrl.value = imageUrl; // 存储图片的 URL
          const markdownImage = `![image](${imageUrl})`; // 将图片路径转化为 Markdown 格式
          content.value += `\n\n${markdownImage}\n`; // 插入到 Markdown 内容中
        };
        reader.readAsDataURL(file); // 将图片读取为 Data URL
      }
    };

    return {
      content,
      renderedContent,
      exportMarkdown,
      exportHTML,
      handleFileUpload,
    };
  },
});
</script>

<style scoped>
.markdown-editor {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding: 20px;
}

.editor-textarea {
  width: 100%;
  height: 300px;
  padding: 10px;
  font-size: 16px;
  line-height: 1.5;
  border: 1px solid #ccc;
  border-radius: 8px;
  resize: none;
}

.preview {
  background-color: #f8f8f8;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
  min-height: 300px;
}

.preview-content {
  max-width: 100%;
  word-wrap: break-word;
}

.export-buttons {
  display: flex;
  gap: 10px;
}

button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}

.file-upload {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

input[type="file"] {
  padding: 5px;
}
</style>

2. 组件说明

  • Markdown 渲染:通过 marked 将用户输入的 Markdown 内容转换为 HTML。
  • 语法高亮:使用 highlight.js 对代码块进行高亮显示,增强代码阅读性。
  • 导出功能:支持将 Markdown 或 HTML 内容导出为文件,方便用户保存或分享。
  • 自动保存功能:使用 localStorage 自动保存 Markdown 内容,避免用户丢失未保存的输入。
  • 文件上传支持:允许用户上传图片文件,并将图片插入到 Markdown 内容中,生成合适的 Markdown 格式。

四、优化与拓展

1. 自动保存功能

通过 localStorage 实现了自动保存功能。每次用户输入内容时,Markdown 编辑器的内容会自动存储到浏览器的本地存储中。当用户刷新页面或重新加载时,保存的内容会自动恢复。这样能有效避免用户因刷新或浏览器崩溃导致的数据丢失。

// 保存内容到本地存储(自动保存)
const saveToLocalStorage = () => {
  localStorage.setItem('markdown-content', content.value);
};

2. 文件上传功能

我们通过 HTML5 的 FileReader API 实现了文件上传支持。当用户上传图片文件时,图片会转换为 Data URL,并自动插入到 Markdown 内容中。Markdown 内容将以 ![image](image-url) 格式显示上传的图片。

// 文件上传功能
const handleFileUpload = (event: Event) => {
  const fileInput = event.target as HTMLInputElement;
  const file = fileInput.files ? fileInput.files[0] : null;
  if (file) {
    const reader = new FileReader();
    reader.onload = () => {
      const imageUrl = reader.result as string;
      fileUrl.value = imageUrl; // 存储图片的 URL
      const markdownImage = `![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%24%7BimageUrl%7D&pos_id=img-QEVKHwoM-1744165078772)`; // 将图片路径转化为 Markdown 格式
      content.value += `\n\n${markdownImage}\n`; // 插入到 Markdown 内容中
    };
    reader.readAsDataURL(file); // 将图片读取为 Data URL
  }
};

五、总结

通过 Vue 3 和 markedhighlight.js,我们实现了一个功能完善的 Markdown 编辑器组件。这个组件不仅支持实时预览,还具备了语法高亮与导出功能,极大地方便了用户的文档编辑与分享需求。同时,自动保存功能和文件上传功能进一步提升了编辑器的可用性和用户体验。

该组件可以轻松地集成到任何 Vue 3 项目中,为你的应用增添强大的编辑能力。


到这里,这篇文章就和大家说再见啦!我的主页里还藏着很多 篇 前端 实战干货,感兴趣的话可以点击头像看看,说不定能找到你需要的解决方案~
创作这篇内容花了很多的功夫。如果它帮你解决了问题,或者带来了启发,欢迎:
点个赞❤️ 让更多人看到优质内容
关注「前端极客探险家」🚀 每周解锁新技巧
收藏文章⭐️ 方便随时查阅
📢 特别提醒:
转载请注明原文链接,商业合作请私信联系
感谢你的阅读!我们下篇文章再见~ 💕

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2332142.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

帆软fvs文件中某表格新增数据来声提醒

1.上传音频文件到帆软安装目录的指定环境 准备一个音频文件&#xff08;如 mp3 格式&#xff09;&#xff0c;并将其放置在合适的目录。 例如&#xff1a;%FR_HOME%\webapps\webroot\help 2.点击 FVS 模板左上角「模板>页面加载结束事件」&#xff0c;输入以下 JavaScript …

从零用java实现 小红书 springboot vue uniapp (11)集成AI聊天机器人

前言 移动端演示 http://8.146.211.120:8081/#/ 管理端演示 http://8.146.211.120:8088/#/ 项目整体介绍及演示 前面的文章我们主要完成了基础模块的开发 这次我们跟一下热点 创建AI聊天机器人 并嵌入到我们的uniapp中 首先需要了解dify我已经完成了搭建win10 VMware安装ubuntu…

$_POST 超级全局变量

$_POST 是一个超级全局变量&#xff0c;在 PHP 中用于收集通过 HTTP POST 方法发送到服务器的数据。与 $_GET 不同&#xff0c;$_POST 允许发送大量数据&#xff0c;且数据不会显示在 URL 中&#xff0c;因此更适用于提交敏感信息&#xff0c;如用户登录信息、表单数据等。 使…

开发一个环保回收小程序需要哪些功能?环保回收小程序

废品分类展示与识别 详细分类列表&#xff1a;清晰展示常见废品类型&#xff0c;如废纸&#xff08;报纸、书本纸、包装纸等&#xff09;、塑料&#xff08;塑料瓶、塑料容器、塑料薄膜等&#xff09;、金属&#xff08;易拉罐、铁制品、铜制品等&#xff09;、玻璃&#xff0…

Debezium嵌入式连接postgresql封装服务

文章目录 1.项目结构&#xff1a;2.依赖&#xff1a;3.application.properties4.DebeziumConnectorConfig类5.TableEnum类6.TableHandler接口&#xff08;表处理抽象&#xff09;7.DefaultTableHandler默认实现类8.UserTableHandler处理类9.TableHandlerFactory工厂10.Debezium…

Python 爬取 1688.item_get_factory 接口:获取工厂档案信息实战指南

在电商采购和供应链管理中&#xff0c;了解供应商的工厂信息是至关重要的一步。1688 作为国内领先的 B2B 平台&#xff0c;提供了丰富的供应商和工厂档案信息。通过 item_get_factory API 接口&#xff0c;开发者可以获取工厂的详细信息&#xff0c;包括工厂名称、地址、联系方…

Rust所有权详解

文章目录 Rust所有权所有权规则作用域 内存和分配移动与克隆栈空间堆空间 关于函数的所有权机制作为参数作为返回值 引用与租借垂悬引用 Rust所有权 C/C中我们对于堆内存通常需要自己手动管理&#xff0c;手动申请和释放&#xff0c;即便有了智能指针&#xff0c;对于效率的影…

CExercise_07_1指针和数组_2数组元素的逆序数组逆序(指针版 reverse_by_ptr 和下标版 reverse_arr)

题目&#xff1a; 数组元素的逆序。要求使用[]运算符以及纯粹指针操作两种方式来完成。 关键点 arr[i] arr[len - 1 - i]; arr[0]arr[len-1]; 如果数组序列是偶数,则调换最中间一对为止;若为奇数,则单出一个不用反转. 思想就是长度取一半 eg:8/2, 9/24.5,反转一半,到5时固定…

框架PasteForm实际开发案例,换个口味显示数据,支持echarts,只需要标记几个特性即可在管理端显示(2)

PasteForm框架的主要思想就是对Dto进行标记特性,然后管理端的页面就会以不一样的UI呈现 使用PasteForm框架开发,让你免去开发管理端的烦恼,你只需要专注于业务端和用户端! 在管理端中,如果说表格是基本的显示方式,那么图表chart就是一个锦上添花的体现! 如果一个项目拥…

Starrocks的Bitmap索引和Bloom filter索引以及全局字典

写这个的主要作用是梳理一下Starrocks的索引效率以及使用场景。 Starrocks Bitmap索引 原理&#xff1a; Bitmap 索引是一种使用 bitmap 的特殊数据库索引。bitmap 即为一个 bit 数组&#xff0c;一个 bit 的取值有两种&#xff1a;0 或 1。 每一个 bit 对应数据表中的一行&…

QML面试笔记--UI设计篇05容器控件

1. QML中容器控件全解&#xff1a;构建灵活界面的基石 1.1. Item&#xff08;万物容器&#xff09;1.2. Rectangle&#xff08;视觉容器&#xff09;1.3. ListView&#xff08;动态列表容器&#xff09;1.4. Frame&#xff08;表单容器&#xff09;1.5. SwipeView&#xff08;页…

VSCode运行,各类操作缓慢,如何清理

VSCode写代码&#xff0c;随着项目逐步进展&#xff0c;代码量在增加&#xff0c;依赖的第三方头文件也在增加&#xff0c; 先是发现代码提示的速度变慢&#xff0c; 后来格式化代码速度太慢 然后c/c代码的语法检查有时候压根就失败&#xff0c;来个错误提示 还有source contro…

redis(2)-mysql-锁

1.数据倾斜&#xff1a; 解决&#xff1a;虚拟节点 2.缓存穿透&#xff1a;缓存雪崩、击穿 3.分布式锁 多把锁控制不同节点上的一致性问题。 锁是有失效时间的。 强制回收。 4.redis 和zookeeper的区别 redis 数据支持有效期 4.1 zookeeper 分布式一致性服务框架&am…

OpenLayers:海量图形渲染之矢量切片

最近由于在工作中涉及到了海量图形渲染的问题&#xff0c;因此我开始研究相关的解决方案。在咨询了许多朋友之后发现矢量切片似乎是行业内最常用的一种解决方案&#xff0c;于是我便开始研究它该如何使用。 一、什么是矢量切片 矢量切片按照我的理解就是用栅格切片的方式把矢…

AI智算-K8s+vLLM Ray:DeepSeek-r1 671B 满血版分布式推理部署实践

K8s + vLLM & Ray:DeepSeek-r1 671B 满血版分布式推理部署实践 前言环境准备1. 模型下载2. 软硬件环境介绍正式部署1. 模型切分2. 整体部署架构3. 安装 LeaderWorkerSet4. 通过 LWS 部署DeepSeek-r1模型5. 查看显存使用率6. 服务对外暴露7. 测试调用API7.1 通过 curl7.2 通…

深入浅出SPI通信协议与STM32实战应用(W25Q128驱动)(实战部分)

1. W25Q128简介 W25Q128 是Winbond推出的128M-bit&#xff08;16MB&#xff09;SPI接口Flash存储器&#xff0c;支持标准SPI、Dual-SPI和Quad-SPI模式。关键特性&#xff1a; 工作电压&#xff1a;2.7V~3.6V分页结构&#xff1a;256页/块&#xff0c;每块16KB&#xff0c;共1…

前端知识点---闭包(javascript)

文章目录 1.怎么理解闭包?2.闭包的特点3.闭包的作用?4 闭包注意事项&#xff1a;5 形象理解6 闭包的应用 1.怎么理解闭包? 函数里面包着另一个函数&#xff0c;并且内部函数可以访问外部函数的变量。 <script> function box() {//周围状态&#xff08;外部函数中定义的…

Java 泛型的逆变与协变:深入理解类型安全与灵活性

泛型是 Java 中强大的特性之一&#xff0c;它提供了类型安全的集合操作。然而&#xff0c;泛型的类型关系&#xff08;如逆变与协变&#xff09;常常让人感到困惑。 本文将深入探讨 Java 泛型中的逆变与协变&#xff0c;帮助你更好地理解其原理和应用场景。 一、什么是协变与…

多线程(进阶)(内涵面试题)

目录 一、常见的锁策略 1. 悲观锁 vs 乐观锁 2. 重量级锁 vs 轻量级锁 3. 挂起等待锁 vs 自旋锁 4. 普通互斥锁 vs 读写锁 5. 可重入锁 vs 不可重入锁 6. 公平锁 vs 非公平锁 7. 面试题 二、synchronized的原理 1. 基本特点 2. 加锁工作过程 1&#xff09;偏向锁&am…

蓝桥杯补题

方法技巧&#xff1a; 1.进行循环暴力骗分&#xff0c;然后每一层的初始进行判断&#xff0c;如果已经不满足题意了&#xff0c;那么久直接continue&#xff0c;后面的循环就不用浪费时间了。我们可以把题目所给的等式&#xff0c;比如说有四个未知量&#xff0c;那么我们可以用…