手把手教学VUE3+TS仿掘金Bytemd编辑器

1,667 阅读4分钟

前言

老大说我们现在后台的编辑器(Tinymce)编辑的内容在不同的客户端展示的不一样 为了使我们客户端的文本格式统一,我们换成掘金的编辑器。然后还给我发bytemd的GitHub链接:github.com/bytedance/b… 让我研究研究。我心想,不同端展示不一样不应该去调客户端展示内容的代码嘛,之前的Tinymce已经经过多方面调试,除了某几个已经发现的奇怪bug(但不影响使用),其他方面(比如解决粘贴第三方图片跨域等问题)都很ok,但是谁叫他是老大呢,那就开始研究。

准备工作

然后对着文档一顿研究,然后开始安装,我是vue3版本的,看好自己的的版本安装,有一堆插件,可以根据自己开发需求来,需要哪个安装哪个就好了。

   npm install bytemd
   npm install @bytemd/vue-next
   npm install @bytemd/plugin-gfm
   npm install @bytemd/plugin-gemoji
   npm install @bytemd/plugin-highlight
   npm install @bytemd/plugin-frontmatter
   npm install @bytemd/plugin-medium-zoom
   npm install @bytemd/plugin-breaks
   npm install juejin-markdown-themes

上代码

<template>
  <div class="markdow-page">
    <Editor :locale="zhHans"  :value="mdValue" :plugins="mdPlugins" @change="handleChange" :upload-images="handleUploadImages"
      @paste="handlePaste" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { Editor, Viewer } from '@bytemd/vue-next';
import gfm from '@bytemd/plugin-gfm';
import gemoji from '@bytemd/plugin-gemoji';
import highlight from '@bytemd/plugin-highlight'; 
import frontmatter from '@bytemd/plugin-frontmatter';
import mediumZoom from '@bytemd/plugin-medium-zoom'; 
import breaks from '@bytemd/plugin-breaks';
import zhHans from 'bytemd/locales/zh_Hans.json';
import 'bytemd/dist/index.css';
import 'juejin-markdown-themes/dist/juejin.min.css'; 
import { imgConverter, uploadImage, htmlToMarkdown } from "@/api/public";

const mdValue = ref('')

const mdPlugins = ref([
  gfm(),
  gemoji(),
  highlight(),
  frontmatter(),
  mediumZoom(),
  breaks()
])

const handleChange = (val: any) => {
  value.value = val;
};

const handlePaste = async (event: any) => {

 // 获取 'html' 格式的数据,数据是一个字符串,而不是实际的DOM元素
  let htmlData = event.clipboardData.getData('text/html');
  
 // 转成DOM元素
  let parser = new DOMParser();
  let doc: any = parser.parseFromString(htmlData, "text/html");
  
 // 由于粘贴的是第三方内容,图片需要处理一下
  const images = doc.getElementsByTagName("img");
  
  if (images.length) {
    let tasks: any = [];
 // 你是否会觉得下面的代码为啥要用images[i]而不用item? 因为此处的images[i] != item
    Array.from(images).forEach((item: any, i: number) => {
 // 如果图片不是可信赖的(自己公司的)
      if (!images[i].src.includes("https://img.xxx.com")) {
        tasks.push(
  
 // 将第三方图片转成自己公司的,dataset是因为粘贴公众号的内容会发现公众号的图片没有src属性,需要转换一下
          imgConverter({ url: images[i].src || images[i].dataset.src }).then((result: any) => {
            images[i].src = result.data;
          })
        );
      }
    });
    await Promise.all(tasks);
  } 
  // 转回html字符串传给后端
  let serializer = new XMLSerializer();
  let str = "";
  for (let node of doc.body.childNodes) {
    str += serializer.serializeToString(node);
  }
  
  htmlToMarkdown({ text: str }).then((res:any) => {
    value.value = res.data;
  })
};

const handleUploadImages = async (files: any) => {
  let imgs: any = [];
  for (let index = 0; index < files.length; index++) {
    const item = files[index];
    let fromData = new FormData();
    fromData.append("file", item);
    let res = await uploadImage(fromData);  // 上传到阿里云
    imgs.push({
      title: item.name,
      url: res.url,
    });
  }
  return imgs;
};
</script>

<style lang="scss" scoped>
.markdown-page {
  width: 100%;
  :deep() {
    .bytemd {
      height: 800px;
    }
  }
}
</style>

遇到的问题或需要注意的

问题一

由于我是vue3+ts,在引入Editor一直报错(应该是我的tsconfig.json文件没有配置好),说什么Editor是一个类型,这个报错好像是因为源码没有写类型,然后我靠着我蹩脚的TS知识加聪明的Chatgpt,找到了解决办法,我在我的类型声明文件 shims-vue.d.ts 中加入以下代码

declare module '@bytemd/vue-next' {
  export const Editor: any;
  export const Viewer: any;
}

问题二

这个文本框看起来差不多了,输入什么的调试都ok,但是当我去粘贴第三方的内容时,高亮什么的并不能生效,去打印change事件的val值,发现这个值有问题,打印出来粘贴第三方文本的值并不是html格式,而是纯文本格式,以为是自己搞错了,然后去官方demo上测试,发现给出的demo也是这样的,这怎么和掘金自己的不一样啊。然后又开始研究,然后想了想Tinymce的文本框是怎么处理粘贴第三方的内容,于是测试paste事件,可以看我上面写的handlePaste事件的内容。

问题三

文本框上面的那些插件选项后面三个鼠标放上去的提示是英文,其他的都是中文,目前没找到解决办法,希望有知道的jym评论区留言指导或者私信我

问题四

当我粘贴的文本是HTML格式的时候,是没有问题的,但是如果我复制的是markdown格式的会粘贴出问题

问题五

如果我粘贴了一部分内容,需要粘贴第二部分内容的时候,由于我上面是直接赋值的方式,会导致第二次粘贴的内容将第一次粘贴的内容给覆盖

总结

由于markdown格式不支持背景图,粘贴第三方的东西可能还是会出现样式问题。还有个问题是,文本框编辑好内容后,传给后端需要转成HTML格式的字符串(传给后端,后端再传到客户端,客户端v-html渲染就好了)

// 装了一个html转markdown格式的包
import * as marked from "marked";

// 最后将result传给后端就好了
let result = marked.parse(value.value);

后端将HTML格式转md格式的库是:github.com/thephpleagu…

可能最后还是会选择用Tinymce文本框,但是这次的需求又然让我进步一点点。有啥好的建议欢迎评论区留言。