MoreRSS

site iconUsubeni Fantasy修改

常用SSShooter zhoudejie等ID,本名周德杰 ,Web前端开发,坐标广州。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Usubeni Fantasy的 RSS 预览

久违的生活碎片记录

2026-03-12 18:43:14

失业之后多次想写点东西,也确实写了,不过都是零散地写在 Obsidian 里面,时间过了,又暂时不想整理发到博客。于是今天还是直接在博客写吧,最近的非日常生活。

2026-02-17

初二被 CC 邀请去他家烧烤,一扫除夕初一的无聊。工具食材都到位了,结果生火生了好久都生饿了。

山姆的羊扒真香嘻嘻!

从天亮吃到天黑,要是我找不到工作,要不要落魄前端在线烧烤呢(

最后摸摸 CC 的猫,乖乖的好猫~

2026-02-28

迫于准备结婚,过完年 2 月底,把送礼的任务完成,又解决一件事。结婚真花钱呀。

2026-03-11

快要领证了,在领导强烈建议下去修脸,换一个更好懂的词,那就是美容。第一次修脸,感觉还不错。

这是直接在大众点评搜的一家店,在石围塘地铁站附近,就地理位置来说比较偏僻,但是因为也住得偏僻,所以一拍即合了。

店的评分很高,可能因为位置比较偏,价格跟同类相比也算实惠,一百多的套餐,服务还挺多的,躺了一个多小时顺便当休息了。按摩挺舒服的最后一步都快睡着了🥱。洁面和剃须的步骤有点小痛不过第二天没什么问题,摸着是挺滑的。

2026-03-12

之前看到工行有羊毛,换一千外汇送积分和微信立减金,于是换了些港币,今天去取。

发现了一个问题,储蓄卡过期了不能在柜台取钱。我倒是知道卡过期了,但是提示只写着过期后 ATM 不能取钱,没想到柜台也不能取。

这一刻,我终于记起来了,ATM 的全称是自动柜员机,柜员机不行,所以柜员也不行(狗头)。

那怎么办呢,就换卡呗,然后被告知工本费 20 元。绝了,宇宙行是我见过第一家办储蓄卡还收工本费的银行。贵行开成宇宙行的资金,就是从这里薅来的吧?

换卡取钱,完事之后我突然想起来,噢,我旧卡还能拿回来吗?被告知不行,已经被剪了,而且不能拿回来。我知道这个需求也是比较怪,但那好歹也是大学交学费的卡,跟了我十几年,它就这样被砍头了,尸骨都不能交由我处理它后事,有点伤感。

一个插件让你在 Obsidian 画思维导图

2026-03-10 13:42:32

最近新鲜出炉的一个 Obsidian 插件 mindelixir-mindmap:https://github.com/SSShooter/obsidian-mindmap

主要有两个功能:

  1. 让你可以以思维导图的形式阅读 markdown 文件
  2. 让你可以在 markdown 文件中插入思维导图

markdown 转思维导图

mindelixir-mindmap 可以根据标题和列表的层级关系把 markdown 文件转换为思维导图。

Mind Elixir Plaintext

Mind Elixir Plaintext 是一种类似 markdown 嵌套列表的格式,不过加上了连线、总结和样式的语法。

你可以通过简单的缩进、ID 引用和类似于 JSON 的尾部声明,快速在文本里构建复杂的思维导图结构。同时,这种结构 AI 生成起来也非常方便。

- 产品研发流程
  - 调研阶段 [^research]
    - 用户访谈 {"color": "#3298db"}
    - 竞品分析 {"color": "#3298db"}
    - }:2 调研总结
  - 开发阶段 [^dev]
    - 架构设计 {"color": "#2ecc71"}
    - 前后端联调 {"color": "#f39c12"}
    - } 开发总结
  - > [^research] >-进入-> [^dev]

Mind Elixir Plaintext 同样也可以作为代码块嵌入到现有的文章中,顺便看看移动端的显示效果:

