BrewUI:给Homebrew包管理器打造一个可视化的Web界面
折腾了差不多一个月我在周末时间把 BrewUI 这个项目从零搭了出来。它的定位很简单给 Homebrew 包管理器做一套图形化操作界面。Homebrew 在 macOS 和 Linux 上用得非常多日常装软件、更新依赖、清理旧版本一行命令基本都能搞定。但命令行这个东西对不熟悉终端的人来说始终有一道门槛而且即使是经常用命令行的开发者在大量包需要升级、依赖关系变得复杂的时候光靠一行行 brew outdated 和 brew upgrade 去盯也会觉得累。BrewUI 就是冲着这个痛点去的打开一个本地 Web 界面所有包列表、版本状态、可升级项一目了然安装、卸载、升级、查看依赖都能用鼠标完成底层命令自动生成并执行。这个项目适合三类人看想给 Homebrew 管理工作降噪的日常用户受够了反复敲命令的老手以及打算自己动手做一款开发工具 UI 的同学。文章会把整个设计思路、技术选型、核心实现和踩坑过程完整拆开讲里面涉及的原理不局限于 BrewUI 本身换成任何“命令行工具被迫拥有图形界面”的场景都适用。1. 项目定位与整体设计思路1.1 这个工具到底解决了什么问题先说一个我自己很深的感受Homebrew 的命令本身设计得已经相当顺手绝大多数操作其实只需要两三个单词。但真正的问题不在于“命令记不住”而在于“状态不可视”。你装了一百多个包之后哪些包有新版本哪些包已经不再被任何东西依赖哪些包占用的磁盘空间最大这些信息不是不能查而是要组合好几条命令外加人工对比输出结果。BrewUI 的第一版核心功能就瞄准了这三个场景包列表可视化、升级状态可视化、依赖关系可视化。它把 brew list、brew outdated、brew info、brew deps 这些命令的输出变成结构化数据再通过一个本地 Web 页面展示出来。用户不再需要自己在终端里翻屏找重点打开页面就能看到“当前有 17 个包可升级其中 3 个是安全更新最大占用空间的是 X”这些信息比一堆文本列表直观得多。还有一个常见场景是“帮别人解决问题”。不少同事出了问题会截图终端报错给我来回沟通成本很高。有了 BrewUI 之后我直接把页面截给他们列出已装包和异常状态定位问题的速度明显更快。这其实说明了一个通用规律开发者工具的核心价值未必是“替代命令行”而是“降低信息理解成本”。1.2 为什么在命令行工具旁边还要做一个 UI大多数人第一次听到 BrewUI 的反应是Homebrew 用终端不就好了为什么要搞个界面这个质疑很合理我一开始也是这么想的。但真正推动我去做的是在一台长期不用的旧 MacBook 上维护环境时产生的烦躁感。那台机器上 Homebrew 自己提示有 40 多个包可以升级我花了大半个小时一条一条地看哪些需要升、哪些会连带升级依赖、哪些可能破坏现有环境。命令行能给我答案但给答案的方式非常“原始”一大堆包名和版本号混在一起依赖关系要靠 brew deps 单独查升级影响范围要靠 brew outdated --verbose 的结果自己脑补。图形界面的优势恰好在这里它不改变底层操作只改变信息的组织方式。一个设计良好的 UI 能把“包名、当前版本、最新版本、依赖了谁、被谁依赖、是否可升级、升级风险等级”这些属性平行铺开人在这种信息密度下做判断的速度会快很多。这不是说命令行不行而是不同形态的工具适合不同状态的大脑——工作了一天之后我真的不想再盯着终端输出做信息整合了。另外UI 还有一个命令行天然缺失的能力操作前的可视化确认。在终端里执行 brew uninstall 某个包命令一旦回车就没有回头路。在 BrewUI 里卸载按钮被点击后界面先展示这个包的依赖清单和“谁还在用它”让用户二次确认然后才执行。这一个交互流程就能避免不少手误。1.3 项目形态与适用场景BrewUI 最终做成了“本地后端 浏览器前端”的形态。后端用 Go 编写监听 127.0.0.1 上的随机端口负责执行 brew 命令并把结果解析成 JSON API前端是 React SPA所有页面静态构建后由后端直接托管。这么做的好处有三个第一不用给用户增加额外依赖任何装有现代浏览器的机器都能用第二跨平台特性天然成立Homebrew 支持的系统就是 BrewUI 支持的系统第三本地服务模式规避了“桌面应用签名和分发”的麻烦只要把可执行文件丢给用户就行。适用场景我整理了一下主要集中在这么几类个人环境维护快速查看所有包状态一键清理不需要的旧版本。开发机批量管理配合后端服务一次管理多台机器的包环境。团队知识分享把包依赖关系截图放进文档比贴一长串命令输出清楚得多。新人上手引导刚接触 Homebrew 的人用界面了解每个命令具体做了什么再去看命令行会容易得多。2. 技术选型与关键决策2.1 UI 技术路线的对比与取舍这是整个项目里最值得展开的部分。给“命令行工具做 UI”其实不止一种做法我已经把主流路线都试了一遍各有各的坑。第一条路是纯前端方案直接在浏览器里用 JavaScript 库去调用 brew。这条路最先被我否决因为浏览器安全模型不允许网页随便执行本地命令除非通过 WebSocket 之类的中转。真要这么干还是得写一个本地服务等于多绕了一圈。第二条路是 Electron。Electron 的优势是生态成熟、组件丰富一套代码搞定界面。但它的问题也很突出打包体积轻松超过 100MB内存占用随便两三百 MB对一个本来只想做“查看 brew 包状态”的小工具来说体量完全不成比例。第三条路是 Tauri。Tauri 用系统 WebView 渲染界面后端用 Rust打包体积小、内存占用低理论上非常适合这种小工具。但它要求开发者有 Rust 编译环境跨平台构建时还需要处理一堆系统 WebView 的兼容问题我折腾了两天之后决定放弃。第四条路就是最终选择的 Go 后端 React 前端。 Go 编译出来的单个二进制文件只有十几 MB没有外部运行时依赖配合前端静态文件做嵌入式托管非常方便。React 只负责渲染和交互所有的命令执行、数据解析、格式转换全交给 Go 后端职责边界很清楚。加上我本身对 Go 比较熟这套方案最终胜出没什么悬念。这里其实有个很重要的选型原则如果一个小工具需要同时解决“命令交互”和“界面展示”两个问题尽量把命令交互放在编译型语言后端前端只做展示层。这样调试方便部署简单而且后端可以独立测试不用每次改界面都带着 brew 命令一起跑。2.2 后端进程模型的思考BrewUI 后端的核心任务只有一个执行 brew 命令并解析输出。但这个任务要想做好比看起来复杂不少。Homebrew 的 command 系列里brew info 可以输出 JSONbrew list 可以加 --formula 限定只看软件包brew outdated 也有对应的 JSON 输出方式。但 brew install 这类“动作型”命令是实时的会持续输出进度信息如果后端只是简单地把进程执行完再返回结果用户在前端就会看到好几十秒的空白期体验非常差。所以我在后端设计上引入了一个简易的“命令执行队列”所有 brew 子命令被封装为任务对象后端通过 goroutine 异步执行每 200 毫秒向前端推送一次进度事件。前端通过 Server-Sent Events 接收这些事件再在界面上渲染出“正在下载依赖”“正在安装”这样的实时状态。这样既保留了 brew 命令的完整输出又不需要前端去轮询几十个接口。另一个值得提的设计细节是“read-only 命令与写命令的隔离”。brew list、brew outdated、brew info 这类只读查询可以随时并发执行brew install、brew upgrade、brew uninstall 这类写操作则必须串行。否则两个写命令同时跑Homebrew 自带的锁机制会直接报错甚至可能互相等死锁。我把两类操作分成了两个执行通道读操作可以并行写操作排队执行这个设计后来救了我无数次。2.3 前端页面结构与交互模式前端整体分四个核心页面仪表盘、包列表、依赖图、操作记录。仪表盘展示整体状态已安装包数量、可升级数量、磁盘占用 Top 榜单、最近操作历史。这个页面承担的信息密度最高所以我在设计时刻意做了一些“克制”——不堆卡片只放四到五个必要区块核心指标用大数字展示细节全部折叠到次级页面。包列表是使用频率最高的页面。默认按名称排序支持按“已安装”“可升级”“被依赖最多”“最近安装”等条件过滤。每一行展示包名、当前版本、最新版本、安装来源homebrew-core 还是第三方 tap、一句话描述。行尾的操作按钮只有三个升级、卸载、查看详情。依赖图用前端组件绘制成力导向图每个节点是一个包边表示依赖关系。因为 brew 的依赖关系往往很深限制默认只显示两层再深的点击节点扩展。这个页面做起来不难但很多“细节工程”花在了图布局的稳定性和性能上后面会专门细说。操作记录页本质上是一个日志查看器。后端每次执行 brew 命令都会记录完整输出前端把它按时间线展示出来方便用户回溯“上一次到底跑了什么、结果怎么样”。3. 核心实现细节与逻辑拆解3.1 安全地调用 brew 命令BrewUI 作为本地工具安全性是我最先考虑的问题。不能在界面输入框里直接把字符串拼进 shell 命令这是基本底线。后端的做法是所有 brew 命令通过 Go 的 os/exec 包执行命令名固定为 brew参数全部以数组形式传递不经过 shell 解释。cmd : exec.Command(brew, list, --formula, --jsonv2)关键点在于绝对不使用 exec.Command(bash, -c, ...) 这种写法。一旦经由 shell用户输入里的特殊字符分号、管道、重定向符号就可能被解释成新的命令等于把任意命令执行漏洞开给前端。参数数组的方式直接绕过了这层风险即使参数里带着奇怪的字符也只会被当作正则表达式或搜索关键字传给 brew而不是被当成新命令执行。另外brew 命令的路径也要处理。macOS 上 Homebrew 的默认安装路径是 /opt/homebrew/bin/brewApple Silicon或 /usr/local/bin/brewIntel用户可能会用自定义路径安装。BrewUI 的做法是在首次启动时依次探测常见路径并允许用户在设置页里手动指定探测结果缓存到配置文件里。3.2 结构化数据的获取与解析Homebrew 本身就提供了很好的 JSON 输出这是 BrewUI 能够成立的基础。核心是这三个命令brew info --jsonv2 --formula返回所有已安装包及其依赖、版本、安装信息。brew outdated --jsonv2返回所有有可用升级的包的当前版本和最新版本。brew info --jsonv2 --formula 查询特定包的完整信息。brew info --jsonv2 的输出结构非常庞大一个包的信息包含几十个字段name、full_name、desc、homepage、versions、installed、dependencies、build_dependencies、runtime_dependencies 等等。第一版 BrewUI 只用了其中一小部分但解析时还是要保留整个 JSON 对象因为页面上随时可能展示“这个包依赖了什么”以及“它被谁依赖”。稍微留意一下的话brew info 默认还会带上 --installed 之类的状态信息不同版本 Homebrew 的输出结构有细微差别。我处理的方式是用一个大结构体做兼容解析字段拿不到就置空不因为某个字段缺失就让整个页面打不开。这个思路在做第三方工具时非常实用上游数据格式只要还能产出结果解析层就不要因噎废食。type FormulaInfo struct { Name string json:name FullName string json:full_name Desc string json:desc Versions Versions json:versions Installed []Installed json:installed Dependencies []string json:dependencies Dependents []string json:dependents,omitempty }提到 dependents谁在依赖这个包brew info 的 JSON 里其实没有这个字段它只有 dependencies。所以 BrewUI 需要在后端自己建立“反向依赖索引”把所有包的 dependencies 读完一遍反推每个包被谁依赖。这个计算放在内存里做就行装了几百个包的情况下遍历一次的时间连一毫秒都不到不需要引入数据库。3.3 写操作的执行流程与状态追踪brew install、brew uninstall、brew upgrade 都是长时任务执行期间前端必须能感知进度。BrewUI 的做法是把每次写操作包装成一个任务任务状态分为 pending、running、success、failed、canceled。任务流程是这样的前端发起 POST /api/tasks请求体包含操作类型和包名。后端把任务放进写操作队列返回任务 ID。队列调度器按顺序执行任务每条 brew 命令用 exec.Command 启动。命令输出用一个 bufio.Scanner 逐行读取每读一行都通过 SSE 推送给前端。命令结束后任务状态更新为 success 或 failed记录退出码和 stderr 输出。这中间有个容易踩的坑brew 命令的 stdout 和 stderr 都有内容install 的进度信息走 stderr而正常输出走 stdout。如果只读 stdout界面上会什么都看不到。我在启动命令时把 stderr 重定向到同一个管道统一处理并且给行内进度\r 结尾的行做了特殊处理不然前端会把一条进度刷成几十行。这个细节在测试时特别明显不处理的话界面日志会原地爆炸。3.4 前端组件选型与数据流前端我用了 React 18 TypeScriptUI 组件库选了 Ant Design。选择 Ant Design 的原因很简单它的表格组件功能全自带筛选排序和分页对包列表这种典型的表格场景能省大量开发时间。依赖图视图形状组件用了 React Flow——这个库上手快、文档全拖拽缩放都内置好了不必自己写 canvas 交互。数据流方面没有引入 Redux 之类的状态管理库。BrewUI 的页面状态其实很简单包列表数据、任务状态、筛选条件这三个用 React 自带的 useState useReducer 就足够了。引入 Redux 只会增加模板代码量对这个小项目没有实际帮助。前端通过自定义 Hook 封装了 API 请求和 SSE 订阅页面组件拿到数据后只管渲染状态管理逻辑全部收敛到几个 Hook 里。一个比较有用的设计是“数据缓存分层”。brew list 的结果相对稳定用户不会频繁改动所以前端做了五分钟的本地缓存而 brew outdated 的结果变化较快每次都拉取实时数据。页面上手动刷新按钮后面接的是一个清理缓存接口点完才是真正的强制刷新。4. 实操过程从零到可用的完整实现4.1 项目结构与基础搭建先交代一下项目目录结构整体非常标准BrewUI/ ├── backend/ │ ├── main.go # 入口启动HTTP服务 │ ├── api.go # 路由与APIHandler │ ├── brew.go # brew命令封装与执行 │ ├── parser.go # 解析brew JSON输出 │ ├── tasks.go # 任务队列管理 │ └── index.html # 嵌入的静态文件占位 ├── frontend/ │ ├── src/ │ │ ├── pages/ # 四个页面组件 │ │ ├── hooks/ # useBrewData, useTaskQueue等 │ │ ├── components/ # 表格、标签、依赖图等复用组件 │ │ └── api/ # 请求封装 │ └── package.json └── Makefile后端启动的第一步是加载所有已安装包的数据。这里我在 main.go 里做了一次“预热加载”在服务启动时先异步跑一遍 brew info --jsonv2把结果解析后放进内存缓存接口直接读缓存返回而不是每次请求都去调 brew 命令。这样前端打开页面时几乎秒开体验接近原生应用。func loadBrewData() error { data, err : runBrewCommand(info, --jsonv2, --formula) if err ! nil { return err } var result BrewInfoResult if err : json.Unmarshal(data, result); err ! nil { return err } cache.Store(result) go loadOutdatedData() return nil }4.2 API 设计与接口文档后端 API 一共就七个功能边界都很清楚方法路径功能GET/api/packages获取所有已安装包信息GET/api/packages/:name获取单个包详细信息GET/api/outdated获取所有可升级包GET/api/deps获取依赖关系图数据POST/api/tasks发起安装/卸载/升级请求GET/api/tasks/:id查询任务状态GET/api/eventsSSE 事件流接口数量少的好处在于前端研发可以直接对着文档写不用来回对字段。比如 /api/packages 返回的格式是这样{ data: [ { name: wget, current_version: 1.21.4, latest_version: 1.24.5, desc: Internet file retriever, dependencies: [openssl3, pcre2], dependents: [some-tool], outdated: true, size: 1247290 } ], total: 128, outdated_count: 17 }前端拿到这份数据之后筛选和排序都在本地做不需要频繁请求后端。只有点击“立即刷新”按钮时才会清除后端缓存并重新拉取。4.3 包列表页面的实现要点包列表是 BrewUI 的门面实现质量直接决定用户对工具的第一印象。核心代码其实就是 Ant Design Table 的配置但有几个细节值得单独拎出来说。第一表格筛选不能只靠前端字段过滤还得支持按“是否可升级”“安装来源”“是否有反向依赖”等计算属性过滤。我在数据加载完成之后构建了一个索引对象提前把包按这些维度分好组筛选时直接查索引效率远高于每次渲染都遍历全量数组。第二行操作的二次确认。升级、卸载按钮点击后不直接调接口而是先弹出一个 Modal展示影响范围。卸载 wget 时会显示“下列 3 个包仍依赖于 wgetxxx、yyy、zzz”用户确认后才会真正执行。这个交互极大减少了误操作尤其是共用开发机上的“清理旧包”场景非常有价值。第三长列表性能。后端一次性返回几百个包的数据前端如果一次性渲染几百行复杂表格滚动时会卡顿。我用 Ant Design Table 内置的 virtual 属性开启虚拟滚动同时把每个包详情组件用 React.memo 包住尽量降低无关数据的渲染频率。实测在 600 个包的场景下滚动帧率能稳定在 60fps 左右。4.4 依赖关系图的实现依赖图页面用 React Flow 构建力导向图布局。难点在于数据量大的时候如何保证用户能看懂而不是画成一张蜘蛛网。默认只渲染两层以当前选中的包为中心第一层是它直接依赖的包第二层是这些依赖再依赖的包。被超过 N 个包共同依赖的“公共依赖”节点会单独高亮并标注“被 15 个包依赖”。这样即使用户完全不熟悉某个包也能快速判断它在整个依赖网络中的分量。布局算法直接用 React Flow 内置的 dagre 布局插件它输出的是分层有向图刚好符合依赖关系“上到下、一层层展开”的视觉预期。在数据上我做了降采样超过 80 个节点时才展开第二层否则全部展开。import { useMemo } from react; import ReactFlow, { Background } from xyflow/react; import dagre from dagre; function buildGraph(deps) { const g new dagre.graphlib.Graph(); g.setGraph({ rankdir: TB, nodesep: 30, ranksep: 60 }); deps.nodes.forEach((n) g.setNode(n.id, { width: 160, height: 40 })); deps.edges.forEach((e) g.setEdge(e.source, e.target)); dagre.layout(g); const nodes deps.nodes.map((n) { const pos g.node(n.id); return { id: n.id, position: { x: pos.x, y: pos.y }, data: { label: n.label } }; }); return { nodes, edges: deps.edges }; }第一版依赖图在超过 200 个节点时拖动会掉帧排查后发现是 React Flow 默认开启了“节点拖拽”和“边动画”两个特性后者其实是多余的。关掉边动画、关闭节点可拖拽这个图是只读关系视图不需要移动之后性能立刻好了很多。4.5 打包与发布流程BrewUI 的发布物就是一个 Go 编译出来的可执行文件前端静态文件通过 go:embed 打进二进制里。用户在目标机器上执行 ./brewui 就会自动打开浏览器并跳转到本地服务地址。编译的时候做了三件事一是交叉编译在一台 Linux 机器上直接编译 macOS ARM64 和 AMD64 两个版本二是压缩体积用 upx 把二进制从 17MB 压到 6MB 左右三是内置版本号通过 ldflags 把 git 版本号和构建时间写进二进制操作记录页面能看到当前工具的版本信息。GOOSdarwin GOARCHarm64 go build -ldflags -s -w -X main.version$(git describe --tags) -o dist/brewui-darwin-arm64 ./backend GOOSdarwin GOARCHamd64 go build -ldflags -s -w -X main.version$(git describe --tags) -o dist/brewui-darwin-amd64 ./backend cd frontend npm run build cd .. go generate ./...这套发布流程的好处是干净利落没有安装器没有依赖项下载解压就能跑。对开发者而言信任一个二进制文件比信任一个安装脚本要容易得多尤其是那些本机长期跑着大量开发工具的人群。5. 常见问题与排查技巧实录5.1 brew 命令执行缓慢甚至“看起来死掉”这是最早被反馈最多的问题。BrewUI 启动时预热加载要跑 brew info --jsonv2正常情况一两秒就能完成但某些环境里 Homebrew 会自动出发更新检查连带跑 brew update耗时能拉到几十秒甚至几分钟。现象就是界面白屏转圈半天用户以为工具坏了。排查思路是先看日志。我把每次 brew 命令的 stderr 都完整记录下来定位到是 brew 在执行自动更新。解决办法有两个一是在执行查询命令时关闭自动更新增加环境变量 HOMEBREW_NO_AUTO_UPDATE1二是把自动更新单独拆成一个可手动触发的动作放在设置页里。同时预热加载改成异步并发用户打开页面时先显示缓存结果五秒后数据仍未到位再显示加载状态。cmd.Env append(os.Environ(), HOMEBREW_NO_AUTO_UPDATE1)这一行代码直接让包列表的加载时间从平均 20 秒降到了 2 秒以内。至于 brew 本身依赖的 git 更新检查那是另一码事不影响正常查询。5.2 macOS 权限目录问题导致安装失败Homebrew 在 Intel Mac 上默认装到 /usr/local在 Apple Silicon 上装到 /opt/homebrew。前者可能出现目录权限问题如果用户切换到其他用户执行 brew install会因为没有 /usr/local 下某些目录的写权限而失败报错信息还是“Operation not permitted”。这个问题的处理不能光靠 UI 层面因为它是系统权限设计的一部分。BrewUI 在命令开始执行前会先检查目标安装目录是否存在以及当前用户是否可写如果不满足条件会出现提示“请先运行 sudo chown -R $(whoami) /usr/local/xxx”或引导用户用 brew doctor 排查。这里唯一要避免的就是在工具里内置 sudo 提权逻辑本地小工具一旦提权安全边界就崩了。5.3 数据不一致与 JSON 解析失败Homebrew 版本迭代比较快某个版本调整了 JSON 输出字段之后BrewUI 的解析器可能会解析失败。第一版我没做容错程序直接 panic后来改成解析出错时返回空数据并在前端显示警告横幅同时把完整原始输出存到日志文件。比较常见的不一致点有两个一是 brew info --jsonv2 在旧版本 Homebrew 里没有 installed 字段导致所有包的版本信息显示为空二是新版 Homebrew 把一些命令从 formula 挪到了 caskJSON 结构本身没问题但字段含义变了。我的方式是解析器尽量只读取必需字段不依赖过于精细的结构能用字符串处理的就不升级成对象解析降低对上游变化的敏感度。5.4 前端长列表渲染卡顿几百个包的数据量其实不大但如果把每个包都渲染成带操作按钮的完整 Table 行DOM 节点数量一旦上千交互就会开始卡顿。这个问题在依赖图组件尤其明显。处理方式前面提过虚拟滚动加 React.memo。这里再补一个关键点不要在 Table 行里渲染过于复杂的搜索规则。最初我在包列表里给每个包名都挂了 toast 提示、详情弹窗、依赖预览等组件后来把详情弹窗改成点击后再懒加载包名的 tooltip 改成纯 CSS 实现把那些在首屏不需要的计算全部后置渲染性能才真正稳定下来。5.5 多进程并发操作 HomebrewHomebrew 平时并发操作基本上是安全的它有自己的 lock 文件机制。但两个 install 操作同时启动时第二个进程会一直等待第一个释放锁并不会直接报错这个“等待”在 UI 上看起来就是“画面卡死了”。我在后端加写操作队列之后这个问题被从根上解决了。还有一个相关的小坑brew upgrade 和 brew install 混跑时Homebrew 的锁检查会出现不一致的失败。虽然不常见但保险起见所有写操作统一走队列串行执行这个策略已经跑了很久没再出过问题。6. 个人经验与后续扩展BrewUI 做下来我最深的一个体会是给命令行工具写 UI最难的从来不是 UI 本身而是对命令行工具行为模型的深刻理解。brew 的所有状态都来自命令输出前端只是这些状态的投影。只要后端把“命令执行→输出解析→状态缓存→事件推送”这条链路做扎实界面上怎么画都只是工作量问题。有几个经验特别想分享给同样打算做类似工具的人一是命令执行层一定要和业务逻辑层分开。BrewUI 里执行 brew 命令的部分是一个独立包不直接依赖 API 层。这样我可以单独跑测试模拟命令输出不用真正去装一个包。二是永远不要把命令输出直接推给前端渲染成文本。哪怕只是为了调试也要先解析成结构化数据前端再决定怎么展示。否则后面加搜索、加筛选、加统计全部要推倒重来。三是http服务地址用随机端口并只在回环地址监听。127.0.0.1 加随机端口既保证本机浏览器可以访问又不向局域网暴露任何接口。四是UI设计上多做减法少做加法。BrewUI 第一版我也做过“包大小可视化饼图”“安装历史趋势图”等功能后来发现这些功能对实际决策毫无帮助反而拖慢了页面加载。最终砍到目前这四个页面每个页面只回答一到两个核心问题整体逻辑反而清晰。后续扩展方向我已经列了几个计划支持批量记住用户的升级策略比如某些包固定不升级、支持导出包清单并在一台新机器上批量安装以及把任务执行结果做成可回溯的快照方便对比升级前后环境差异。不过这些功能不会急着加工具的第一原则永远是“能清晰回答用户当前遇到的问题”而不是把能想到的功能都塞进去。如果你也正在折腾类似的本地开发工具希望这篇文章能给你一点参考。一个小建议在动手写 UI 之前先花一个下午把所有底层命令的输出格式研究透数据模型定清楚后面所有页面的开发速度会快很多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →