在 vue3 中使用 markdown 编辑器 md-editor-v3

本文将介绍编辑器的使用和伴随的某些开发技巧。

该编辑器支持的功能有:基础的md编辑、md语法快捷键、记录保存、暗黑主题、图片上传/复制图片上传/裁剪图片上传、格式化内容、浏览器全屏/屏幕全屏、仅预览模式等功能,静待使用。

详细的编辑器api参考:文档

  • 图片裁剪预览
image
image
  • 编辑器预览
image

1. 基本使用

这里演示两种环境三种写法:

1.1 npm安装用法

这种方式支持两种写法,除了.vue模板写法,还有jsx语法。

安装

yarn add md-editor-v3

.vue模板基础使用

<template>
  <md-editor v-model="text" />
 </template>
 
 <script lang="ts">
 import { defineComponent } from 'vue';
 import MdEditor from 'md-editor-v3';
 import 'md-editor-v3/lib/style.css';
 
 export default defineComponent({
   components: { MdEditor },
   data() {
     return { text: '' };
   }
 });
 </script>

jsx语法基础使用

import { defineComponent, ref } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  name: 'MdEditor',
  setup() {
    const text = ref('');
    return () => (
      <MdEditor modelValue={text.value} onChange={(v: string) => (text.value = v)} />
    );
  }
});

1.2 script标签引入用法

链接可前往https://cdn.jsdelivr.net搜索md-editor-v3

<!--添加样式-->
<link href="https://cdn.jsdelivr.net/npm/md-editor-v3@1.2.0/lib/style.css" rel="stylesheet" />
<!--引入vue3-->
<script src="https://cdn.jsdelivr.net/npm/vue@3.1.5/dist/vue.global.prod.min.js"></script>
<!--引入组件-->
<script src="https://cdn.jsdelivr.net/npm/md-editor-v3@1.2.0/lib/md-editor-v3.umd.js"></script>

注册组件

const App = {
 data() {
   return {
      text: 'Hello Editor!!'
   };
 }
};

Vue.createApp(App).use(MdEditorV3).mount('#md-editor-v3');

使用组件

<div id="md-editor-v3">
  <md-editor-v3 v-model="text" />
</div>

2. 渲染内容

该编辑器使用marked解析mdhtml,没有扩展语法。

通常来讲,编辑内容存储为md格式,渲染内容时,通过marked解析为html。

2.1 默认渲染

1.3.0版本后,编辑器支持了previewOnly功能,可以直接使用编辑器预览文章,没有bar、编辑等等。

<template>
  <md-editor
    v-model="text"
    previewOnly
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  components: { MdEditor },
  data() { return { text: '## 我只会显示预览内容' }; }
});
</script>

2.2 主动解析演示

这种方式用于保存md,然后自行解析md内容。

import marked from 'marked';

// 代码高亮
import hljs from 'highlight.js';
// 自选代码高亮样式
import 'highlight.js/scss/atom-one-dark.scss';

// 用于记录标题数,根据业务代替
let count = 0;
// 记录标题内容
const headstemp = [];

// marked设置
const rendererMD = new marked.Renderer();

// 调整标题内容
rendererMD.heading = (text, level) => {
  headstemp.push({ text, level });
  count++;
  return `<h${level} id="heading-${count}"><span class="h-text">${text}</span></h${level}>`;
};

// 设置图片内容,统一显示一张缓存图,用于懒加载~
rendererMD.image = (href, _, text) =>
  `<img data-src="${href}" src="/cos/2020/1211175603.png" alt="${text}" >`;

marked.setOptions({
  highlight(code) {
    return hljs.highlightAuto(code).value
  },
  renderer: rendererMD
});

// 这里的html就是插入到页面的元素文本了
const html = marked('## md内容');

2.3 标题导航实现

上面的例子headstemp记录了解析过程中的所有标题,作用是借助UI库的组件Anchor,构建一个标题导航。

下面演示一个基于ant-design-vue的版本,如果你使用的UI库是类似的锚点组件,那么代码将只需要小改动即可。代码使用jsx语法,vue模板语法请自行分离代码~

Recursive.tsx 导航中的链接组件

import { Anchor } from 'ant-design-vue';
import { defineComponent, PropType } from 'vue';

const { Link } = Anchor;

export interface Head {
  text: string;
  level: number;
}

export interface TocItem extends Head {
  anchor: string;
  children?: Array<TocItem>;
}

const Recursive = defineComponent({
  props: {
    tocItem: {
      type: Object as PropType<TocItem>,
      default: () => []
    }
  },
  setup({ tocItem }) {
    return (
      <Link href={`#${tocItem.anchor}`} title={tocItem.text}>
        {tocItem.children &&
          tocItem.children.map((item) => <Recursive key={item.anchor} tocItem={item} />)}
      </Link>
    );
  }
});

export default Recursive;

Topicfy.tsx 用于生成整个导航内容

import { Anchor } from 'ant-design-vue';
import { computed, defineComponent, PropType, ref, watch } from 'vue';

import Recursive, { Head, TocItem } from './Recursive';

const Topicfy = defineComponent({
  props: {
    // 解析得到的标题列表
    heads: {
      type: Array as PropType<Array<Head>>
    }
  },
  setup(props) {
    const topics = computed(() => {
      const tocItems: TocItem[] = [];

      // 标题计数器
      let count = 0;

      const add = (text: string, level: number) => {
        count++;

        const item = { anchor: `heading-${count}`, level, text };

        if (tocItems.length === 0) {
          // 第一个 item 直接 push
          tocItems.push(item);
        } else {
          let lastItem = tocItems[tocItems.length - 1]; // 最后一个 item

          if (item.level > lastItem.level) {
            // item 是 lastItem 的 children
            for (let i = lastItem.level + 1; i <= 6; i++) {
              const { children } = lastItem;
              if (!children) {
                // 如果 children 不存在
                lastItem.children = [item];
                break;
              }
              // 重置 lastItem 为 children 的最后一个 item
              lastItem = children[children.length - 1];

              if (item.level <= lastItem.level) {
                // item level 小于或等于 lastItem level 都视为与 children 同级
                children.push(item);
                break;
              }
            }
          } else {
            // 置于最顶级
            tocItems.push(item);
          }
        }
      };

      props.heads?.forEach((item) => {
        add(item.text, item.level);
      });
      return tocItems;
    });

    return () => (
      <Anchor affix={false} showInkInFixed={true}>
        {topics.value.map((item) => (
          <Recursive key={item.anchor} tocItem={item} />
        ))}
      </Anchor>
    );
  }
});

export default Topicfy;

该组件是19年参考了网络上的实现完成的,非本人完全原创,react版本参考Topicfy

2.4 获取html代码

编辑器考虑到了可能后端不存储md格式的文本,而是html内容,所以提供了onHtmlChanged方法,用于编辑内容变化后,marked编译了内容的回调,入参即是html内容。

<template>
  <md-editor
    v-model="text"
    @onHtmlChanged="saveHtml"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  components: { MdEditor },
  data() { return { text: '' }; },
  methods: { saveHtml(h: string) { console.log(h) }}
});
</script>

jsx语法相同。

3. 编辑器的功能演示

3.1 扩展库链接

编辑器扩展内容大多使用了cdn,考虑了无外网情况,支持了内网链接扩展,演示(假设外部库都在根目录下):

<template>
  <md-editor
    v-model="text"
    highlightJs="/highlight.min.js"
    highlightCss="/atom-one-dark.min.css"
    prettierCDN="/standalone.js"
    prettierMDCDN="/parser-markdown.js"
    cropperJs="/cropper.min.js"
    cropperCss="/cropper.min.css"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  components: { MdEditor },
  data() { return { text: '' }; }
});
</script>

v1.2.0版本目前支持上述链接,图标链接将在后续补丁中添加。

3.2 工具栏自定义

默认的全部工具栏,并且每个功能都绑定了快捷键,如果需要选择性显示工具栏,提供了两个api:toolbarstoolbarsExclude,前者显示数组中的全部,后者屏蔽数组中的全部,后者的权重更大。下面是个参考:

案例不显示github按钮

<template>
  <md-editor
    v-model="text"
    :toolbars="toobars"
  />
  
  <md-editor
    v-model="text"
    :toolbarsExclude="toolbarsExclude"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  components: { MdEditor },
  data() {
    return {
      text: '',
      toobars: ['bold', 'underline', 'italic', 'strikeThrough',
      'sub','sup','quote','unorderedList', 'orderedList', 'codeRow',
      'code', 'link', 'image', 'table', 'revoke',
      'next', 'save', 'pageFullscreen', 'fullscreen',
      'preview', 'htmlPreview'],
      toolbarsExclude: ['github']
    };
  }
});
</script>

3.3 扩展语言

编辑器默认内置了中文和英文,并且两者都可以通过扩展api覆盖,该功能主要用来设置内容提示,比如弹窗中的标题等。

扩展一门语言,我们取名为zh-NB

<template>
  <md-editor
    v-model="text"
    :language="language"
    :languageUserDefined="languageUserDefined"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MdEditor, { StaticTextDefaultValue } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

const languageUserDefined: { 'zh-NB': StaticTextDefaultValue } = {
  'zh-NB': {
    toolbarTips: {
      bold: '加粗',
      underline: '下划线',
      italic: '斜体',
      strikeThrough: '删除线',
      title: '标题',
      sub: '下标',
      sup: '上标',
      quote: '引用',
      unorderedList: '无序列表',
      orderedList: '有序列表',
      codeRow: '行内代码',
      code: '块级代码',
      link: '链接',
      image: '图片',
      table: '表格',
      revoke: '后退',
      next: '前进',
      save: '保存',
      prettier: '美化',
      pageFullscreen: '浏览器全屏',
      fullscreen: '屏幕全屏',
      preview: '预览',
      htmlPreview: 'html代码预览',
      github: '源码地址'
    },
    titleItem: {
      h1: '一级标题',
      h2: '二级标题',
      h3: '三级标题',
      h4: '四级标题',
      h5: '五级标题',
      h6: '六级标题'
    },
    linkModalTips: {
      title: '添加',
      descLable: '链接描述:',
      descLablePlaceHolder: '请输入描述...',
      urlLable: '链接地址:',
      UrlLablePlaceHolder: '请输入链接...',
      buttonOK: '确定',
      buttonUpload: '上传'
    },
    // v1.2.0新增
    clipModalTips: {
      title: '裁剪图片上传',
      buttonUpload: '上传'
    },
    // v1.1.4新增
    copyCode: {
      text: '复制代码',
      tips: '已复制'
    }
  }
};

export default defineComponent({
  components: { MdEditor },
  data() {
    return {
      text: '',
      language: "zh-NB",
      languageUserDefined
    };
  }
});
</script>

如果key = 'zh-CN',就可以实现中文覆盖,依次类推。

3.4 主题切换

这一块相对比较简单了,内置了暗黑主题默认主题,通过themeapi切换,demo如下:

<template>
  <md-editor
    v-model="text"
    :theme="theme"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';

export default defineComponent({
  components: { MdEditor },
  data() {
    return {
      text: '',
      theme: 'dark'
    };
  }
});
</script>

4. 结尾

更多的更新请关注:md-editor-v3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容