普通 markdown 只能通过编辑文本更新思维导图,针对 Mind Elixir Plaintext 文本,现在正在开发编辑思维导图反向更新文本的功能。

安装方式

尽管已经提交了官方插件列表的 PR,但是现在 AI 时代随手出插件,前面一千个 PR 排着队……我估计维护团队都要放弃审批第三方插件了。

所以呢,下面推荐两种非官方安装方式。

BRAT

BRAT 是一个已上架的 Obsidian 插件,本意是可以让你更方便地测试你的插件。但是实际上你完全可以用这个插件来安装生产级的插件。

在社区插件列表搜索 BRAT 安装:

安装后在 BRAT 配置里点击 Add beta plugin 按钮,填入 https://github.com/SSShooter/obsidian-mindmap,就能自动安装思维导图插件:

手动安装

不想使用 BRAT 也可以进入插件 Release 页面下载以下 3 个文件:

  • main.js
  • style.css
  • manifest.json

然后在 Obsidian 的设置中,打开插件目录,建一个文件夹把这三个文件放进去,然后刷新一下插件列表即可。

目前 mindelixir-mindmap 仍在持续迭代优化中,如果你在使用中遇到任何问题,或是对新功能有什么好想法,非常欢迎到 GitHub 提交 Issue 和 PR 🤗

看完就懂 useSyncExternalStore

2026-02-27 16:24:32

功能

React 引入 useSyncExternalStore 也很长一段时间了,但是存在感还不太强。简而言之,它专门用来搞定那些不受 React 内部生命周期控制的外部数据源

过去最大的问题其实是 React 渲染时的 「撕裂」,这是 React 为了优化页面响应速度引入的并发渲染机制带来的副作用。

简单来说就是 React 为了防止在渲染时长时间无法响应用户输入,把渲染过程拆分成多个可中断的小任务,这就能小任务的间隙中插入用户响应,从而模拟出「并发」的感觉。更完整的前因后果可以参考《React 的设计哲学》

在 React 并发渲染机制下,如果用普通的 useEffect 去同步外部数据,可能会出现渲染进行到一半时数据突然发生变化,导致同一份页面中,一半的组件拿着老数据,另一半拿着新数据的灵异现象(但是实际上出现这个问题的几率其实非常小,大家都忽略了,这就导致了 useSyncExternalStore 的存在感很低)。使用 useSyncExternalStore 后,如果在渲染过程中快照发生变化,React 会丢弃当前渲染并重新开始,从而保证同一次提交中的所有组件看到的是同一个版本的数据。

使用场景

订阅浏览器 API

拿监听网络状态来说。不使用这个 Hook 之前,我们通常得在组件里写个包含完整挂载和清理逻辑的 useEffect 去监听 onlineoffline 事件。

function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

// 组件里直接这么用
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

监听媒体查询(Media Queries)响应式布局也是同样的套路:

const query = window.matchMedia("(max-width: 600px)");

