惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
腾讯CDC
爱范儿
爱范儿
Google DeepMind News
Google DeepMind News
V
V2EX
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
量子位
F
Fortinet All Blogs
G
Google Developers Blog
T
The Blog of Author Tim Ferriss
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hugging Face - Blog
Hugging Face - Blog
Last Week in AI
Last Week in AI
T
Tailwind CSS Blog
J
Java Code Geeks
S
SegmentFault 最新的问题
D
Docker
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
M
MIT News - Artificial intelligence
博客园 - 【当耐特】

OhYee 博客

小鹏辅助驾驶测评|OhYee 博客 小鹏非支持手机开启自动解锁|OhYee 博客 使用函数计算实现 301 重定向|OhYee 博客 针对 HTML 内容使用 Ant Design 图片弹框|OhYee 博客 博客进程泄露及僵尸进程解决|OhYee 博客 蓝易云服务器体验|OhYee 博客 SSH 调起本地 VSCode|OhYee 博客 【2022 秋招内推】阿里云后端研发工程师|OhYee 博客 使用函数计算获取 IP 地址信息|OhYee 博客 正确获取客户端 IP/HTTP Header 也可能重复|OhYee 博客 评测 Oculus Quest2 及 BigScreen|OhYee 博客 NextJS 热重载保留状态|OhYee 博客 如何优雅地贴 gist 代码|OhYee 博客 Linux 精细化文件权限|OhYee 博客 VSCode 容器开发环境|OhYee 博客 Clash 的不兼容更新排查|OhYee 博客 Zeek 导出 PCAP|OhYee 博客 记一次 ssh 配置问题|OhYee 博客 Git Commit 规范化工具|OhYee 博客 谈谈《星之卡比-探索发现》|OhYee 博客 VSCode 快捷键绑定 Shell 命令|OhYee 博客 ASN.1 语法及 X.509 证书格式解析解析|OhYee 博客 腾讯企业邮箱忽略 MX 记录发信|OhYee 博客 Chrome/Edge 标签组插件|OhYee 博客 【应届内推】阿里云后端研发工程师|OhYee 博客 损坏的 Typecho 备份处理为 JSON|OhYee 博客 VS Code VIM 插件高效使用|OhYee 博客 SSH 正反向代理|OhYee 博客 Let's Encrypt 根证书过期引发的问题|OhYee 博客 OpenWRT 忽略内核依赖|OhYee 博客
【弃案】在博客中插入思维导图|OhYee 博客
2020-11-08 · via OhYee 博客

【弃案】在博客中插入思维导图

首先检索了常见的可交互思维导图功能:

  • 可放大、缩小
  • 点击节点可折叠
  • 支持多种格式的数据导入(如 Markdown、JSON……)

从这些中选取了两个较为不错的思维导图选择:

前者基于 d3.js 渲染,支持右向拓展的思维导图。色彩丰富,支持 Markdown 输入,社区较为活跃。
后者则是百度脑图的引擎,相比较之下可能不太好看,但是功能强大,使用百度内部的 kity 渲染,但官方已弃坑。

博客渲染思路

无论是前端 Markdown 渲染还是后端 Markdown 渲染,原理上都只是将渲染内容原封不动渲染为 HTML(使用特定标签标记),而后页面前端检索所有该标签,并重新渲染该标签内容。

以前端 Markdown 渲染使用的 markdown-it 为例,只需要实现一个针对代码块的监听插件即可,它会将对应的内容渲染至<div class="mindmap-svg">。(在这里使用列表来支持多种触发标签,为了处理后续可能存在的多种输入格式(如输入 Markdown 渲染思维导图或直接输入 JSON 渲染思维导图)

const mindmapPlugin = (md) => {
  const temp = md.renderer.rules.fence.bind(md.renderer.rules);

  md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
    const token = tokens[idx];
    const { ids = ['mindmap'] } = options;

    if (ids.indexOf(token.info) !== -1) {
      try {
        const data = token.content.trim();
        return `<div class="mindmap-svg" type="${token.info}">${data}</div>`;
      } catch (ex) {
        return `<pre>${ex}</pre>`;
      }
    }

    return temp(tokens, idx, options, env, slf);
  };
};

export { mindmapPlugin };

接下来只需要在渲染页面的时候,检索所有的mindmap-svg类即可

