一个简单的markdown编辑器,图片加载不理想

前言

最近我在项目中需要实现一个 markdown编辑器 的需求,并且是以React为开发基础的, 类似就可以。
在网上也一顿查找了一番,要么是vue的,要么源码拿来也用不了,所以。。。

需要实现的功能

我们自己实现的话,看看需要支持哪些功能,因为做一个初版的简易编辑器,所以功能实现得不会太多:

markdown语法解析,并实时渲染
markdown主题css样式
代码块高亮展示
编辑器工具栏中图片上传功能的实现(预览图片是带token才能预览)
这里先放上我最终实现好了的效果图:


image.png

1、安装依赖

npm install marked 
npm install highlight.js

2、允许直接使用css, 在config.js中加入

context.resourcePath.includes('css') ||

3、创建index.js

// index.js
import { useState, useEffect } from 'react';
import { Icon, Modal, Upload, message, Button, Card, Form } from 'antd';
import { marked } from 'marked';
import hljs from 'highlight.js';
import './github-dark.css';
import './index.css';
import index from 'memoize-one';

class Marked extends React.Component {
  state = {
    text: '',
    visible: false,
    curTab: 'articles'
  }

  docInput = null

  // 更改当前文档
  changeCurrentArticle = async (url) => {
    const res = await fetch(url);
    const content = await res.text();
    // console.log(content)
    return content;
  }


  // 更改当前文档
  getActicle = () => {
    const fs = require('fs');
    console.log(fs)
    fs.readFile('E:/新建文件夹/宇信科技/ant-design-pro-2/public/ok.md', 'utf8', function (err, dataStr) {
      if (err) {
        return console.log("读取文件失败!" + err.message)
      }
      console.log("读取文件成功!" + dataStr)
    })
  }

  componentDidMount() {
    // 如果有md格式文件内容,先要直接展示出来
    const { defaultContent } = this.props;
    // 配置highlight
    hljs.configure({
      tabReplace: '',
      classPrefix: 'hljs-',
      languages: ['CSS', 'HTML', 'JavaScript', 'Python', 'TypeScript', 'Markdown'],
    });
    // 配置marked
    marked.setOptions({
      renderer: new marked.Renderer(),
      highlight: code => hljs.highlightAuto(code).value,
      gfm: true, //默认为true。 允许 Git Hub标准的markdown.
      tables: true, //默认为true。 允许支持表格语法。该选项要求 gfm 为true。
      breaks: true, //默认为false。 允许回车换行。该选项要求 gfm 为true。
    });

    this.docInput = document.getElementById("inputDiv");


    // 图片加载成功时触发load事件,加载失败时触发error事件,成功不会触发
    document.addEventListener("error", function (event) {
      var ev = event ? event : window.event;
      var elem = ev.target;
      const token = "1213";
      if (elem.tagName.toLowerCase() == 'img') {
        var url = elem.getAttribute('src');
        var request = new XMLHttpRequest();
        request.responseType = 'blob';
        request.open('get', `${url}`, true);
        // 添加header里的token
        request.setRequestHeader('actoken', token);
        // 成功加载图片后把结果赋值给elem.src
        // request.onreadystatechange = e => {
        //   if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
        //     elem.src = URL.createObjectURL(request.response);
        //     elem.onload = () => {
        //       URL.revokeObjectURL(elem.src);
        //     }
        //   }
        // };
        request.send(null);
        // 图片加载失败  --替换为默认 
        // elem.src = "../img/default.jpg";
      }
    }, true);

    // 初始值设置
    if (defaultContent) {
      this.docInput.innerText = defaultContent;
      this.setState({
        text: defaultContent
      })
    }
  }

 /**
  * @param {string} name - 操作按钮类型
  */

  handleIcons = (name) => {
    if (name === 'picture') {
      this.setState({ visible: true })
      return;
    }
  }

  setPicInfo = (name, path) => {
    // 如果返回的是可以直接访问的地址就最好了,但我这里的预览地址不能直接访问,需要token
    const picUro = `/api/center/previewImage?filePath=${path}`;
    const resP = `![${name}](${picUro})`;

    // 编辑框设置焦点
    this.docInput.focus()
    // 获取选定对象
    var selection = window.getSelection();
    // 判断是否有最后光标对象存在
    if (this.lastEditRange) {
      // 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
      selection.removeAllRanges();
      selection.addRange(this.lastEditRange)
    }

    if (selection.anchorNode.nodeName != '#text') {
      // 如果是编辑框范围。则创建表情文本节点进行插入
      var emojiText = document.createTextNode(resP)
      const len = this.docInput.childNodes.length;
      if (len) {
        // 如果文本框的子元素大于0,则表示有其他元素,则按照位置插入表情节点
        for (var i = 0; i < len; i++) {
          if (i == selection.anchorOffset) {
            this.docInput.insertBefore(emojiText, edit.childNodes[i])
          }
        }
      } else {
        // 否则直接插入一个表情元素
        this.docInput.appendChild(emojiText)
      }
    } else {
      // 如果是文本节点则先获取光标对象
      var range = selection.getRangeAt(0)
      // 获取光标对象的范围界定对象,一般就是textNode对象
      var textNode = range.startContainer;
      // 获取光标位置
      var rangeStartOffset = range.startOffset;
      // 文本节点在光标位置处插入新的表情内容
      textNode.insertData(rangeStartOffset, resP)
      // 光标移动到到原来的位置加上新内容的长度
      range.setStart(textNode, rangeStartOffset + resP.length)
      // 光标开始和光标结束重叠
      range.collapse(true)
      // 清除选定对象的所有光标对象
      selection.removeAllRanges()
      // 插入新的光标对象
      selection.addRange(range)
    }
    // 无论如何都要记录最后光标对象
    this.lastEditRange = selection.getRangeAt(0)
    this.setState({
      text: this.docInput.innerText
    })
  }

