【弃案】在博客中插入思维导图
首先检索了常见的可交互思维导图功能:
- 可放大、缩小
- 点击节点可折叠
- 支持多种格式的数据导入(如 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 本身单线程,他们的渲染耗时将会直接导致编辑框打不进去字。或许是时候把分块渲染提入日程,不过暂时没有较好的实现思路。