function subscribe(callback) {
  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

const isMobile = useSyncExternalStore(subscribe, () => query.matches);

轻量级全局状态

如果你接手了一个极小的项目,不想引入 Redux 或 Zustand 这样繁琐的包,但又迫切需要在几个跨层级的组件间共享某部分状态。这时候你可以直接手搓一个简易的 Store:

// 丢在 React 外面的状态中心
let internalState = { count: 0 };
const listeners = new Set();

const store = {
  increment() {
    internalState = { count: internalState.count + 1 };
    listeners.forEach((l) => l());
  },
  subscribe(callback) {
    listeners.add(callback);
    return () => listeners.delete(callback);
  },
  getSnapshot() {
    return internalState;
  },
};

// 任何组件里都可以直接同步获取状态
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

注意:useSyncExternalStore 内部用 Object.is 比较前后快照,如果 getSnapshot 在数据未变的情况下每次都返回新对象,会导致无限循环重渲染。

只要把这段代码看懂,你就掌握了 Zustand 这种现代状态管理库的核心原理

竞品 API

useEffect + setState

曾经大家都习惯在 useEffect 里监听外部变化,如果变了,再跑一下 setState 触发更新。

这就又到了日常批判 useEffect 的时候了。

useEffect 带来重复渲染和闪烁问题。如果你的外部状态和页面初始计算的状态不对齐,页面渲染就会经历「旧值 -> 闪烁 -> 新值」这三步。而 useSyncExternalStore 在渲染中途就能直接取走最新的正确值。

另外,在处理服务端渲染时,用副作用很容易抛出水合(Hydration)错误,因为服务端和客户端首次生成的 HTML 大概率因为外部数据对不上。useSyncExternalStore 为此专门开了一个叫 getServerSnapshot 的参数,让你传能兜底服务端的静态快照。

Context

很多人滥用 Context 做全局状态,但如果是频繁变动的数据,Context 的广播机制简直是一场灾难。只要 Provider 提供的值发生了变动,它底下所有的子组件也会跟着无脑重跑 Render,除非你给每个组件层级套一层 React.memo(当然现在有 compiler,但也不是毫无代价)。

相比之下,useSyncExternalStore 实现了高精度的按需订阅——只有从 Store 取出的快照真的有了变化,关联的组件才会再次渲染。在这里还是顺便强调一下,没事别用 Context。

总结

要判断何时使用 useSyncExternalStore 其实很简单,只要你的数据依然在 React 的生命周期里流转(例如表单实时输入、控制弹窗开闭的布尔值),那就老老实实用回你的 useStateuseReducer

一旦数据满足游离于 React 管理之外、会随时间变化、且你要让 UI 能自动响应这种变化这三个条件,就毫不犹豫上 useSyncExternalStore。日常写前端页面也许碰不到几次,但之后你要是去造底层 Hook 库,或者需要硬啃第三方库内部暴露出的状态时,useSyncExternalStore 绝对好使~

相关链接

看完就懂 useLayoutEffect

2026-02-24 23:08:12

差异

useLayoutEffect 与大家熟悉的 useEffect 语法完全一致,从产生副作用的角度上看,功能上也是一样的,唯一差别就是调用时机。

useEffect 会在画面绘制后异步执行,而 useLayoutEffect 会在画面绘制前同步执行。为了讲清楚这个时机的具体区别,得先复习一下浏览器渲染页面的过程。

浏览器渲染流程

注意最后 js 运行的那一块,useLayoutEffect 和 useEffect 就分别位于 paint 之前和之后。

执行的顺序是:

  • useLayoutEffect
  • 画面绘制
  • 下一轮 js 运行 useEffect

顺便我们也能看出来,useLayoutEffect 之所以叫 useLayoutEffect 就是因为它的运行时间点沾着 layout。

使用场景

知道这两个函数的区别,我们还需要知道,到底什么时候用 useLayoutEffect 呢?

答案是,如果进行了 DOM 操作,且这个 DOM 操作会引起回流(reflow)、重绘(repaint),那么就应该使用 useLayoutEffect,例如:

function Tooltip() {
  const ref = useRef<HTMLDivElement>(null);
  const [pos, setPos] = useState({ top: 0, left: 0 });

  // 如果用 useEffect,这里会先渲染一次默认位置,再跳到正确位置 → 可能会造成闪烁
  useLayoutEffect(() => {
    const rect = ref.current!.getBoundingClientRect();
    setPos({
      top: rect.top + rect.height + 8,
      left: rect.left + rect.width / 2,
    });
  }, []);

  return (
    <>
      <div ref={ref}>hover me</div>
      <div style={{ position: 'fixed', top: pos.top, left: pos.left }}>tooltip</div>
    </>
  );
}

因为如果你用 useEffect,在浏览器绘制之后又要重新跑一遍 reflow、repaint,用户可能会看到画面“闪烁”。

如果你有代码洁癖,想要一个最优解,那么你确实该按上面说的这么做,但是事实上在这个场景使用 useEffect 可能也不会有很明显的问题。

其实即使是官网的例子里,作为反模式使用 useEffect,用户也不会感知到明显的“闪烁”,因为两次渲染的时间其实是快到肉眼看不清的,为了确定真的存在区别你还要故意写个 while 循环卡一下主进程。

既然一般情况下无论 useEffect 和 useLayoutEffect 都不会有明显区别,那么我觉得,作为一个有专业素养的 React 开发者,应该优先使用 useEffect,只在 reflow、repaint 造成闪烁的场景下,使用 useLayoutEffect。

当然,useEffect本身也不能乱用,之前在useEffect 清除计划里已经讲述了它的必要使用场景。

总结

useLayoutEffect 适用于“需要在浏览器绘制前同步完成的副作用”,典型场景是读取布局信息并立即修改 DOM,避免视觉闪动。

但因其会阻塞浏览器绘制,影响性能,因此不应滥用。在绝大多数副作用场景下,优先使用 useEffect,只有在感知到闪动才改为使用 useLayoutEffect。

相关链接

复习 DOM 事件机制

2026-02-08 17:30:53

本期前端复习的主题是 DOM 事件机制,是前端开发非常重要的基础。除了常听到的“冒泡”,完整的事件流其实更有意思。

事件传播

完整的 DOM 事件传播分为三个阶段:

  1. 捕获阶段(Capturing Phase)
    • 事件从 window 一路向下传递到目标元素的父节点。
    • 期间可以通过 addEventListener(type, listener, true) 第三个参数设为 true 来监听此阶段。
  2. 目标阶段(Target Phase)
    • 事件到达目标元素本身,即 event.target
    • 此阶段监听函数会被触发。
  3. 冒泡阶段(Bubbling Phase)
    • 事件从目标元素向上传播至 window
    • 默认通过 addEventListener(type, listener, false) 注册的事件监听器会在这个阶段触发。

但也不是所有事件都支持冒泡,例如 focus, blur 等就不冒泡,具体各个事件是否支持冒泡可以 w3c 官方文档

监听事件

<div id="outer" class="box">
  Outer
  <div id="middle" class="box">
    Middle
    <div id="inner" class="box">Inner</div>
  </div>
</div>

事件监听注册如下:

const boxes = ["outer", "middle", "inner"];
boxes.forEach((id) => {
  const el = document.getElementById(id);

  // 事件捕获阶段
  el.addEventListener(
    "click",
    (event) => logEvent("捕获阶段", id, event),
    true, // 捕获阶段
  );

  // 事件冒泡阶段
  el.addEventListener(
    "click",
    (event) => logEvent("冒泡阶段", id, event),
    false, // 冒泡阶段
  );
});

可以参考这个示例:https://codesandbox.io/p/sandbox/w93cgd

阻止传播

调用 event.stopPropagation() 可以阻止事件传播。注意,这不仅能停掉冒泡,在捕获阶段也能把事件截下来。

举个例子:

child.addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("child");
});

