bytemd掘金同款markdown编辑器vue配置踩坑指南

9,076 阅读6分钟

前言

vue配置bytemd掘金同款markdown编辑器

markdown编辑器对于程序员来书写还算比较方便的,特别是学习掌握了语法后,就更是写作的利器了,所以今天就学习看了下掘金同款的bytemd的官方文档介绍来配置配置

为什么使用

因为为自己写博客内容的后台时需要一个,markdown的编辑器,编辑操作我的博客内容。看了掘金的编辑器,感觉很不错,于是开始了搜索发现使用的是bytemd的,于是开始了查看资料,在自己的项目去配置使用!下面就放上项目官方地址和demo

Github地址:Github

官方demo:demo演示

踩坑过程

总体总结下来,就是自己的技术太菜了吧!不太了解书写内容,看过掘友分享的是react的帮助不少,于是开始踩坑vue的,希望可以帮助到有需要的小伙伴吧!

do'w下载安装vue里使用

npm install @bytemd/vue -S (或者使用yarn) yarn add @bytemd/vue -S

vue文件里面使用

<template>
  <div>
    <Editor class="editos" :value="value" />
    <Viewer class="viewer" :tabindex="2" :value="value"
    ></Viewer>
  </div>
</template>


<script>
import { Editor, Viewer } from '@bytemd/vue'
export default {
  data () {
    return {
       value: '', // 获取的markdow文档内容
     }
  },
  components: { Editor, Viewer }, // 组件注册
}
</script>

安装插件

github文档的官网有插件介绍,大家可以安装需求安装,也可以全部安装

23.png

我的建议如果是个人项目那就不需要care 直接全部上它丫的

大家npm安装就可以,接下来继续安装两个一个是主题样式一个是配置中文的,大家去github直接拷贝json文件或者是css也可以

npm i bytemd -S // 等会配置中文

npm i juejin-markdown-themes -S // 等会配置主题

配置中文

npm i bytemd -S 你也可以直接拷贝这个包下面的json文件

<Editor :locale="zhHans" />

js 代码
import zhHans from 'bytemd/lib/locales/zh_Hans.json'
data () { return { zhHans } }

配置图片上传

原本是需要配置插件的,但在最新版已直接集成了,开启的方法也是特别简单!

<Editor :uploadImages="uploadImage"/>


JS代码
配置点击事件就可以了
async uploadImage (files) {
      // files 获取的图片文件,这里处理逻辑
      console.log('files', files)
      return [
        {
          title: files.map((i) => i.name),
          url: 'http'
        }
      ]
    }

这里再配置两个图片代码方便不清楚怎么上传图片的小伙伴们查看

上传代码.png

图片上传逻辑.png

使用方法就是通过uploadImage方法获取二进制文件,然后这个文件直接就可以通过axios或者是其他上传到服务器,然后把返回的url赋值到数组列表里面就可以了。同理,二进制文件流是可以转成base64图片的

配置样式主题

这里卡了我半天,后面知道了就是简简单单引入css文件,我人麻了~ 无语

Markdown 主题: Markdown主题Github地址

import 'juejin-markdown-themes/dist/juejin.min.css' 直接引入就可以使用不同的主题了,这里使用的掘金的,小伙伴们喜欢其他的也可以使用不同风格的!

完整配置代码

这里简单解释一下@bytemd/vue里面导出两个组件

编辑器Editor

阅读者的Viewer

咱们理解简单一点,那就是编辑(Editor)可以修改,增加, (Viewer)阅读只可以看,也就是渲染出你存储的markdow文档!同样的编辑器配置了插件,阅读器也是需要配置的才可以对应渲染出你存储的样式代码

<template>
  <div class="details">
    <Editor
      class="editos"
      :value="value"
      :plugins="plugins"
      :locale="zhHans"
      @change="handleChange"
      :uploadImages="uploadImage"
    />
    <Viewer
      class="viewer"
      :tabindex="2" //  github官方文档有解释
      :sanitize="23" // 官方文档有解释
      :value="value"
      :plugins="plugins"
      :locale="zhHans"
    ></Viewer>
    <div class="fl al btn">
      <el-button @click="send" type="primary">修改</el-button>
      <el-button @click="send" type="primary">发布</el-button>
    </div>
  </div>
</template>

<script>
// 这里就是引入所有的扩展的插件
import 'bytemd/dist/index.css'  // 导入编辑器样式
import { Editor, Viewer } from '@bytemd/vue'
import gfm from '@bytemd/plugin-gfm'
import highlightssr from '@bytemd/plugin-highlight-ssr'
import highlight from '@bytemd/plugin-highlight'
import breaks from '@bytemd/plugin-breaks'
import footnotes from '@bytemd/plugin-footnotes'
import frontmatter from '@bytemd/plugin-frontmatter'
import gemoji from '@bytemd/plugin-gemoji'
import mediumZoom from '@bytemd/plugin-medium-zoom'
import zhHans from 'bytemd/lib/locales/zh_Hans.json'
import 'highlight.js/styles/vs.css'
import 'juejin-markdown-themes/dist/juejin.min.css'  // 其实就是需要这个css文件


