news 2026/9/20 11:22:16

用BrewUI把Homebrew打包成可视化Web应用,包管理更直观

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用BrewUI把Homebrew打包成可视化Web应用,包管理更直观

如果你跟我一样,平时习惯在终端里靠 Homebrew 管理软件,一定遇到过这样的场景:同事问某个软件是怎么装的,你啪啪啪敲一串brew listbrew searchbrew info,对方看完还是一脸茫然。Homebrew 的命令确实不难,但对那些不习惯终端的人来说,它就像一堵很高的墙。这就是我做BrewUI的最直接动机——把 Homebrew 这支命令行工具,包一层本地运行的图形界面,让高频操作像逛网页一样简单。

BrewUI 本质上是一个本地 Web 应用。它不会替代brew本身,而是在brew和用户之间加一个可视化层,帮你完成搜索、安装、卸载、升级、依赖查看、服务管理等常见任务。你打开浏览器,输入 localhost 地址,剩下的就是点按钮、看状态。对于想接触 Homebrew 但恐惧终端的用户来说,这是一个很低的入门门槛;对于熟悉命令行的老手,也能在可视化大列表里更快地核对包的状态。下文会把 BrewUI 的核心设计思路、技术细节、完整实现步骤和踩坑记录都摊开来讲。

1. 项目概述与核心痛点

1.1 先搞清楚:Homebrew 到底有哪些“难受”的点

Homebrew 是 macOS 和 Linux 上最常见的包管理器之一,理论上它已经足够好用,命令短、生态全、更新快。但“足够好用”是针对已经习惯命令行的人而言的。把视角切到真实使用场景,你会发现几个很现实的痛点。

第一个痛点是信息密度太低brew searchbrew list输出的都是纯文本列表,看起来像一坨连续字符串,不装第三方工具很难一眼看清哪些包已经过时、哪些包依赖了什么、哪些包占了多少空间。第二个痛点是操作不可逆感太强brew uninstall一下就把包装卸掉了,初学者根本不知道它会连带卸掉什么;brew upgrade更像是开盲盒,升级完有的软件行为变化了,你只能干瞪眼。第三个痛点是服务管理不直观brew services list的输出虽然已经结构化,但终端里看表格始终不如图形界面舒服。

这些痛点并不是 Homebrew 自身设计有问题,而是 CLI 交互的上限就在那里。BrewUI 要做的不是改进命令行本身,而是用图形界面把这些高频操作重新组织一遍。如果你负责帮家里长辈装软件,或者帮刚转行的同事配环境,你会发现对方需要的不是“学命令行”,而是一个足够直观的操作面板。BrewUI 就是这个面板的雏形。

1.2 BrewUI 的目标用户与适用场景

我在设计 BrewUI 时,把目标用户分成了三类。第一类是初次接触 Homebrew 的开发者或爱好者,他们知道 brew 很强大,但面对终端有点发怵,希望有一个安全的试错入口。第二类是需要管理多台开发机的工程师,图形列表比记忆一堆命令更快,还能直观比对不同机器上装的包。第三类是只想用好软件而不想折腾配置的人,他们不关心底层怎么实现,只要“一键安装”“一键卸载”就够了。

适用场景也分三个层次。日常最常用的场景是搜索和安装软件:输入关键字,看到匹配列表,点一下安装按钮,页面滚动日志,最后显示安装成功。第二个场景是系统体检:打开已安装列表,一眼看到哪些包有新版本,哪些包很久没更新,哪些包有依赖问题。第三个场景是服务管理:启动、停止、重启,查看运行状态,全部通过按钮完成。

我刻意没把 BrewUI 做成“实时监控终端输出”的工具。命令行的高阶操作仍然有必要保留在终端里,比如自定义构建参数、切换镜像、调试安装脚本。BrewUI 的定位是覆盖 80% 的日常操作,剩下 20% 的深度控制权继续留给终端。这样设计既降低了用户的学习成本,也让 BrewUI 的内部逻辑不会因为过度封装而变得脆弱。