此时点击按钮,只会输出 child,不会触发 parentgrandparent 的监听器。

同理,如果是在捕获阶段调用 stopPropagation(),事件就无法到达目标元素和冒泡阶段:

parent.addEventListener(
  "click",
  (event) => {
    event.stopPropagation();
    console.log("parent capture");
  },
  true,
); // 注意第三个参数 true 开启捕获

此时点击子元素,事件在 parent 被截获,child 的点击事件将永远不会触发

常见场景

  1. 防止重复触发(阻止冒泡)
    • 场景:点击卡片中的按钮(如“删除”),但不希望触发卡片本身的点击事件(如“跳转详情”)。
    • 做法:在按钮的点击事件中调用 event.stopPropagation()
  2. 全局拦截(阻止捕获)
    • 场景:页面进入“编辑模式”或“引导模式”,需要禁用页面上所有元素的点击交互,只允许特定区域或完全接管交互。
    • 做法:在 window 或最外层容器上监听 click 事件(设置 capture: true),并调用 event.stopPropagation(),这样内部元素都无法收到点击事件。

如果同一个元素绑定了多个监听器,想把后续的监听器也一并拦住,得用 event.stopImmediatePropagation()

默认行为

浏览器会对某些事件执行默认动作。例如:

  • 点击 <a> 标签会跳转链接。
  • 点击表单的提交按钮会提交表单。
  • 在输入框按键会输入字符。
  • 选中文本后右键会弹出上下文菜单。