const plugins = [
  // 将所有的扩展功能放入插件数组中,然后就可以生效了
  gfm(),
  highlight(),
  highlightssr(),
  breaks(),
  frontmatter(),
  footnotes(),
  gemoji(),
  mediumZoom()
]

export default {
  components: { Editor, Viewer }, // 组件注册
  data () {
    return {
      value: '', // 获取的内容
      plugins,  // 插件
      zhHans, // 简体中文
    }
  },
  methods: {
    // 获取书写文档内容
    handleChange (v) {
      console.warn(v)
      this.value = v
    },
    
    
    // 上传图片 点击触发上传图片事件,大家获取文件把图片上传服务器然后返回url既可
    async uploadImage (files) {
      console.log('files', files)
      return [
        {
          title: files.map((i) => i.name),
          url: 'http'
        }
      ]
    }
  }
}
</script>
<style lang="scss">
.details {
  position: fixed;
  top: 60px;
  left: 0;
  width: 100vw;
  height: 100vh;
  .editos {
    .bytemd {
      height: calc(100vh - 150px) !important; // 改变编辑器默认高度,不需要的可以不配置
    }
  }
  .viewer {
    margin-top: 20px;
    background: #fff;
    padding: 20px;
    .bytemd {
      height: calc(100vh - 200px) !important;
    }
  }
  .btn {
    flex-direction: row-reverse;
    margin: 20px;
    .el-button {
      margin-right: 20px;
    }
  }
}
</style>

vue3使用bytemd

实现地址: 个人博客

仓库里面包括了前端样式和接口,我现在博客地址后台就是用的这个,还要API接口也开发完成使用的nodejs, 前端是vite+vue3 前端显示 接口地址

官网出了vue3的这个包,但是我安装了。按照之前的引入就发现渲染出来的markdown文件没有样式 ~

image.png

image.png

引用方法就是


<Viewer :value="state.content"></Viewer>

import { Viewer } from '@bytemd/vue-next'

vue3的兼容感觉还不是很好,于是就重新封装一下组件实现了渲染效果。

安装配置,发现并不能如vue2那样方便使用 ~ 磕磕绊绊的还是实现了,应该会有更好的实现方法,这里我就抛砖引玉了.

下面代码实现了有样式渲染和目录

编辑模式配置代码:

<Editor
    class="editos"
    :value="state.value"
    :plugins="plugins"
    :locale="zhHans"
    @change="handleChange"
    :uploadImages="uploadImage"
/>

// js
const plugins = [gfm(), highlight(), breaks(), frontmatter(), footnotes(), gemoji(), mediumZoom()]
import 'bytemd/dist/index.css'
import { Editor } from '@bytemd/vue-next'
import gfm from '@bytemd/plugin-gfm'
import highlight from '@bytemd/plugin-highlight'
import breaks from '@bytemd/plugin-breaks'
import footnotes from '@bytemd/plugin-footnotes'
import frontmatter from '@bytemd/plugin-frontmatter'
import gemoji from '@bytemd/plugin-gemoji'
import mediumZoom from '@bytemd/plugin-medium-zoom'
import zhHans from 'bytemd/locales/zh_Hans.json'

编辑的导入,区别不大。主要是渲染的时候,引入viewer出现了问题,后面是通过封装组件的方法实现的,


// MdViewer.vue 组件

<script setup lang="ts">
import { ref, onMounted, getCurrentInstance, watch } from 'vue'
import * as bytemd from 'bytemd'
import 'bytemd/dist/index.min.css'
import 'juejin-markdown-themes/dist/scrolls-light.min.css'
import zhHans from 'bytemd/locales/zh_Hans.json'
import breaks from '@bytemd/plugin-breaks'
import highlight from '@bytemd/plugin-highlight'
import footnotes from '@bytemd/plugin-footnotes'
import frontmatter from '@bytemd/plugin-frontmatter'
import gfm from '@bytemd/plugin-gfm'
import mediumZoom from '@bytemd/plugin-medium-zoom'
import gemoji from '@bytemd/plugin-gemoji'

interface Props {
    value?: string
    plugins?: any
    locale?: any
}

const props = withDefaults(defineProps<Props>(), {
    value: '',
    locale: zhHans,
    plugins: [breaks(), highlight(), footnotes(), frontmatter(), gfm(), mediumZoom(), gemoji()],
})

const viewer = ref<bytemd.Editor | any>(null)
const instance:any = getCurrentInstance()

onMounted(() => {
    viewer.value = new bytemd.Viewer({
        target: instance?.subTree.el,
        props,
    })
})

watch(props, newValue => {
    viewer.value.$set(Object.fromEntries(Object.entries(newValue).filter(v => v)))
})
</script>