1.3 项目范围:核心功能清单

在动手写代码之前,我先列了一份功能清单,避免做到一半需求蔓延。BrewUI 第一版只做五件事:

  • 已安装软件包列表展示,包含版本号、安装时间、占用空间等基础信息;
  • 软件包搜索与详情查看,支持 formula 和 cask 两种类型;
  • 安装、卸载、升级操作,带实时日志输出与任务状态管理;
  • 依赖关系展示,方便卸载前评估影响范围;
  • 后台服务(brew services)的列表、启动、停止、重启。

这五件事覆盖了 Homebrew 日常使用时 80% 以上的打交道频率。另外我还留了一个扩展位:批量更新。第一版先实现“全部升级”的简单按钮,后续再考虑做成可勾选更新的形式。

功能范围确定后,项目的技术难点也跟着浮现了。核心问题有三个:怎么稳定拿到 brew 的结构化数据、怎么处理安装任务的长耗时和中断、怎么确保 Web 界面在本地使用足够安全。下一节就讲我针对这些问题做的技术选型。

2. 技术选型与整体设计思路

2.1 为什么不做传统桌面应用?——本地 Web UI

BrewUI 第一个替代方案是直接用 Electron 写桌面应用。Electron 的好处是界面现代、交互流畅、用户不用开浏览器;坏处是打包体积大、内存占用高、开发效率也不高。对一个围绕命令行工具做包装的小项目来说,Electron 的体量有点杀鸡用牛刀。

第二个替代方案是用 Swift 写 macOS 原生应用。原生应用体验当然好,但只能跑在 macOS 上,没办法兼顾 Linux,而且 SwiftUI 和 AppKit 的新手门槛不低,开发速度也慢。我最终选择了本地 Web 应用方案:后端用 Python 的 Flask,前端用原生 HTML/CSS/JavaScript,启动后监听127.0.0.1的某个端口,用户通过浏览器访问。

选择这个方案的核心原因是平台兼容性最好。Homebrew 本身支持 macOS 和 Linux,一份 Python 代码可以同时在两个平台上跑,不需要针对不同系统单独编译。另一个原因是调试成本低。Flask 自带开发服务器,改完代码保存就能热加载,前端直接在浏览器开发者工具里调样式,比桌面应用开发环境轻太多了。

当然,本地 Web 应用也要面对一些缺点,最明显的是界面风格受限,以及用户习惯“应用应该是一个独立窗口”的心理预期。我的解决办法是启动时自动打开默认浏览器,并给页面加上一个简单的桌面应用样式——顶部有导航栏、左侧有侧边栏、卡片式布局,尽量让网页看起来像一个工具而不是一个网站。

2.2 命令层设计:如何安全调用 brew

BrewUI 的底层操作归根结底就是调用brew命令。怎么调、怎么解析输出、怎么处理异常,是决定项目稳定性的关键。

我严格遵循一个原则:不要用 shell=True,不要拼接字符串命令。所有调用都通过subprocess传入参数数组,例如["brew", "list", "--formula", "--json=v1"]而不是"brew list --formula --json=v1"。这么做首先避免了 shell 注入风险,其次免去了对引号、转义字符的纠结。用户在搜索框输入的关键字只作为参数传给搜索命令,Python 会安全地处理空格和特殊字符。

获取结构化数据时,我优先让 brew 自己输出 JSON。Homebrew 对listinfo命令都支持--json参数,返回的数据里包含了公式名、版本号、依赖项、安装路径等信息。直接解析 JSON 比解析普通文本要稳定得多,哪怕 Homebrew 改版导致文本格式变化,只要其 JSON 字段还保持兼容,我的解析代码就不用大改。

命令执行分两种模式。快速查询类命令,比如搜索、查看版本、查看服务状态,用subprocess.run()同步执行,加一个超时时间。安装、卸载、升级类命令,执行时间可能从几秒到几分钟,必须放在后台线程里执行,前端通过轮询接口获取任务状态。一开始我计划用 WebSocket 推送日志,后来考虑到 Flask 自带的开发服务器对 WebSocket 支持不算好,就退而求其次用轮询方案。实测下来,每秒轮询一次日志更新完全没有性能问题。