resetMindMap() {
  const mindmaps = document.getElementsByClassName('mindmap-svg');
  for (var i = 0; i < mindmaps.length; ++i) {
    try {
      const mindmap = mindmaps[i];
      if (!mindmap.getAttribute('rendered')) {
        const text = mindmap.innerHTML;
        console.log(mindmap);
        console.log(text);
        mindmap.setAttribute('rendered', '1');
        ReactDOM.render(<MindMap markdown={text} width="100%" height="50vh" />, mindmap);
      }
    } catch (e) {
      console.error(e);
    }
  }
}

markmap 组件

官方推荐的是前端引入 d3.js 和 markmap-view.js,不过在这里直接从依赖里引入也只是让包稍微大了一点而已。同时解决了可能存在的引入顺序问题。(如果判断是否存在思维导图标签再引入 d3.js,可能会存在引入问题)

import React from 'react';
import Head from 'next/head';

import { ComponentProps } from '@/utils/component';

import { transform } from 'markmap-lib';
import { Markmap } from 'markmap-view';

export declare type MindMapProps = ComponentProps<{
  markdown?: string;
  width?: React.ReactText;
  height?: React.ReactText;
}>;

export function MarkMap(props: MindMapProps) {
  const { markdown = '', ...restProps } = props;
  const ref = React.useRef<SVGSVGElement>();
  React.useEffect(() => {
    try {
      const { root, features } = transform(markdown.trim());
      var el = Markmap.create(ref.current, { autoFit: false }, root);
    } catch (e) {
      console.error(e);
    }
    return () => {
      el.styleNode.interrupt();
      el.g.interrupt();
      el.svg.interrupt();
    };
  }, [markdown]);
  return <svg ref={ref} {...restProps}></svg>;
}

整体思路就是渲染出一个<svg>标签,在渲染结束后初始化 Markmap 对象,并绑定上述的标签。在组件销毁时,对内部的标签进行处理。

尽管看上去没有问题,但是在销毁时,d3.js 本身的动画等待并未结束,因此可能会发生组件已销毁,但是动画还在持续,导致找不到对应的组件的错误。尽管有人提到可以使用.interrupt()解决,但是测试似乎仍然不太有效(不过也有可能是函数组件的问题,如果换成对象组件,可能会有更为严格的组件生命周期)
相关讨论见 d3/d3-timer#32 Saving handle to call cancelAnimationFrame is required. #32

整体来说,除去上述可能会报错的问题外,这个组件体验还是很不错的,如果后续有精力可以尝试进一步想办法解决。

kityminder

作为百度的良心产品,百度脑图支持的功能可以说是非常不错了(相对于其他的前端思维导图渲染)。不过默认不支持滑轮缩放,因此需要自己去实现(这里自己监听了下滚轮操作,效果勉强能用)

由于这时一个上古项目,而且本身是要求浏览器引入的,因此导入的 NodeJS 包仍然需要从window上获取对应的变量。剩下的操作与之前的类似。只是不太确定为什么会存在ref.current=undefined的情况。

export function KityMinder(props: MindMapProps) {
  const { markdown = '', ...restProps } = props;
  const ref = React.useRef<HTMLDivElement>();

  const minder = React.useMemo(() => {
    var kityminder = (window as any).kityminder;
    if (!kityminder) {
      require('kity');
      require('kityminder-core');
      kityminder = (window as any).kityminder;
    }
    if (!!ref.current) {
      ref.current.innerHTML = '';
      var minder = new kityminder.Minder({ renderTo: ref.current });
    } else {
      var minder = new kityminder.Minder();
    }
    return minder;
  }, []);
  React.useEffect(() => {
    minder.renderTo(ref.current);
  }, [ref]);
  React.useEffect(() => {
    try {
      minder.importData('markdown', markdown);
      minder.disable();
      minder.execCommand('hand');
      minder.on('mousewheel', (e) => {
        const minder = e.minder;
        if (e.originEvent.wheelDelta > 0) {
          minder.zoom(e.minder.getPaper().getViewPort().zoom * 100 + 5);
        } else {
          minder.zoom(Math.max(1, e.minder.getPaper().getViewPort().zoom * 100 - 5));
        }
        e.preventDefault();
        return false;
      });
      (window as any).mm = minder;
    } catch (e) {
      console.error(e);
    }
  }, [minder, markdown]);
  return <div ref={ref} {...restProps}></div>;
}

export default KityMinder;

这个组件实现下来,起码没有奇怪的报错,但是由于其本身并没有实现放大缩小、触摸移动,可能需要较多的工作量处理这些东西。

总结

当然,最终导致我放弃这两个的原因是,他们都会减慢渲染速度。由于 JS 本身单线程,他们的渲染耗时将会直接导致编辑框打不进去字。或许是时候把分块渲染提入日程,不过暂时没有较好的实现思路。