<template>
    <div />
</template>

<style lang="less" scoped></style>

实现的方法是参考掘金的一位帅哥的方法。只需要传入数据库保存的md文档内容就可以正确渲染出来,然后还配置了目录,基本实现了掘金的目录定位和滚动跳转到,指定位置。

实现的方法和原理就是 获取h1~ h4 标签,滚动监听,和a标签的锚点去实现

目录实现的代码


// html
<div class="tree">
    <h3 class="directory">目录</h3>
    <el-divider style="margin: 10px 0" />
    <ul class="menu_content">
        <li v-for="(item, key) of cata.menuData" :key="key"
           :style="menuStyle(item.type)"
           >
            <a
                :href="'#' + item.point"
                :class="cata.menuState === item.txt ? `tree_list active`:`tree_list`"
            >
                {{ item.txt }}
            </a>
        </li>
    </ul>
  </div>
  
 // css 
 
  .tree {
    width: 100%;
    min-height: 400px;
    background-color: $white;
    padding: 20px;
    max-height: 70vh;
    min-height: 10vh;
    overflow-y: scroll;
    .directory {
        @include font-set($font18, #1a1a1a, 400, 1.5);
    }
    .menu_content {
        width: 100%;
        .tree_list {
            display: block;
            @include font-set($font14, #888, 400, 1.3);
            padding: 10px 0;
            &:hover {
                background-color: #f7f8fa;
                border-radius: 6px;
            }
        }
        .active {
            position: relative;
            color: #1e80ff;
            &::before {
                content: '';
                height: 20px;
                width: 5px;
                background: #1e80ff;
                position: absolute;
                left: -17px;
                top: 50%;
                border-radius: 0 5px 5px 0;
                transform: translate(-50%, -50%);
            }
        }
     }
   }
   
   
// js 核心代码实现目录定位和滚动监听
import { onMounted, reactive, ref, nextTick } from 'vue'

interface Menu {
    type: string;
    txt: string;
    offsetTop: number;
    point: string
}

const cata = reactive({
    menuData: <Menu[]>[],
    menuState: '',
})
 
 
 /**
 * h1 h2 h3 h4 标签样式
 * @param type 
 */
const menuStyle = (type: string) => {
    let style = {}
    if (type === 'H2') style = { 'padding-left': 10 + 'px' }
    if (type === 'H3') style = { 'padding-left': 20 + 'px' }
    if (type === 'H4') style = { 'padding-left': 30 + 'px' }

    return style
}


onMounted(() => {
    componentDidMount()
    window.addEventListener('scroll', onScroll, true)
})

// 重新实现目录的定位
const componentDidMount = () => {
    nextTick(() => {
        getElement(['H1', 'H2', 'H3', 'H4'])
    })
}

/**
 * 获取标题锚点
 * 参数nodeArr 表示需要解析目录内容的标题
 */
const getElement = (nodeArr: string[]) => {
    let nodeInfo: Menu[] = []
    const dom: any = document.querySelector('.markdown-body')
    // console.log(dom.childNodes)
    dom.childNodes.forEach((item: any, key: number) => {
        // console.log(item.nodeName)
        if (nodeArr.includes(item.nodeName)) {
            nodeInfo.push({
                type: item.nodeName,
                txt: item.innerText,
                offsetTop: item.offsetTop,
                point: `target_${key}`,
            })
            item.setAttribute('id', `target_${key}`)
            console.log(item)
        }
    })

    cata.menuData = nodeInfo
    cata.menuState = nodeInfo[0].txt
    console.log('nodeInfo', nodeInfo)

}

/**
 * 监听页面开始滚动
 */
const onScroll = (e: any) => {
    // 当前页面滚动的距离
    let scrollTop = e.target.documentElement.scrollTop || e.target.body.scrollTop
    // console.log(scrollTop)
    //变量windowHeight是可视区的高度
    let windowHeight = document.documentElement.clientHeight || document.body.clientHeight
    //变量scrollHeight是滚动条的总高度
    let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight

    let currentmenu = cata.menuData[0].txt // 设置menuState对象默认值第一个标题
    for (let item of cata.menuData) {
        console.log(item.offsetTop)
        if (scrollTop >= item.offsetTop) {
            currentmenu = item.txt
        } else break
    }

    if (currentmenu !== cata.menuState) {
        cata.menuState = currentmenu
    }

    // 如果到底部,就命中最后一个标题
    if (scrollTop + windowHeight === scrollHeight) {
        console.log('滚动到底部了')
        cata.menuState = cata.menuData[cata.menuData.length - 1].txt
    }
}

实现的效果呢,只能说还是不够完美!还需要优化 ~ 不过基本上满足我自己的需求,后续再踩踩试试。

结尾

以上就是我使用Vue去配置使用bytemd这款编辑器时候碰到的问题,记录下来,方便后续查阅读

已更新vue3的踩坑 ~