2.3 模块划分与目录结构

BrewUI 代码结构不复杂,但要保证后续能扩展,所以从一开始就按职责拆了模块。我的最终目录结构是这样的:

BrewUI/ ├── app.py # Flask 应用入口,路由与页面渲染 ├── brew_core.py # 与 Homebrew 命令交互的核心层 ├── task_manager.py # 安装/卸载任务状态管理 ├── requirements.txt # Python 依赖清单 ├── templates/ │ └── index.html # 单页应用的 HTML 模板 └── static/ ├── app.js # 前端逻辑:请求接口、渲染列表、轮询任务 └── style.css # 样式文件

app.py只负责 HTTP 接口和页面跳转,不直接写操作命令;brew_core.py封装了所有针对brew的调用;task_manager.py管理后台任务的启动、状态更新和日志记录。这个分层的好处是,如果以后想给 BrewUI 增加新的 Homebrew 操作,只需要在brew_core.py里补一个函数,再在app.py里加一个路由就行。

前端虽然是单页,但我没有用 Vue 或 React,原因很简单:这个项目的界面复杂度有限,原生 JavaScript 完全够用。用框架反而要多维护一套 node_modules,给用户徒增安装成本。前端的核心交互就三个:拉取列表、渲染页面、轮询任务状态,原生 fetch 和 DOM 操作完全可以胜任。

3. 核心模块的实操实现

3.1 已安装软件包列表:用 JSON 接口拿数据

列表页是所有后续操作的基础。BrewUI 首页第一时间就要展示当前机器上装了哪些包,所以我先写的就是list的封装。

Homebrew 在较新版本中支持brew list --formula --json=v1brew list --cask --json=v1,输出是一段 JSON 数组。我分别拉取 formula 和 cask 的数据,合并到一个列表里,前端用标签区分类型。一个简化的核心函数长这样:

import json import shutil import subprocess BREW_BIN = shutil.which("brew") or "/opt/homebrew/bin/brew" def run_brew(*args): proc = subprocess.run( [BREW_BIN, *args], capture_output=True, text=True, timeout=60, ) if proc.returncode != 0: raise RuntimeError(proc.stderr.strip()) return proc.stdout def list_installed(): formulae = json.loads(run_brew("list", "--formula", "--json=v1")) casks = json.loads(run_brew("list", "--cask", "--json=v1")) result = [] for item in formulae: result.append({ "name": item["name"], "version": item.get("installed", [{}])[0].get("version", "unknown"), "type": "formula", "dependencies": item.get("dependencies", []), }) for item in casks: result.append({ "name": item["name"], "version": item.get("version", "unknown"), "type": "cask", "dependencies": [], }) return result

这里有几个细节需要注意。首先,shutil.which("brew")是为了兼容不同安装路径。Intel Mac 上 Homebrew 装在/usr/local/bin,Apple Silicon 上装在/opt/homebrew/bin,直接写死路径容易出问题。其次,--json=v1这个参数标志的是 JSON schema 版本,不是 Homebrew 版本,这个字段目前仍然是稳定可用的。最后,安装时间在 JSON 里有install_time字段,但不同版本格式略有不同,我第一版先展示版本号和依赖数,等后续再补全安装时间。

3.2 搜索与软件详情页

搜索功能的实现比想象中简单。brew search命令会输出一行或多行文字,我们只需要简单解析一下。不过有个坑:brew search返回的数据是文本流,不是 JSON,而且可能会同时返回 formula 和 cask 两个列表。我的处理办法是直接拿文本去前端展示,搜索结果的每一项都提供一个“去详情”按钮。

详情接口用brew info --json=v1 <name>拿数据,返回的是单 formula 的 JSON 对象,其中包含了desc(描述)、homepageversionsdependenciesbuild_dependencies等字段。前端详情页用卡片式布局展示,顶部是包名和版本,中间是描述文字和官方链接,下面用标签页区分“依赖关系”和“安装信息”。