我们可以使用 event.preventDefault() 来阻止这些默认行为。

跟不是所有事件都会冒泡一样,也并非所有事件的默认行为都能被取消。可以通过 event.cancelable 属性查看事件是否可取消。

passive

passive 的意思是“被动”。用这个选项,等于向浏览器承诺:在这个监听器里,我绝不调用 preventDefault()

既然你不打算阻止滚动,浏览器就不用等你的 JS 跑完再动,页面滚动自然就丝滑多了。否则,浏览器为了确认你会不会拦截滚动,每一帧都得等你,很容易卡顿。

现代浏览器为了优化体验,默认把 touchstartwheel 等滚动事件设为 passive: true。也就是说,你在这个监听器里调 preventDefault() 是没用的。如果非要阻止滚动,必须在绑定时显式加上 { passive: false }

// 默认情况下 passive 为 true,preventDefault() 无效
document.addEventListener("touchstart", function (e) {
  e.preventDefault(); // 控制台会显示警告,滚动无法阻止
});

// 显式设置 passive: false,preventDefault() 生效
document.addEventListener(
  "touchstart",
  function (e) {
    e.preventDefault(); // 阻止滚动
  },
  { passive: false },
);

阻止传播与默认行为的影响

别搞混了:阻止传播(Stop Propagation)阻止默认行为(Prevent Default) 是两码事。

  • stopPropagation():让事件不再通过 DOM 树传播(冒泡/捕获),但阻止浏览器执行默认动作。
  • preventDefault():告诉浏览器不要做默认动作,但阻止事件在 DOM 中的传播。

连锁效应

虽然两者独立,但要注意一个事件的默认行为可能是触发另一个事件

例如,在输入框中按键,keydown 事件的默认行为通常包括“将字符输入到文本框”。如果你在 keydown 阶段调用了 event.preventDefault(),浏览器就会取消这个默认动作,即使你阻止的不是 input 的默认行为,字符也不会出现在输入框中。

示例

有个常见的坑:粗暴地在全局阻止默认行为。例如,为了禁用某个快捷键,但在 document 上直接阻止了所有 keydown 的默认行为:

document.addEventListener("keydown", (e) => {
  // 这会导致整个页面的输入框即使获得焦点也无法输入文字
  // 因为“输入文字”也是按键的默认行为之一
  e.preventDefault();
});

所以在调用 preventDefault() 前,一定要加条件判断(比如只针对特定键码 e.key === 'Enter' 阻止)。

事件委托

这是冒泡最实用的功能。

有了冒泡,**事件委托(Event Delegation)**才成为了可能:

document.getElementById("parent").addEventListener("click", (e) => {
  if (e.target.tagName === "BUTTON") {
    console.log("Clicked button:", e.target.id);
  }
});

这样可以只给 parent 绑定一次事件监听器,而不需要为每个 button 单独绑定,提高性能。

这里就得区分 targetcurrentTarget 了:

  • target 是事件触发的具体目标元素。
  • currentTarget 是事件监听器绑定的当前元素。

不得不吐槽,这个命名属实有点抽象,久了不用就总会把 currentTarget 记成是当前触发的元素,这就反了😂

