react解析md文件

效果

效果1
效果2

需求

因为项目最后是对外开放的,所以在项目中有个文档中心,里面有一些用户手册、开发文档等展示需求。同时支持文章目录点击。

分析

第一时间想到的就是解析md文件了,语法简单,满足大部分写作要求,市面上也很有很多成熟的解析方案。

依赖项

这里是使用了react-markdowngithub-markdown-css这两个库,一个是解析md文件,一个是github的md文档样式。

具体
解析md文件(大概)
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown/with-html';

import 'github-markdown-css/github-markdown.css'

const articles = {
  '1': '/developer_guide.md',
  '2': '/user_manual.md'
}
const NormalTest: React.FC<any> = () => {
  const [currentArticle, setCurrentArticle] = useState<{ url: string, content: any }>({ url: '', content: '' });
      
  // 初始为开发文档
  useEffect(() => {
    changeCurrentArticle(articles['1'])
  }, [])
      
  // 更改当前文档
  const changeCurrentArticle = async (url: string) => {
    const res = await fetch(url);
    const content = await res.text();
    setCurrentArticle({ ...currentArticle, content, url })
  }

   return (
     <>
       <ReactMarkdown
           className="markdown-body"
           source={currentArticle.content}
           escapeHtml={false}
        />
      </>
    )
};

如果页面正确显示,说明是解析成功

单独解析目录

这里要用到react-markdown组件的renderersAPI,给title加上锚点

HeadBlock.tsx

import React from 'react';

const HeadBlock: React.FC<any> = (props) => {

  const { level, children } = props;
  const { nodeKey } = children[0].props;

  return (
    <>
      {React.createElement(`h${level}`, { className: 'article-nav', id: nodeKey, 'data-level': level }, children)}
    </>
  );
}

export default HeadBlock;

index.tsx

import HeadBlock from './HeadBlock';

<ReactMarkdown
    className="markdown-body"
    source={currentArticle.content}
    escapeHtml={false}
    renderers={{
    heading: HeadBlock
    }}
/>
完整版
import React, { useState, useEffect } from 'react';
import { Row, Col, Menu, Affix, Anchor } from 'antd';
import ReactMarkdown from 'react-markdown/with-html';
import { isEmpty } from "lodash";

import HeadBlock from './HeadBlock';

import 'github-markdown-css/github-markdown.css'
import './index.less';

const { Link } = Anchor;

const articles = {
  '1': '/developer_guide.md',
  '2': '/user_manual.md'
}


/**
 * 
 * @param lists 
 * 这里只做两级处理
 */
export const navsToTree = (lists: any[]) => {
  if (isEmpty(lists)) return [];
  // 提取第一个level为最大level 后续比他大的一律为同级
  const maxLevel = lists[0].level;
  const newLists: any[] = [];
  lists.forEach((item: any) => {
    // 一级 同级
    if (item.level <= maxLevel) {
      newLists.push(item)
    } else {
      // 非同级
      if (newLists[newLists.length - 1].children) {
        newLists[newLists.length - 1].children.push(item)
      } else {
        newLists[newLists.length - 1].children = [item]
      }
    }
  })
  return newLists;
}



const NormalTest: React.FC<any> = () => {

  const [currentArticle, setCurrentArticle] = useState<{ url: string, content: any }>({ url: '', content: '' });

  const [treeNavs, setTreeNavs] = useState<any[]>([])

  // 初始为开发文档
  useEffect(() => {
    // console.log(1);

    changeCurrentArticle(articles['1'])
  }, [])

  // 这里是根据文档修改进行获取目录
  useEffect(() => {
    /**
    *  获取所有的文章标题
    */
    // console.log(currentArticle);

    const markdownNavs = document.querySelectorAll('.article-nav')
    const navs: any[] = [];
    markdownNavs.forEach((item: any) => {
      const level = item.getAttribute('data-level');
      const value = item.textContent;
      const nodeKey = item.id;
      navs.push({ level, value, nodeKey })
    })
    transArticleNavs(navs)

  }, [currentArticle.content])

  // 更改当前文档
  const changeCurrentArticle = async (url: string) => {
    const res = await fetch(url);
    const content = await res.text();
    setCurrentArticle({ ...currentArticle, content, url })
  }

  // 书籍导航点击
  const menuOnClick = (e: any) => {
    const url = articles[e.key]
    changeCurrentArticle(url)
  }

  // 转换为文章右侧目录
  const transArticleNavs = (navs: any) => {

    // 转换为二级导航
    const treedevelopDocs = navsToTree(navs);
    setTreeNavs(treedevelopDocs)

  }

  return (
    <>
      <Row className='articles'>
        <Col flex='200px' className="articles-list">
          <Affix offsetTop={24}>
            <Menu defaultSelectedKeys={['1']} onClick={menuOnClick} theme='light'>
              <Menu.Item key="1">开发文档</Menu.Item>
              <Menu.Item key="2">使用文档</Menu.Item>
            </Menu>
          </Affix>

        </Col>
        <Col flex='1' className='articles-content'>
          <div className='articles-content_wrpper'>
            <ReactMarkdown
              className="markdown-body"
              source={currentArticle.content}
              escapeHtml={false}
              renderers={{
                heading: HeadBlock
              }}
            />
          </div>
        </Col>
        <Col flex='200px' className="articles-menu">
          <Affix offsetTop={20} >
            <Anchor style={{ width: 160 }}>
              {
                treeNavs.map((item: any) => {
                  if (item.children) {
                    return (
                      <Link href={`#${item.nodeKey}`} title={item.value} key={item.nodeKey}>
                        {
                          item.children.map((childItem: any) => (
                            <Link href={`#${childItem.nodeKey}`} title={childItem.value} key={childItem.nodeKey} />
                          ))
                        }
                      </Link>
                    )
                  } else {
                    return (
                      <Link href={`#${item.nodeKey}`} title={item.value} key={item.nodeKey} />
                    )
                  }
                })
              }
            </Anchor>
          </Affix>
        </Col>
      </Row>
    </>
  );
};

export default NormalTest;

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值