搜索是高频操作,性能必须快。brew search本身执行速度还行,但每次都调一次命令仍然有延迟,所以我加了一个很简单的缓存:把搜索结果按关键字存到内存字典里,过期时间是五分钟。对于本地工具来说,这个方案够用且不需要引入 Redis 之类的重型组件。

3.3 安装、卸载与升级任务管理

这是整个 BrewUI 最核心也最容易出问题的地方。安装一个包可能要下载几十上百兆文件,页面不能一直等着,必须把任务放到后台线程执行,并给前端提供查询状态的接口。

我写了一个TaskManager类,用一个全局字典保存任务状态:

import threading import uuid class TaskManager: def __init__(self): self.tasks = {} def start(self, task_type, package): task_id = uuid.uuid4().hex[:8] self.tasks[task_id] = { "type": task_type, "package": package, "status": "pending", "log": "", "exit_code": None, } thread = threading.Thread( target=self._run, args=(task_id, task_type, package), daemon=True, ) thread.start() return task_id def _run(self, task_id, task_type, package): self.tasks[task_id]["status"] = "running" args = [BREW_BIN, task_type, package] proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) for line in proc.stdout: self.tasks[task_id]["log"] += line proc.wait() self.tasks[task_id]["exit_code"] = proc.returncode self.tasks[task_id]["status"] = ( "success" if proc.returncode == 0 else "failed" ) def get(self, task_id): return self.tasks.get(task_id) def prune(self, max_tasks=100): # 简单内存清理,限制保存的任务数量 if len(self.tasks) > max_tasks: keys = list(self.tasks.keys()) for key in keys[:-max_tasks]: del self.tasks[key]

关键设计有三点。第一,Popenstdoutstderr合并输出,避免日志顺序错乱。第二,把daemon=True设上,避免 Python 进程退出时因为非守护线程而卡住。第三,任务日志会无限增长,所以我给任务数设了一个上限,超出后按时间顺序丢弃旧任务。

前端页面收到安装请求后,立即启动任务,然后每隔一秒调用一次/api/task/<task_id>获取最新状态,把日志渲染到页面的文本框里。安装成功后,刷新已安装列表。

3.4 依赖关系可视化

依赖管理是 Homebrew 使用中最容易让人心虚的部分。brew uninstall一个包时,你有没有想过它会不会带走一些共享依赖?brew deps --tree <package>能在终端里输出一棵依赖树,但纯粹的文字树在复杂场景下不好读。BrewUI 的方案是把依赖树渲染成可折叠的目录树结构。

后端先调用brew deps --tree <package>,拿到缩进文本,然后按缩进层级解析成树形结构:

def parse_deps_tree(text): root = {"name": "[\u9879\u76ee\u672c\u8eab]", "children": []} stack = [(-1, root)] for line in text.splitlines(): if not line.strip(): continue indent = len(line) - len(line.lstrip()) name = line.strip() node = {"name": name, "children": []} while stack and indent <= stack[-1][0]: stack.pop() stack[-1][1]["children"].append(node) stack.append((indent, node)) return root

前端拿到树形 JSON 后递归渲染成可折叠列表。用户点开一个节点,会看到它依赖的下游包;点枝干末端,可以继续进入该包的详情。这样卸载之前能先看一眼影响范围,心里有底。

3.5 后台服务(services)管理

Homebrew 的 services 命令管理的是通过 brew 安装的后台服务,比如数据库、消息队列、Web 服务。brew services list会输出一个文本表格,包含服务名、状态、用户和启动路径。直接解析文本表格虽然能用,但列宽不固定,容易出错。我选择先把输出按行拆分,再按空白字符切分,取出前两列作为服务名和状态。

状态管理操作分三种:startstoprestart。这类命令同样需要放在后台线程中,不过它们通常执行时间不长,日志也比较少。我在前端把服务列表做成了一个表格,每一行末尾有“启动”“停止”“重启”三个按钮。点击后调用对应接口,操作完成后刷新表格。