  beforeUpload = (file) => {
    const { dispatch } = this.props;
    return new Promise((resolve, reject) => {
      const typeF = file.type;
      if (!['image/jpeg', 'image/png', 'image/gif', 'image/jpg'].includes(typeF)) {
        message.error('只能选择jpeg、png、gif、jpg格式的图文文件');
      }
      const formData = new FormData();
      formData.append('file', file);
      // 调接口获得当前上传图片的path
      // dispatch({
      //   type: '',
      //   payload: formData,
      //   callback: res => {

      //   }
      // })
      const res = '1213.jpg';
      this.setPicInfo(file.name, res);
    }).then(() => {
      reject();
    })
  }

  render() {
    const { text } = this.state;
    return (
      <>
        
          {/* 顶部图片上传按钮 */}
          
this.handleIcons('picture')} />
{/* markdown编辑和预览区域 */}
{/* 左侧markdown编辑区域 */}
{ this.setState({ text: e.target.innerText }) }} >
{/* 右侧markdown预览区域 */}
/g, "
"),
              }}
            >
this.setState({ visible: false })} onCancel={() => this.setState({ visible: false })} > , ); } }; export default Marked;

4、创建index.css

/* html,
body,
#root {
    height: 100%;
    margin: 0;
    padding: 0;
} */

.header {
  background-color: #d9d9d9;
  border-bottom: 1px solid #ccc;
  /* font-size: 30px; */
  height: 50px;
  display: flex;
  /* justify-content: center; */
  align-items: center;
  /* border-bottom: 1px solid #eee; */
}

.header .input-icon {
  font-size: 22px;
  padding: 20px;
  cursor: pointer;
}

.marked {
  /* min-height: calc(100vh - 60px); */
  height: 500px;
  display: flex;
}

.input-region {
  /* background-color: rgb(233, 233, 233); */
  outline: none;
}

.input-region,
.show-region {
  flex: 1;
  padding: 20px;
  font-size: 20px;
}

/* ——————————————————————————————显示区markdown样式—————————————————————————————— */
/* 左右区域纵向滚动条 */
.markdownStyle {
  overflow-y: auto;
  border: 1px solid #ccc;
}

/* 表格 */
.markdownStyle table {
  border: 1px solid #000;
  border-spacing: 0;
  /*去掉单元格间隙*/
}

.markdownStyle table th,
.markdownStyle table td {
  padding: 6px 14px;
  border: 1px solid #000;
}

/* 行内代码 */
.markdownStyle code {
  color: red;
  background-color: #f9f2f4;
  padding: 0 4px;
}

/* 图片 */
.markdownStyle p img {
  display: block;
  max-width: 100%;
  margin: 14px auto;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}

/* ———————————————————————————————————————————————————————————— */

5、创建github-dark.css

#hljs {
  padding: 12px;
  color: #c9d1d9;
  background: #0d1117;
  border-radius: 12px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}

code {
  font-family: 'FiraCode';
}

/* 代码片段 */
#hljs code {
  color: #c9d1d9;
  background: #0d1117;
  padding: 0;
  font-size: 16px;
}

.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
  /* prettylights-syntax-keyword */
  color: #ff7b72;
}

.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
  /* prettylights-syntax-entity */
  color: #d2a8ff;
}

.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
  /* prettylights-syntax-constant */
  color: #79c0ff;
}

.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
  /* prettylights-syntax-string */
  color: #a5d6ff;
}

.hljs-built_in,
.hljs-symbol {
  /* prettylights-syntax-variable */
  color: #ffa657;
}

.hljs-comment,
.hljs-code,
.hljs-formula {
  /* prettylights-syntax-comment */
  color: #8b949e;
}

.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
  /* prettylights-syntax-entity-tag */
  color: #7ee787;
}

.hljs-subst {
  /* prettylights-syntax-storage-modifier-import */
  color: #c9d1d9;
}

.hljs-section {
  /* prettylights-syntax-markup-heading */
  color: #1f6feb;
  font-weight: bold;
}

.hljs-bullet {
  /* prettylights-syntax-markup-list */
  color: #f2cc60;
}

.hljs-emphasis {
  /* prettylights-syntax-markup-italic */
  color: #c9d1d9;
  font-style: italic;
}

.hljs-strong {
  /* prettylights-syntax-markup-bold */
  color: #c9d1d9;
  font-weight: bold;
}

.hljs-addition {
  /* prettylights-syntax-markup-inserted */
  color: #aff5b4;
  background-color: #033a16;
}

.hljs-deletion {
  /* prettylights-syntax-markup-deleted */
  color: #ffdcd7;
  background-color: #67060c;
}

/* .hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {

} */

你可能感兴趣的:(一个简单的markdown编辑器,图片加载不理想)