<div id="parent">
  <button id="child">Click me</button>
</div>

<script>
  const parent = document.getElementById("parent");

  parent.addEventListener("click", function (e) {
    console.log("target:", e.target);
    console.log("currentTarget:", e.currentTarget);
  });
</script>

点击按钮 <button id="child"> 时:

  • e.target<button>:你点的元素
  • e.currentTarget<div>:绑定事件的元素(parent)

总结

  • 传播机制:事件流分为捕获、目标、冒泡三个阶段。日常开发主要利用冒泡进行事件委托,但在特定场景下捕获阶段也可以用于拦截事件。
  • 行为控制:区分 stopPropagationpreventDefault。前者阻止事件继续传播,后者阻止浏览器的默认行为(如表单提交),两者互不影响。
  • 性能优化:滚动类事件(如 touchstart, wheel)建议使用 passive: true,明确告知浏览器不会调用 preventDefault,从而让页面滚动更加流畅。
  • 对象区分event.target 是实际触发事件的元素,event.currentTarget 是绑定监听器的元素。在处理复杂的组件嵌套时,分清这两个属性非常重要。

参考文献

  • https://w3c.github.io/uievents/#event-type-keydown
  • https://developer.mozilla.org/en-US/docs/Web/Events

读完书容易忘?这个开源 AI 应用能帮你!

2026-01-30 14:06:59

其实我们不必回避看完书就忘的问题,因为大多数人看书都是会忘的。其实人类的大脑就是这么设计的,它会过滤掉大部分不重要的信息,只保留下重要的信息。如果真的想要记住一本书重要的知识,需要反复阅读,反复思考,反复练习。

在前 AI 时代,做读书笔记是一件非常耗费精力的事情,但是有大模型之后,我们可以在做笔记这件事上偷偷懒。

注意:做笔记可以偷懒,但是思考和反复回看是绝对不能偷懒的。

那么有什么好用的工具呢?朋友们,有的!欢迎使用 ebook-to-mindmap!简单来说,你可以通过 ebook-to-mindmap 把 pdf 或 epub 格式的电子书转换为分章节的思维导图或者文字总结。

思维导图模式

点击这里即可立即体验。整个网页应用功能比较简洁,大家可以直接上手,当然,下面我也会比较详细地介绍一下这个应用的使用方法🤗

模型配置

使用 ebook-to-mindmap 的第一步是配置模型。它和很多 AI 应用一样,都是选择 byok(Bring Your Own Key)的模式,你可以在这里配置你自己的大模型。

这里还是要强调一下,在 ebook-to-mindmap 填写 Key 时不必担心 Key 泄露,因为 Key 只是保存在你自己的浏览器里,请求也是直接从你的浏览器发送到大模型提供商的服务器的。你可以在浏览器的开发者工具里查看网络请求,确认这一点。同时,ebook-to-mindmap 作为一个开源项目你可以随时检视它的代码,还可以自己部署一个属于你的 ebook-to-mindmap。

说回模型的选择,可能很多人会担心使用 ebook-to-mindmap 的花费太高,其实倒也不必,毕竟现阶段还是能找到很多免费或者低价的大模型。我的首推还是 openrouter,你只需要充值 10 刀,就能获得一个较大的免费模型(其中包括一些 deepseek 变体、最近小米的新模型、之前一段时间还有 grok)使用额度,基本上一天让它处理好几本书都没问题了。其他详细推荐可以参考免费和付费 AI API 选择指南

model list

在获取到 Key 后如上图填写信息即可。

你还可以配置多个模型,点击左侧的星星后会成为默认模型,后续处理时默认使用星标的模型:

model list

生成笔记

配置模型后,在主页选择电子书即可。之后 ebook-to-mindmap 会自动识别电子书的格式,然后开始识别章节:

AI 总结页面

[!TIP] 提示:如果 epub 无法获取到章节,可以在设置里勾选使用 Spine 获取章节