4. BrewUI 的安装与使用

4.1 环境准备与安装步骤

我这里把 BrewUI 的部署过程整理成一个清晰步骤,方便你自己复现。假设你已经安装了 Python 3.9 及以上版本,也安装了 Homebrew。

把 BrewUI 的代码克隆到本地后,首先创建虚拟环境,避免污染系统全局 Python 环境:

cd BrewUI python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt

requirements.txt里的内容很简单,核心只有 Flask 一个依赖:

Flask>=3.0.0

我这里刻意没加其他重量级库。日志解析、JSON 处理、后台任务全部用 Python 标准库完成,能用标准库解决的绝不引入第三方依赖。这样不仅安装更快,也减少了未来依赖冲突的可能。

4.2 启动 BrewUI 并打开界面

启动命令很简单:

python app.py --port 8000

启动后,BrewUI 会默认在浏览器打开http://127.0.0.1:8000页面。如果没有自动打开,你手动访问这个地址也行。需要特别注意的是,BrewUI 默认只监听本机回环地址,这意味着同一台机器上的浏览器才能访问,局域网内的其他设备无法访问。这种安全设计是故意的,因为 brew 命令涉及安装和卸载软件,绝对不应该暴露给网络上任意设备。

页面加载后,侧边栏有四个导航入口:已安装搜索服务升级。首页默认展示已安装列表,顶部有一个搜索框和一个“清理旧版本”按钮。整体布局维持简洁风格,没有多余的花哨元素,毕竟工具类项目最重要的就是效率。

4.3 一次完整的实操演示

我拿一个具体的例子来演示 BrewUI 的完整使用流程。假设我想安装一个名为htop的系统监控工具。

在搜索页输入htop,点击搜索,系统调用brew search并将结果渲染成卡片列表。点击 htop 那项进入详情页,可以看到描述、版本和依赖信息。页面右下角有“安装”按钮,点击后会创建一个任务,日志区域实时显示下载进度和安装输出。当日志末尾出现固定的安装完成提示时,任务状态变为成功。

安装完成后,切到已安装列表,可以看到 htop 出现在列表中,版本号是当前安装的最新版。点击 htop 的详情,可以看到它的依赖项,如果我想卸载它,界面会先展示依赖树,提醒可能会有其他包依赖它。确认无影响后,点击卸载按钮,任务完成后列表刷新,htop 消失。

整个流程不需要打开终端敲一条命令。对于不熟悉命令行的用户,这才是理想的使用方式。

5. 常见问题与排查技巧实录

5.1 提示“brew: command not found”怎么办

BrewUI 通过shutil.which("brew")来定位 brew 可执行文件。如果你在终端可以正常使用brew,但 BrewUI 提示找不到命令,多半是因为启动 BrewUI 的环境和终端的环境不一样。比如你通过 IDE 或 launchd 启动了 BrewUI,这时候 PATH 环境变量可能没有包含/opt/homebrew/bin

解决办法是在brew_core.py里手动指定路径,或者启动前在终端里执行source ~/.zprofile。我建议直接在代码开头留一个可配置常量,默认值填充常见安装路径,用户按需修改。这个设计很小,但能省掉环境不一致带来的大量排查时间。

5.2 页面刷新慢或者接口超时

如果你在执行安装任务时,页面一直转圈,有可能是另一个 brew 命令正占着进程锁。Homebrew 自己有一个锁机制,同一时刻只允许一个安装或更新操作执行。如果用户在终端里已经跑着一个长时间的brew install,BrewUI 发起的命令会一直排队等待。

这种情况在日志里表现为没有新内容输出,任务状态一直停留在 running。排查方法是先回终端看看有没有其他 brew 进程,有的话等它结束,或者手动终止。BrewUI 这边可以优化成:启动任务前先执行pgrep -f "brew install"检查是否已有安装进程,如果有就在前端提示“brew 正在被其他任务占用”。

5.3 端口被占用

Flask 默认使用 5000 端口,如果你机器上已经跑了其他应用,启动 BrewUI 会报错。最简单的办法是启动时换一个不常用端口,我习惯用 8000 或者 3721。如果嫌每次手动敲端口麻烦,可以在代码里写一个端口自动递增的逻辑:端口被占用时自动尝试下一个端口,启动后把最终的访问地址打印出来。

5.4 服务状态显示不准确

我遇到过一种情况:某个服务明明已经停止了,但brew services list的表格里仍然显示它存在。这是因为 Homebrew 的记录文件没有更新,或者该服务的 plist 文件已经被手动删除。遇到这种显示异常,最简单的处理是用brew services cleanup清理残留记录,然后再刷新 BrewUI 的服务列表。

5.5 安全使用建议

最后重点说安全。BrewUI 拥有执行任意 brew 命令的能力,这意味着如果它被外部设备访问,别人可以远程安装、卸载、升级软件,风险非常大。我强烈建议:

  • 永远不要用--host=0.0.0.0启动 BrewUI,除非你明确知道自己要做什么;
  • 默认监听127.0.0.1,必要时用系统防火墙限制端口访问;
  • 不要在公网云主机上部署 BrewUI;
  • 如果确实需要远程使用,至少加上一层 Token 认证,并在前面架设 HTTPS 网关。

我在 BrewUI 中内置了一个简单的访问令牌机制,启动时指定--token参数,前端请求接口时需要在请求头携带对应的令牌。这个机制不是绝对安全,但能挡住绝大多数误访问。

5.6 附:常见问题速查表

问题现象可能原因解决办法
提示 brew 找不到PATH 环境变量不一致检查解释器环境,手动指定 brew 路径
安装任务长时间无响应brew 进程锁被占用检查是否有其他 brew 命令在执行
页面无法访问端口被占用换端口启动,或排查占用端口的进程
服务列表不更新残留 plist 记录执行brew services cleanup
日志中文乱码终端编码问题设置PYTHONIOENCODING=utf-8
卸载后列表仍有残留缓存未刷新手动清理浏览器缓存或刷新按钮

6. 几个后续可以扩展的方向

做完第一版 BrewUI 之后,我自己用了一周,整体感受是“日常装机真的方便很多”。特别是帮别人远程处理问题时,我只需要让他打开 BrewUI 页面,把界面截图发给我,我就能告诉他点哪里,再也不用反复纠正他敲错命令。这个替代交流成本的价值,已经超过了 BrewUI 本身实现的价值。

后续有几个可以继续扩展的点。第一个是批量升级,现在的“升级”按钮是一次性全部升级,后续可以做成可勾选列表,让用户手动选择升级哪些包。第二个是安装统计与磁盘空间分析,brew 命令能拿到安装路径和包大小,可以在界面上做一张饼图或柱状图,直观展示哪类包最占空间。第三个是多机器管理,如果 BrewUI 支持配置远端机器列表并复用底层接口,就能在一个浏览器页面管理多台开发机。

还有一个我私下在摸索的功能是安装脚本回放。很多项目要求先装一堆依赖包,再跑一条安装命令,整个流程可以在 BrewUI 里录制成配置,下次一键复现。这样新同事入职配环境时,不用再对着 README 手动敲一长串命令。

从个人经验来说,BrewUI 最成功的设计不是技术亮点,而是把“命令行的不安全感”降到了最低。新手看到按钮,敢点下去;点错了,能根据日志和状态判断接下来怎么办。这种心理门槛的降低,才是图形界面相比终端最不可替代的价值。如果你也经常被身边的人问“这个命令怎么敲”,不妨试试这个思路,把常用操作封装成界面,既帮了别人,也省了自己的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 11:18:55

BetterNCM Installer 安装与插件失效排查全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:15:43

Visual Studio 2022 的 Copilot Chat,模型通道改到 TaoToken 通道行不行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:15:34

BrewUI:macOS原生Homebrew图形化工作流平台

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:10:49

OrcaTerm性能优化实战:从DOM泥潭到Canvas分层渲染

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华