章节识别成功后,选择你需要总结的章节,或者使用分组功能(可以使用快捷键 Ctrl + G)把零碎的章节组合成分组一起发送给 AI 处理。

一切准备好后,点击开始解释按钮即可开始生成笔记。

默认情况下,ebook-to-mindmap 会生成思维导图,你也可以点击小齿轮切换到文字总结模式:

模式切换

[!TIP] 虽然有整书思维导图生成功能,但是如果书的内容比较长,AI 可能吃不下这么长的上下文,所以建议还是分章节生成,最后系统会自动拼接

生成笔记如果想要中途取消,放心点取消就好,之前处于完成状态的章节会被缓存,不用担心之后需要再浪费 Token 重新生成。

提示词

举个例子吧,你在提示词列表里添加一个“小·红书风格”提示词,在生成环节选择这个提示词,就能直接生成小红书风格的笔记。

小红书风格

不止小红书风格,你也可以让 AI 只简单地提取该章节最重要的 5 个观点,帮助你对整本书的主要内容有一个简要的了解。

你还可以使用“反论法”提示词:

选取本章的核心论点或思想,并探索它的对立面。如果作者要为相反的观点辩护,他们需要证明什么?文本中是否有无意间支持反面观点的蛛丝马迹?

参考分享几条有意思的 NotebookLM 提示词这篇文章,里面有几个有趣的提示词,或许能让你眼前一亮。

内容管理

ebook-to-mindmap 充满了下载按钮,是的,你生成的数据必须还是属于你的!你可以很轻易地把数据拿出来!

导出的文字内容可能是 markdown 文件或是思维导图 json 文件。

markdown 文件可以直接阅读,或者导入到 Obsidian、Notion 等笔记软件再细化修改。

思维导图 json 文件可以使用 mind-elixir-core 等前端库渲染,当然,如果你是技术人员,理解 json 数据的结构你也可以随意修改和渲染。

思维导图亦可导出为图片,点击思维导图页面右上角的下载按钮即可。

格式选择

最后谈谈电子书格式的问题,ebook-to-mindmap 支持 pdf 和 epub 格式的电子书,但是这两种格式如何选择呢?

或许大家都会比较喜欢看 pdf,因为看起来比较工整,但是使用 ebook-to-mindmap,我还是比较推荐 epub 格式的电子书。

稍微讲一下 pdf 和 epub 的原理吧。

pdf 的特点是在任何设备上看起来都一样,这就很容易想到,其实 pdf 的排版是非常固定的,而且更重要的是,pdf 的排版是没有语义的。也就是说,人类能看到一个标题是加粗黑字,但是 pdf 本身并不知道这是一个标题,它只是知道这一块区域的文字是加粗黑字的。

更严重的问题是 pdf 如果有一些复杂的排版,例如在角落嵌入一段文字,在解释的时候就很难理解那段文字的意义。所以,大模型理解 pdf 的难度会比较大。

而 epub 格式就不一样,它更像是一张网页,有语义,有结构,有层次,就跟 HTML 差不多。但缺点就是人类看来这样的排版有点粗糙,在不同的阅读器上显示效果也不同。在某些落后的 epub 阅读器上阅读时可能会觉得排版很有年代感。但是大模型不在乎排版,有清晰的结构就能得到好的输出结果。

写在最后

总的来说,ebook-to-mindmap 是一个能帮你快速复习或者把书本变薄的工具。在这个信息爆炸的时代,高效地获取和整理知识变得越来越重要。希望这个小工具能成为你阅读路上的得力助手,让你把更多的时间花在深度思考和理解上,而不是机械地摘抄。

如果你觉得这个项目对你有帮助,欢迎在 GitHub 上点个 Star ⭐️ 支持一下!如果你有任何建议或发现了 bug,也欢迎提 Issue 或者加入讨论。

Happy Reading!