阅读视图

发现新文章,点击刷新页面。
🔲 ☆

一天重写 JSONata,我用 400 美元干掉了公司 50 万美元的 K8s 集群

本文永久链接 – https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai

大家好,我是Tony Bai。

过去的几年,我们见证了 AI 编程工具从“玩具”到“神器”的进化。无数开发者都在分享自己效率翻倍的喜悦。

你有没有想过,用 AI 来完成一次“外科手术式”的精准重构,一天之内,就能帮你把公司每年烧掉的 50 万美元(约 360 万人民币)的服务器成本,直接砍到零?

这听起来像天方夜谭,但它真实地发生了。

就在前几天,以色列安全公司 Reco 的工程师 Nir Barak 发表了一篇极其硬核的博客。他详细复盘了自己是如何在一天之内,花费了仅仅 400 美元的 Token 费用,利用 AI 将一个用 JavaScript 编写的核心组件 JSONata,完美地重写为了纯 Go 版本,最终为公司节省了每年 50 万美元的开销,并带来了 1000 倍的性能提升。

这不仅仅是一个“AI 真牛逼”的简单故事。它背后揭示的,是一套足以改变我们未来架构选型和技术债偿还方式的“AI 驱动重构(AI-Driven Refactoring)”实用方法。

跨语言 RPC,微服务架构中最昂贵的“性能税”

要理解这次重构的意义有多么重大,首先得看看 Nir Barak 的团队曾经陷入了多深的泥潭。

他们的核心业务是一个用 Go 编写的高性能数据管道,每天处理数十亿的事件。但其中有一个环节,需要用到一个名为 JSONata 的查询语言(你可以把它想象成带 Lambda 函数的 jq)来执行动态策略。

尴尬的是,JSONata 的官方实现是 JavaScript 写的。

这就导致了一个极其痛苦的架构:他们的主业务 Go 服务,为了执行这些规则,不得不去远程调用(RPC)一个专门部署在 Kubernetes 上的庞大的 Node.js 服务集群。

这个“小小的”跨语言调用,给他们带来了三大噩梦:

  1. 恐怖的成本:为了扛住流量,这个 jsonata-js 集群常年需要维持 300 多个 Pod 副本,光是这部分,每年就要烧掉 30 万美元的计算资源。
  2. 惊人的延迟:一次最简单的字段查找,比如 email = “admin@co.com”,在 Node.js 内部执行可能只需要几纳秒。但算上序列化、跨进程网络往返的开销,一次 RPC 调用在啥也没干之前,150 微秒的延迟就先进来了。对于一个每天处理几十亿事件的系统来说,这简直是灾难。
  3. 意想不到的运维黑洞:随着业务增长,Pod 数量一度多到耗尽了 Kubernetes 集群的 IP 地址分配上限!

Nir Barak 的团队当然也尝试过各种小修小补:优化表达式、加缓存、甚至用 CGO 把 V8 引擎直接嵌进 Go 里……但这些都只是“头痛医头”,无法根治“跨语言”这颗毒瘤。

Cloudflare 的“抄作业”哲学

转机发生在前几周。Nir Barak 看到了 Cloudflare 那篇刷爆全网的文章《我们如何用 AI 在一周内重构 Next.js》。

Cloudflare 的做法极其“暴力”且有效:他们没有让 AI 去创造新东西,而是把 Next.js 现成的spec,以及包含几千个 case 的官方测试套件(Test Suite)直接扔给大模型,然后对 AI 下达了一个简单粗暴的指令:

“我不管你怎么实现,给我写一个能在 Vite 上跑通所有这些测试的 API 就行!”

Nir Barak 看到这里,瞬间被点醒了:“我们面临的问题一模一样!我们也有 jsonata-js 官方那套包含 1778 个测试用例的完整套件啊!”

与其让 AI 去搞创新,不如把它变成一个任劳任怨、24 小时待命的“代码翻译工”!

于是,他花了一个周末,用 AI 制定了一个极其清晰的“三步走”作战计划:

  1. 第一步(人类智慧):用 Go 语言把 jsonata-js 的测试套件先“翻译”过来。
  2. 第二步(AI 体力):把 JSONata 2.x 的官方文档和规范全部喂给 AI。
  3. 第三步(测试驱动):对 AI 下达指令:“开始写 Go 代码,目标是跑通第一步的所有测试用例。”

第二天,他按下了“开始键”。

7 小时,400 美元,13000 行 Go 代码

接下来的故事,充满了令人肾上腺素飙升的极客快感。

Nir Barak 坐在电脑前,看着 AI Agent 像一台失控的缝纫机一样,疯狂地生成 Go 代码、运行测试、读取报错、然后自我修正。

整个过程被划分成了几个“波次(Waves)”:先实现核心解析器,再实现内置函数,最后处理各种边缘 case。

在 AI 与测试用例的左右互搏之下,仅仅 7 个小时 后,奇迹发生了:

一个包含 13,000 多行纯 Go 代码的、名为 gnata 的全新 JSONata 实现诞生了。它完美通过了官方所有的 1778 个测试用例。

而这整个过程的成本呢?

400 美元的 Token 费用。

Nir Barak 在博客中晒出了一张截图,数据显示,在重构 gnata 的那一天,AI 生成的代码占比高达 91.7%

当他把这个 PR 提交到公司内部时,立刻有人质疑 ROI(投资回报率)。而他的回答简单粗暴:

“上个月,jsonata-js 集群的成本是 2.5 万美元。现在,是 0。”

百倍性能与意外之喜:“手术刀式”重构的深远影响

成本降为零已经足够震撼,但性能上的收益更是堪称“恐怖”。

这还只是开始。由于 gnata 是纯 Go 实现,Nir Barak 团队得以进行更深度的“魔改”:他们设计了一套两层评估架构。对于简单的字段查找,gnata 直接在原始的 JSON 字节流上操作,实现了 零堆内存分配(Zero Heap Allocations)!只有遇到复杂表达式时,才会启动完整的解析器。

在接下来的两周内,他们乘胜追击,用 gnata 的批量处理能力,替换掉了主数据管道中另一个极其臃肿、靠启动上万个 Goroutine 来并发处理规则的旧引擎。 结果:又省下了每年 20 万美元。

短短两周,两次“外科手术式”的重构,总共为公司节省了每年 50 万美元的开销。

最让人意想不到的是,这次重构还带来了组织层面的“意外之喜”:

gnata 是公司内部第一个完全由 AI Agent 大规模参与生成的 PR。在 Code Review 的过程中,团队成员被迫去学习如何分辨“AI 真正发现的并发 Bug”和“AI 瞎操心的代码格式问题”。这次经历,为他们后续制定全公司的 AI Code Review 规范积累了宝贵的实战经验。

小结:我们不再只是“氛围感编码”

在文章的结尾,Nir Barak 提到了 AI 大神 Andrej Karpathy 最近的观点,大意是:

“编程正在变得面目全非。在底层,深厚的技术专长正成为比以往任何时候都更强大的‘乘数效应放大器’。”

Nir Barak 感慨道,直到最近,他自己都对那种完全由 AI Agent 生成代码的“氛围编码(Vibe coding)”持怀疑态度。但 2026 年 2 月,成为了一个连他这样固执的开发者都无法忽视的“拐点”

gnata 的诞生,标志着我们不再只是用 AI 去写一些无关紧要的玩具项目。在拥有明确测试用例和边界规范的前提下,AI 已经具备了对生产环境核心组件进行“手术刀式重构”的惊人能力。

你准备好拿起这把名为“AI”的手术刀,去切掉你系统里那些最昂贵、最臃肿的“技术肿瘤”了吗?

资料链接:https://www.reco.ai/blog/we-rewrote-jsonata-with-ai


今日互动探讨:

在你的公司里,是否存在类似的“异构技术栈”调用导致的性能瓶颈或成本黑洞?你有没有想过,可以用 AI + 测试用例的方式,对某个核心组件进行“代码翻译”式的重构?

欢迎在评论区分享你的架构痛点与大胆构想!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

性能之战的“罗生门”:Go 重写 Node.js 项目,究竟赢在了哪里?

本文永久链接 – https://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon

大家好,我是Tony Bai。

在当今的后端开发圈,“用 Go/Rust 重写 Node.js/Python 项目”似乎成了一种政治正确。在许多开发者的刻板印象中,只要换上静态编译语言,性能就能获得“降维打击”般的提升。

然而,真实世界的工程往往是一出“罗生门”——不同的人看着同一份数据,得出的结论截然不同。

近日,在 GitHub 的某个开源项目reverse-shell中,开发者公布了一份极其详尽的 Go 重写版 vs 原生 Node.js 版 的性能基准测试报告。面对这份数据,Go 的拥趸看到了内存消耗的断崖式下降,而 Node.js 的铁粉则指着热启动(Warm Path)的耗时反击:“看,V8 引擎依然能打!”

这绝不是一场单方面的碾压,Go 并没有在所有维度上将 Node.js 钉在耻辱柱上。本文将基于该 Issue 提供的真实 Benchmark 数据,从执行耗时、内存占用、CPU 消耗以及部署体积等多个维度,为你深度剥析这场性能之战的“罗生门”。Go 究竟赢在了哪里?到底值不值得重写?真相就藏在这些数据里。

测试背景与环境基调

在深入数据之前,我们需要明确测试的上下文。根据 Issue 提供的信息,本次测试运行在主流的现代硬件上(Apple M4 Max芯片),对比了使用 Go 编写的新版本与原有的 Node.js 版本。

测试场景涵盖了后端服务最核心的指标:HTTP 接口响应时间(冷启动/热启动)、系统内存占用(Memory Usage)、CPU 消耗以及最终交付的构建产物体积(Distribution Size)。

值得注意的是,原作者在总结中非常客观地给出了各项指标的“胜者(Winner)”。这为我们的分析奠定了一个理性的基调:我们不谈神话,只看数据。

响应时间(Execution Time):V8 引擎的绝地反击

许多人主张重写,最大的诉求就是“天下武功唯快不破”。然而,这份 Benchmark 数据在执行时间上给出了非常微妙的结果,这也是引发“罗生门”争议的核心所在。

首次请求/冷启动(Uncached/Cold Path)

在未经缓存或首次执行的路径上,Go 展现出了编译型语言的天然优势。

从数据报表可以看出,Go 在处理未命中缓存的 HTTP 请求时,其 P50、P90、P99 延迟均低于 Node.js。

Node.js 依赖 V8 引擎执行 JavaScript。在代码刚启动或首次执行特定路径时,V8 需要进行解释执行(Ignition 解释器),此时尚未触发 JIT(即时编译)的深度优化。此外,Node.js 庞大的模块加载树在冷启动时也会拖慢初始响应速度。而 Go 语言是直接编译为机器码的,没有预热过程,代码一经执行便是最高形态,因此在冷请求处理上先拔头筹。

预热后/热路径(Cached/Warm Path)

这是这份报告中最令人瞩目,也是让 Node.js 捍卫尊严的部分。

当系统运行一段时间,进入“热路径”后,两者的差距被急剧缩小。报告的 Summary 明确指出,在某些状态下,Node.js 的表现极具竞争力,甚至在特定的小负载处理上与 Go “打平”或略占优势。

千万不要低估 Google V8 引擎的威力!当 Node.js 的代码被反复执行后,V8 的 TurboFan 编译器会将热点代码(Hot Code)编译为高度优化的机器码。在纯 CPU 逻辑不复杂、主要依赖非阻塞 I/O 的 Web 场景下,预热后的 Node.js 同样快如闪电。

如果你只看冷启动,Go 是赢家;如果你看系统平稳运行后的常态,Node.js 并没有输。如果你的业务对极端情况下的毫秒级冷启动延迟不敏感,仅仅为了追求 API 的“绝对响应速度”而重写,带来的收益可能远低于预期。

内存占用(Memory Footprint):Go 的绝对统治区

如果说在响应速度上两人是势均力敌的对手,那么在内存管理上,这场“罗生门”的迷雾瞬间散去——Go 展现出了对 Node.js 的绝对统治力。

根据 Benchmark 数据,在承受相同并发压力的前提下,Go 版本的内存使用量仅仅是Node.js版本的五分之一不到。并且在内存增长方面也尽显优势。作者在Summary 表格中毫无悬念地将 Memory 的 Winner 颁给了 Go。

为什么 Node.js 这么吃内存?

  1. V8 的基础开销:仅仅是启动一个 Node.js 进程,V8 引擎就需要预先分配相当一部分内存用于自身的运行、垃圾回收堆(Heap)和执行上下文。
  2. 万物皆对象:在 JavaScript 中,几乎所有的数据结构都是对象(即便是一个简单的数字,内部也可能有复杂的包裹)。这带来了巨大的内存碎片和对象头(Object Header)开销。
  3. GC 策略:Node.js 的垃圾回收倾向于在内存达到一定阈值时才进行大规模清理,这导致其峰值内存(RSS)往往处于高位。

Go 赢在了哪里?

  1. 值类型与内存对齐:Go 允许开发者使用纯粹的值类型(Value Types),结构体(Structs)在内存中是连续紧凑排列的,没有对象的额外负担。
  2. 逃逸分析(Escape Analysis):Go 编译器极其聪明,它会尽可能将短生命周期的变量分配在栈(Stack)上,而不是堆(Heap)上。栈内存的分配和释放开销几乎为零,且不需要 GC 介入。
  3. 微型协程(Goroutine):Go 的协程初始栈极小(仅 2KB),相比之下,传统的线程或 Node.js 维持高并发异步上下文树要轻量得多。

可以看出,内存优化是这次重构最核心的“硬核红利”。在 Kubernetes 盛行的云原生时代,内存直接与真金白银(Pod 资源限制、节点数量)挂钩。如果你正在为 Node.js 应用居高不下的 OOM(内存溢出)和高昂的云服务器账单发愁,这才是用 Go 重写的最大底气。

部署与分发(Distribution Size):运维的终极解脱

最后一个维度,往往被性能测试忽略,但却是运维和 DevOps 团队最关心的指标:部署体积与运维体验。

基准测试的最后一部分给出了令人舒适的对比:

  • Node.js:部署时需要携带庞大的 node_modules 文件夹(被戏称为宇宙中最重的物质),还需要在服务器或 Docker 镜像中安装完整的 Node.js 运行时环境。这不仅导致镜像臃肿,还增加了极大的安全攻击面。
  • Go:通过静态链接(Static Linking),Go 编译器将所有依赖、业务逻辑和 Runtime 打包成了一个孤立的、极小的二进制文件(Single Binary)。

作者也认为,Go 在这方面取得了毋庸置疑的决定性胜利。

Go 的构建产物通常只有十几兆到几十兆,且无需外部动态库依赖。这使得 Go 的 Docker 镜像可以基于极简的 scratch 构建,拉取速度极快,启动瞬间完成。这在 Serverless 架构或需要频繁扩缩容的微服务场景下,带来了 Node.js 无法企及的运维优势。

小结:看透罗生门,回归工程本质

综合这份来自一线的真实 Benchmark 报告,这场关于性能的“罗生门”其实有着非常清晰的结论:

Go 并没有在单纯的“运行速度”上全面秒杀 Node.js。如果你的瓶颈仅仅在于 I/O 等待,且代码经过了 V8 引擎的充分预热,Node.js 依然是一个性能强悍的后端利器。

然而,Go 究竟赢在了哪里?它赢在了“工程维度的全面占优”:

  1. 绝对的内存红利:用极低的内存消耗承载高并发,直接降低了云资源成本。
  2. 更快的冷启动速度:在微服务和 Serverless 时代,冷启动速度就是金钱。
  3. 极简的部署体验:单文件二进制彻底解放了 CI/CD 流水线和镜像仓库。

技术选型永远是权衡(Trade-off)的艺术。如果你只是盲目追求“快那么几毫秒”,V8 引擎的表现可能会让你觉得重写是个错误;但如果你真正想要解决的是内存账单爆炸、冷启动缓慢、以及部署运维臃肿的综合困局,那么这场罗生门的结局早已注定——Go 语言,就是那个无可替代的破局者之一。

资料链接:https://github.com/lukechilds/reverse-shell/pull/38


你会为了“省内存”而重写吗?

很多时候,Go 赢在工程,而非纯粹的运行速度。在你的项目中,你是否遇到过 Node.js 内存溢出(OOM)的噩梦?你认为为了极简的部署和低成本的云账单,值得进行一次大规模的语言重构吗?

欢迎在评论区分享你的选型“罗生门”!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

当“安全性”遭遇“交付速度”:2026 年,我为什么告别了 Rust

本文永久链接 – https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026

大家好,我是Tony Bai。

在软件工程的铁三角中,Rust 占据了“安全性”与“性能”的绝对高地。凭借借用检查器(Borrow Checker)和极其严格的类型系统,它向开发者承诺了一个没有内存错误、没有空指针崩溃的完美世界。

然而,在商业软件开发的战场上,还有一个至关重要的维度往往被技术纯粹主义者忽视,那就是——交付速度(Delivery Speed)

近日,资深工程师 Dmitry Kudryavtsev 发表了长文《Farewell, Rust》,详述了他为何忍痛将一个运行了多年、已盈利的 Rust 项目全盘重写为 Node.js 的心路历程。这篇文章也引发了一场关于“为了极致的安全性,我们是否值得牺牲过多的交付速度?”的深刻辩论。

缘起:一个 C/C++ 老兵的“安全梦”

Dmitry 绝非那些被即时编译(JIT)宠坏的脚本小子。相反,他的技术底色是硬核的 C/C++。

早在高中时代,他就沉迷于指针的魔力,痴迷于手动管理内存的掌控感。他写过 3D 渲染器、IRC 机器人,甚至操作系统内核。然而,由于第一份工作是 PHP Web 开发,他被迫进入了动态语言的世界。虽然 PHP、Python 和 Ruby 带来了 Web 开发的极速体验,但在内心深处,他始终怀念 C 语言那种“压榨硬件每一滴性能”的快感,同时也痛恨 C 语言中防不胜防的内存安全漏洞。

直到 Rust 横空出世。

对于像 Dmitry 这样的工程师来说,Rust 简直就是“鱼与熊掌兼得”的梦想:

  • 低级控制力:像 C 一样精确控制内存布局。
  • 安全性:编译器在编译阶段就能消除一整类内存错误。
  • 现代体验:拥有像 Cargo 这样优秀的包管理工具。

于是,他做了一个所有热血工程师都会做的决定:为了追求极致的质量与安全,用 Rust 从零构建一个商业 Web 应用。

起初,一切都很完美。他在 2023 年底成功上线了项目,甚至因此受邀在两个技术大会上发表演讲。但随着时间的推移,业务逻辑日益复杂,“安全性”的红利开始被“交付速度”的损耗所抵消。到了 2026 年初,为了项目的生存,他不得不做出了那个艰难的决定:告别 Rust

深度复盘:Rust 在 Web 交付中的“五大减速带”

Dmitry 的文章之所以珍贵,是因为他用亲身经历证明了:在 Web 开发的特定场景下,Rust 引以为傲的“安全性”机制,如何一步步变成了拖慢“交付速度”的罪魁祸首。

1. 模板与视图:类型安全 vs. 迭代速度

在后端逻辑中,Rust 的类型系统坚不可摧。但当数据流向前端(HTML/Email 模板)时,这种为了安全而设计的严格性,变成了修改 UI 时的噩梦。

  • 安全性的代价:为了保证编译时的类型安全,Rust 社区诞生了 Maud 或 Askama 这样的编译时模板库。它们通过宏(Macro)在编译期检查 HTML 模板中的每一个变量引用。这听起来很棒,意味着你永远不会渲染出错误的变量。
  • 速度的牺牲:但这带来的副作用是,每次修改 HTML 哪怕一个标点符号,都会触发漫长的重新编译。在 Web 前端开发这种需要“所见即所得”的高频迭代场景下,这种等待是毁灭性的。
  • 对比 Node.js:TypeScript 配合 JSX/TSX 提供了全链路的类型安全,同时保持了极快的热重载(Hot Reload)速度。重构一个字段,VS Code 会立即标红所有受影响的视图组件,修改后毫秒级生效。这种“安全且快”的体验,是 Rust 目前无法提供的。

2. 国际化(i18n):生态缺失带来的效率黑洞

对于商业应用,支持多语言是刚需。

虽然 Mozilla 开发了 Project Fluent,但 Rust 生态中缺乏成熟的、开箱即用的 i18n 解决方案。你往往需要为了“正确性”而去处理繁琐的加载逻辑和类型绑定,编写大量的胶水代码。而Node.js生态中的i18next 等库不仅极其成熟,还能配合 TypeScript 提供键值级别的类型安全。Node.js 原生内置了完整的 ICU 标准(Intl API),处理货币、日期、复数格式化信手拈来。在这一点上,Rust 开发者需要花费数倍的时间来实现同样的功能,严重拖慢了产品推向全球市场的速度。

3. “动态”业务 vs. “静态”约束

Web 业务充满了动态性:用户提交的 JSON 结构可能是不确定的,筛选条件的组合可能是无穷的。Rust 试图用静态类型系统去约束这些动态行为,结果就是开发效率的暴跌。

  • 序列化之痛:serde 是 Rust 的瑰宝,但在处理复杂的、充满 Option 的业务数据时,为了安全地取出一个嵌套字段,你不得不编写大量的 match 或 unwrap 处理代码。为了优雅地处理错误,Dmitry 定义了十几个自定义错误枚举。虽然代码很健壮,但写起来太慢了。
  • SQL 的僵局:sqlx 提供了极其强大的编译时 SQL 检查,这在静态查询时非常棒。但是,一旦你需要根据用户输入动态构建查询(例如:用户选了 A 筛选条件就加个 WHERE 子句),Rust 的强类型系统就变成了噩梦。你无法像在 Node.js 中使用 Kysely 或 Prisma 那样,流畅地拼接查询片段。为了“安全”地构建 SQL,你付出了巨大的代码复杂度成本。

4. 编译时间:CI/CD 的隐形杀手

这是最让 Dmitry 崩溃的一点,也是“交付速度”最直观的体现。

  • Rust 的等待:随着依赖增多(尤其是使用了大量宏的 Web 框架),编译时间呈指数级增长。Dmitry 的 CI 流程需要 12-14 分钟 才能完成部署。“每次我在 Sentry 上看到一个简单的 Bug,想到修复它需要等待 15 分钟的构建流程,我就失去了修复的动力。”
  • Node.js 的极速:迁移到Node.js后,完整的 CI 流程(含 Lint 和测试)仅需 5 分钟。部署速度提升了 3 倍。这意味着“发现 Bug -> 修复 -> 上线”的反馈闭环被大大缩短了。在商业竞争中,修复速度往往比绝对的“无 Bug”更重要。

5. 生态成熟度:造轮子的时间成本

Rust 的 Web 生态虽然在成长,但面对长尾需求时仍显稚嫩。

  • 场景:你需要集成一个冷门的第三方支付网关,或者处理一个特定的 Webhook 签名验证。
  • Rust 的困境:官方 SDK?没有。社区库?两年前就不更新了。为了安全,你不得不对着 API 文档,自己手写 HTTP 请求、自己实现加密验签逻辑。这占用了大量本该用于开发业务核心功能的时间。
  • Node.js 的便利:npm install 通常能解决一切。几乎所有 SaaS 服务商都会提供第一方的 Node.js SDK。“拿来主义”是提升交付速度的最佳捷径。

总结与反思:我们到底为了什么而编程?

Dmitry 的文章并没有否定 Rust 的价值。相反,他依然热爱 Rust,依然怀念那些与编译器“斗智斗勇”并最终获得完美代码的日子。

他的结论非常客观,为所有正在做技术选型的团队提供了一把衡量“安全”与“速度”的标尺:

  1. 资源占用 vs. 开发效率的账本
    Rust 版本的应用内存占用仅 60-80MB,而 Node.js 版本约为 117MB。
    Rust 确实更省资源。但对于业务应用来说,这 50MB 的内存差异,在云服务器几美元一个月的成本面前不值一提。然而,为了节省这 50MB 内存,开发者付出了几倍的开发时间、调试精力以及心智负担。这笔账,在商业逻辑上是划不来的。

  2. 技术选型的“黄金法则”

    • 何时拥抱“安全性”(选 Rust):如果你在构建数据库内核、搜索引擎、高频交易系统、嵌入式设备固件,或者像 Lambda 这样对冷启动时间极度敏感的 Serverless 函数。在这些场景下,性能和稳定性是核心竞争力,为了安全牺牲开发速度是值得的。
    • 何时拥抱“交付速度”(选 Node.js/Go/Python):如果你在构建 CRUD 后端、SaaS 业务逻辑、内部管理工具,或者处于需要快速试错、频繁变更需求的初创阶段。在这些场景下,迭代速度(Velocity)才是核心竞争力。
  3. 给 Go 开发者的启示
    有趣的是,Dmitry 在注脚中提到了 Go:“Yes, there is Go. But I never really had the chance to like Go.”
    这其实是一个非常有意思的信号。在 Rust 的“极致安全”和 Node.js 的“极致速度”之间,Go 恰恰占据了那个“黄金平衡点”

    • 它有静态编译和类型系统,比 Node.js 更安全、性能更好。
    • 它有极快的编译速度和简单的语法,比 Rust 的心智负担低得多。
    • 它有极其成熟的中间件和微服务生态。

    对于那些厌倦了 Node.js 运行时错误,又被 Rust 借用检查器拖慢脚步的 Web 开发者来说,Go 依然是当下最务实的选择。

小结

技术选型从来没有绝对的优劣,只有“最适合当下约束条件的工具”。

Dmitry 的故事提醒我们:不要因为手里拿着“安全性”这把锤子(Rust),就无视了“交付速度”这个钉子。在商业软件的世界里,有时候,为了让产品活下去,为了让用户更快用上新功能,“足够好”且“跑得快”的代码,往往比“完美但迟到”的代码更有价值。

Rust 是系统编程的未来,但这并不意味着它是所有 Web 业务的终点。对于独立开发者或初创团队而言,“快”,本身就是一种极其重要的功能。

资料链接:https://yieldcode.blog/post/farewell-rust/


你会为了“安全”放弃“速度”吗?

软件工程永远是权衡的艺术。在你的项目中,你是否也曾为了追求某种“先进特性”,而导致项目进度失控?如果给你 50MB 的内存节省,你愿意多等 10 分钟的编译时间吗?

欢迎在评论区分享你的选型纠结!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的《Go语言进阶课》终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

© 2026, bigwhite. 版权所有.

🔲 ☆

NodeJS 和 npm 配置全局变量

由于 umami 不再使用 npm 构建,而是改为了使用 yarn 构建安装的方式,所以今天把 umami 删除,重新构建了一遍。

yarn 安装完成后,使用直接报错,查到因为没有配置 npm 成为全局变量,此文记录一下配置过程。


配置

1、获取 npm 安装目录

1
npm bin -g

img

2、创建软链接

1
ln -s 获取到的地址/npm /usr/local/bin/npm

img

3、配置用户环境变量

1
2
cd ~   #切换到用户根目录
vi .bash_profile #修改用户环境变量文件

输入 i 切换为输入模式,在 PATH=$PATH: 行后,添加 :第一步获取到的目录,然后按 esc 推出输入模式,切换到命令模式输入 :wq 之后保存并退出。

img

5、重启配置文件

1
source .bash_profile

img

6、查看 npm 和 yarn 配置

1
2
npm -v
yarn -v

img

此时 NodeJS 和 npm 全局变量即配置成功

🔲 ☆

CDN 方式引入 Monaco Editor

<p>在前端工程中可以用 <code>@monaco-editor/loader</code> 来引入 Monaco,但有时候我们的前端项目不依赖 Webpack、Vite 等打包工具,如何在普通网页中用纯 CDN 的方式引入 Monaco Editor?</p><span id="more"></span><h3 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h3><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span> <span class="attr">lang</span>=<span class="string">&quot;zh-CN&quot;</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;utf-8&quot;</span> /&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">title</span>&gt;</span>Monaco Editor<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">style</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"> <span class="selector-tag">body</span> &#123;</span></span><br><span class="line"><span class="language-css"> <span class="attribute">margin</span>: <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css"> <span class="attribute">padding</span>: <span class="number">0</span>;</span></span><br><span class="line"><span class="language-css"> &#125;</span></span><br><span class="line"><span class="language-css"> <span class="selector-id">#editor</span> &#123;</span></span><br><span class="line"><span class="language-css"> <span class="attribute">width</span>: <span class="number">100vw</span>;</span></span><br><span class="line"><span class="language-css"> <span class="attribute">height</span>: <span class="number">100vh</span>;</span></span><br><span class="line"><span class="language-css"> &#125;</span></span><br><span class="line"><span class="language-css"> </span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;editor&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">&quot;https://registry.npmmirror.com/monaco-editor/0.52.2/files/min/vs/loader.js&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="built_in">require</span>.<span class="title function_">config</span>(&#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">paths</span>: &#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">vs</span>: <span class="string">&quot;https://registry.npmmirror.com/monaco-editor/0.52.2/files/min/vs&quot;</span>,</span></span><br><span class="line"><span class="language-javascript"> &#125;,</span></span><br><span class="line"><span class="language-javascript"> &#125;);</span></span><br><span class="line"><span class="language-javascript"> <span class="variable language_">window</span>.<span class="property">MonacoEnvironment</span> = &#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">getWorkerUrl</span>: <span class="keyword">function</span> (<span class="params">workerId, label</span>) &#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> <span class="string">`data:text/javascript;charset=utf-8,<span class="subst">$&#123;<span class="built_in">encodeURIComponent</span>(</span></span></span></span><br><span class="line"><span class="subst"><span class="string"><span class="language-javascript"> <span class="string">&#x27;self.MonacoEnvironment=&#123;baseUrl:&quot;https://registry.npmmirror.com/monaco-editor/0.52.2/files/min/&quot;&#125;;&#x27;</span> +</span></span></span></span><br><span class="line"><span class="subst"><span class="string"><span class="language-javascript"> <span class="string">&#x27;importScripts(&quot;https://registry.npmmirror.com/monaco-editor/0.52.2/files/min/vs/base/worker/workerMain.js&quot;);&#x27;</span></span></span></span></span><br><span class="line"><span class="subst"><span class="string"><span class="language-javascript"> )&#125;</span>`</span>;</span></span><br><span class="line"><span class="language-javascript"> &#125;,</span></span><br><span class="line"><span class="language-javascript"> &#125;;</span></span><br><span class="line"><span class="language-javascript"> <span class="built_in">require</span>([<span class="string">&quot;vs/editor/editor.main&quot;</span>], <span class="keyword">function</span> (<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> editorElement = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&quot;editor&quot;</span>);</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> editor = monaco.<span class="property">editor</span>.<span class="title function_">create</span>(editorElement, &#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">value</span>:</span></span><br><span class="line"><span class="language-javascript"> <span class="string">&quot;function main() &#123;\n console.log(&#x27;Hello, iMaeGoo!&#x27;);\n&#125;\n\nmain();\n&quot;</span>,</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">language</span>: <span class="string">&quot;javascript&quot;</span>,</span></span><br><span class="line"><span class="language-javascript"> &#125;);</span></span><br><span class="line"><span class="language-javascript"> <span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">&quot;resize&quot;</span>, <span class="function">() =&gt;</span></span></span><br><span class="line"><span class="language-javascript"> editor.<span class="title function_">layout</span>(&#123;</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">width</span>: editorElement.<span class="property">offsetWidth</span>,</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">height</span>: editorElement.<span class="property">offsetHeight</span>,</span></span><br><span class="line"><span class="language-javascript"> &#125;)</span></span><br><span class="line"><span class="language-javascript"> );</span></span><br><span class="line"><span class="language-javascript"> &#125;);</span></span><br><span class="line"><span class="language-javascript"> </span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h3><iframe style="width:100%;height:300px" src="data:text/html,%3C!DOCTYPE%20html%3E%0A%3Chtml%20lang%3D%22zh-CN%22%3E%0A%20%20%3Chead%3E%0A%20%20%20%20%3Cmeta%20charset%3D%22utf-8%22%20%2F%3E%0A%20%20%20%20%3Ctitle%3EMonaco%20Editor%3C%2Ftitle%3E%0A%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20body%20%7B%0A%20%20%20%20%20%20%20%20margin%3A%200%3B%0A%20%20%20%20%20%20%20%20padding%3A%200%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%23editor%20%7B%0A%20%20%20%20%20%20%20%20width%3A%20100vw%3B%0A%20%20%20%20%20%20%20%20height%3A%20100vh%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E%0A%20%20%3C%2Fhead%3E%0A%20%20%3Cbody%3E%0A%20%20%20%20%3Cdiv%20id%3D%22editor%22%3E%3C%2Fdiv%3E%0A%20%20%20%20%3Cscript%20src%3D%22https%3A%2F%2Fregistry.npmmirror.com%2Fmonaco-editor%2F0.52.2%2Ffiles%2Fmin%2Fvs%2Floader.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%3Cscript%3E%0A%20%20%20%20%20%20require.config(%7B%0A%20%20%20%20%20%20%20%20paths%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20vs%3A%20%22https%3A%2F%2Fregistry.npmmirror.com%2Fmonaco-editor%2F0.52.2%2Ffiles%2Fmin%2Fvs%22%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20window.MonacoEnvironment%20%3D%20%7B%0A%20%20%20%20%20%20%20%20getWorkerUrl%3A%20function%20(workerId%2C%20label)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%20%60data%3Atext%2Fjavascript%3Bcharset%3Dutf-8%2C%24%7BencodeURIComponent(%0A%20%20%20%20%20%20%20%20%20%20%20%20'self.MonacoEnvironment%3D%7BbaseUrl%3A%22https%3A%2F%2Fregistry.npmmirror.com%2Fmonaco-editor%2F0.52.2%2Ffiles%2Fmin%2F%22%7D%3B'%20%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20'importScripts(%22https%3A%2F%2Fregistry.npmmirror.com%2Fmonaco-editor%2F0.52.2%2Ffiles%2Fmin%2Fvs%2Fbase%2Fworker%2FworkerMain.js%22)%3B'%0A%20%20%20%20%20%20%20%20%20%20)%7D%60%3B%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20require(%5B%22vs%2Feditor%2Feditor.main%22%5D%2C%20function%20()%20%7B%0A%20%20%20%20%20%20%20%20const%20editorElement%20%3D%20document.getElementById(%22editor%22)%3B%0A%20%20%20%20%20%20%20%20const%20editor%20%3D%20monaco.editor.create(editorElement%2C%20%7B%0A%20%20%20%20%20%20%20%20%20%20value%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%22function%20main()%20%7B%5Cn%20%20console.log('Hello%2C%20iMaeGoo!')%3B%5Cn%7D%5Cn%5Cnmain()%3B%5Cn%22%2C%0A%20%20%20%20%20%20%20%20%20%20language%3A%20%22javascript%22%2C%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20window.addEventListener(%22resize%22%2C%20()%20%3D%3E%0A%20%20%20%20%20%20%20%20%20%20editor.layout(%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3A%20editorElement.offsetWidth%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3A%20editorElement.offsetHeight%2C%0A%20%20%20%20%20%20%20%20%20%20%7D)%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%3C%2Fscript%3E%0A%20%20%3C%2Fbody%3E%0A%3C%2Fhtml%3E"></iframe>
🔲 ☆

鸿蒙 PC 编译运行 Electron 应用

<p>华为推出的 MateBook Pro 首次搭载了鸿蒙 PC 操作系统,使其能够直接运行鸿蒙手机应用和鸿蒙平板应用,但仅仅这样只能称得上是『大号平板』。</p><p>Electron 框架是优秀的跨平台客户端框架,通过改造,鸿蒙 PC 上也能运行 Electron 应用,具体如何操作呢?</p><span id="more"></span><h2 id="编译-Electron"><a href="#编译-Electron" class="headerlink" title="编译 Electron"></a>编译 Electron</h2><p>可以自己编译,也可以用华为预编译好的版本。</p><h3 id="自己编译"><a href="#自己编译" class="headerlink" title="自己编译"></a>自己编译</h3><p>参考文档:<a href="https://gitcode.com/openharmony-sig/electron">https://gitcode.com/openharmony-sig/electron</a></p><p>编译环境必须使用 Ubuntu 22.04,可以用虚拟机。</p><p>编译耗时很长,我用 8 核虚拟机 <strong>跑了大概 8 个小时左右</strong>,如无特殊需求建议用华为预编译好的版本。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装工具git-lfs, ccache。注:该步骤仅在首次拉取代码时需要执行</span></span><br><span class="line"><span class="built_in">sudo</span> apt install -y git-lfs ccache curl python3 python-is-python3 python3-pip</span><br><span class="line">python --version</span><br><span class="line">pip --version</span><br><span class="line"></span><br><span class="line"><span class="comment"># 下载码云repo工具(可以参考码云帮助中心:https://gitee.com/help/articles/4316)</span></span><br><span class="line"><span class="built_in">mkdir</span> -p ~/bin</span><br><span class="line">curl https://gitee.com/oschina/repo/raw/fork_flow/repo-py3 &gt; ~/bin/repo</span><br><span class="line"><span class="built_in">chmod</span> a+x ~/bin/repo</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;export PATH=~/bin/:$PATH&#x27;</span> &gt;&gt; ~/.bashrc</span><br><span class="line"><span class="built_in">source</span> ~/.bashrc</span><br><span class="line">pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests</span><br><span class="line"></span><br><span class="line"><span class="comment"># 通过NodeSource仓库安装node和npm</span></span><br><span class="line">curl -fsSL https://deb.nodesource.com/setup_lts.x | <span class="built_in">sudo</span> -E bash -</span><br><span class="line"><span class="built_in">sudo</span> apt install -y nodejs</span><br><span class="line">node -v</span><br><span class="line">npm -v</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用https拉取chromium-electron代码</span></span><br><span class="line">git <span class="built_in">clone</span> -b master https://gitcode.com/openharmony-sig/electron.git</span><br><span class="line"></span><br><span class="line"><span class="comment"># 执行命令`git lfs pull`,确保仓库中的大文件已经下载完成</span></span><br><span class="line"><span class="built_in">cd</span> electron</span><br><span class="line">git lfs pull</span><br><span class="line"></span><br><span class="line"><span class="comment"># 拉取chromium-electron对应的ohos chromium代码</span></span><br><span class="line">git config --global user.name <span class="string">&quot;iMaeGoo&quot;</span></span><br><span class="line">git config --global user.email <span class="string">&quot;mail1st@qq.com&quot;</span></span><br><span class="line">repo init -u https://gitcode.com/openharmony-tpc/manifest.git -b pc_chromium_132 -m pc_chromium_132_20251106.xml --no-repo-verify</span><br><span class="line">repo <span class="built_in">sync</span> -c <span class="comment"># 可以执行多次,以确保代码全部拉取成功</span></span><br><span class="line">repo forall -c <span class="string">&#x27;git lfs pull&#x27;</span> <span class="comment"># 可执行多次,以确保大文件全部拉取成功</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 应用chromium-electron的patch到ohos chromium</span></span><br><span class="line"><span class="built_in">pushd</span> src</span><br><span class="line">find -name <span class="string">&quot;*.git*&quot;</span> -<span class="built_in">exec</span> <span class="built_in">rm</span> -rf <span class="string">&quot;&#123;&#125;&quot;</span> \;</span><br><span class="line"><span class="built_in">popd</span></span><br><span class="line"><span class="built_in">chmod</span> +x override_files.sh</span><br><span class="line">./override_files.sh</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行 Electron实际目录/src/build/install-build-deps.sh脚本,安装编译所需的软件包。注:该步骤仅在首次拉取代码时需要执行</span></span><br><span class="line"><span class="built_in">sudo</span> ./src/build/install-build-deps.sh --no-chromeos-fonts</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行脚本electron_build.sh</span></span><br><span class="line">./electron_build.sh</span><br><span class="line"></span><br><span class="line"><span class="comment"># 可以通过如下脚本拷贝所需资源(注:请参考修改为自己的source_path)</span></span><br><span class="line">source_path=./Electron实际目录/src/out/musl_64</span><br><span class="line">destination_path=./electron</span><br><span class="line"><span class="keyword">if</span> [ -d <span class="variable">$&#123;destination_path&#125;</span> ];<span class="keyword">then</span></span><br><span class="line"><span class="built_in">rm</span> -rf <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"><span class="built_in">mkdir</span> <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/libelectron.so <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/libffmpeg.so <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/libadapter.so <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/electron <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/icudtl.dat <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/v8_context_snapshot.bin <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/chrome_100_percent.pak <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/chrome_200_percent.pak <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/resources.pak <span class="variable">$&#123;destination_path&#125;</span></span><br><span class="line"><span class="built_in">mkdir</span> <span class="variable">$&#123;destination_path&#125;</span>/locales</span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/locales/zh-CN.pak <span class="variable">$&#123;destination_path&#125;</span>/locales</span><br><span class="line"><span class="built_in">cp</span> <span class="variable">$&#123;source_path&#125;</span>/locales/en-US.pak <span class="variable">$&#123;destination_path&#125;</span>/locales</span><br></pre></td></tr></table></figure><h3 id="使用预编译版本"><a href="#使用预编译版本" class="headerlink" title="使用预编译版本"></a>使用预编译版本</h3><p>没有调用 addon 和 ArkTS 的需求时可以直接使用以下二进制 release 包进行开发。</p><ol><li><p>获取最新日期的二进制 release 包,华为账号登录<a href="https://devcloud.cn-north-4.huaweicloud.com/codehub/project/b19f5ea8ffd4492ea8c06ca2ebf3f858/codehub/2821214/home?ref=electron34-release">仓库</a>,下载默认 Electron 34 的 release 包。</p></li><li><p>解压</p></li></ol><h2 id="搭建环境"><a href="#搭建环境" class="headerlink" title="搭建环境"></a>搭建环境</h2><p>安装 DevEco Studio,目前是 5.1.0,最新版即可</p><p><a href="https://developer.huawei.com/consumer/cn/download/">https://developer.huawei.com/consumer/cn/download/</a></p><p>配置环境变量,这样以后能方便地使用 hdc 等命令</p><p>假设安装路径是 <code>D:\dev\DevEcoStudio</code>,就在 PATH 中增加 <code>D:\dev\DevEcoStudio\sdk\default\openharmony\toolchains</code></p><h2 id="运行项目"><a href="#运行项目" class="headerlink" title="运行项目"></a>运行项目</h2><p>打开 DevEco,打开前面编译&#x2F;下载好的项目 ohos_hap</p><p>首次运行需要证书,按提示登录华为账号即可生成证书</p><p>跑起来的效果,按 Ctrl + Alt + I 可以打开调试</p><p><img src="/gallery/2025/harmony-pc-electron/1756091289483.webp"></p><p>Electron 的入口点在 <code>src/main/resources/resfile/resources/app/main.js</code>,修改后重新运行即可看到效果</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; app, <span class="title class_">BrowserWindow</span>, <span class="title class_">Tray</span>, nativeImage &#125; = <span class="built_in">require</span>(<span class="string">&#x27;electron&#x27;</span>);</span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> mainWindow, tray;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">createWindow</span>(<span class="params"></span>) &#123;</span><br><span class="line"> tray = <span class="keyword">new</span> <span class="title class_">Tray</span>(nativeImage.<span class="title function_">createFromPath</span>(path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;electron_white.png&#x27;</span>)));</span><br><span class="line"> mainWindow = <span class="keyword">new</span> <span class="title class_">BrowserWindow</span>(&#123;</span><br><span class="line"> <span class="attr">width</span>: <span class="number">800</span>,</span><br><span class="line"> <span class="attr">height</span>: <span class="number">600</span>,</span><br><span class="line"> &#125;);</span><br><span class="line"> mainWindow.<span class="title function_">setWindowButtonVisibility</span>(<span class="literal">true</span>);</span><br><span class="line"> mainWindow.<span class="title function_">loadURL</span>(<span class="string">&#x27;https://caiyunapp.com/map/&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line">app.<span class="title function_">whenReady</span>().<span class="title function_">then</span>(createWindow);</span><br></pre></td></tr></table></figure>
🔲 ☆

近思录 2025-08-03

Microsoft Activation Scripts (MAS)

https://github.com/massgravel/Microsoft-Activation-Scripts

真好。不知为什么,装了Windows 7的台式机提示要激活,用了这个,好啦。

鲁郭茅巴老曹

C++实现的高精度计算

https://linux.do/t/topic/837436

Mermaid在线编辑器

O1NumHess

QIIME 2的安装简直地狱

根本没办法用镜像,是用七拐八拐的脚本来安装R包。索性用docker容器了。

时歌的博客

技术大佬。

突然想到,之前看的道家阴符派后来也打不开了。就像上个月发现的,隔几年很多友情链接都打不开了。

范曾

sci-lib

R的Set3配色

ggplot2的默认配色

1
2
3
4
5
6
library(RColorBrewer)
col2rgb(brewer.pal(12, "Set3"))
# [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
# red 141 255 190 251 128 253 179 252 217 188 204 255
# green 211 255 186 128 177 180 222 205 217 128 235 237
# blue 199 179 218 114 211 98 105 229 217 189 197 111

万宁象棋

凯美瑞怎么改装也赶不上法拉利

这个简单的逻辑解释了为什么现在的生成式AI发展遇到瓶颈。

才真旺姆出家后好像还有很多故事

网上已经搜不到了,我也只是在她刚出家时关注过。

现在DeepSeek很多地方能跑了,让它给我们写几句。

袈裟脱却云卷舒
僧衣重披月盈虚
卸下青山担四海
来去皆是菩提路
(注:按禅宗思想,出家还俗无非修行路径之别,青山是禅院修持的冷寂,四海是红尘渡人的宏愿,云月之喻暗指僧衣穿脱不过是表象,心性方为根本。后两句对仗拆解了传统"出世/入世"的二元对立。)

让它用大量的佛经典故再写。

穷子衣珠逃法华(注1),
维摩丈室不二门(注2)。
楞严迷头影犹在(注3),
金刚筏弃涅槃城(注4)。

注1:《法华经》穷子喻,暗喻众生本具佛性却流落红尘
注2:《维摩诘经》借居士身示现修行,破出家相执
注3:《楞严经》迷头认影喻,点破出家还俗俱是颠倒妄见
注4:《金刚经》如筏喻者,修道如渡河需舍筏;《涅槃经》常乐我净,究竟还灭处非出非入

(全偈以四部大乘经典典故连缀,穷子喻表出家之因,维摩示现解还俗之机,楞严破相显真,金刚涅槃明体,层层剥落名相,直指心性本源。)

赛博大师呵。除了不二法门我知道,其他几个我也不懂,姑且信了。又让它写《一纪旧闻新论,才真旺姆果还俗否?》

一纪转经轮未歇(注1),
旧闻新论沸雪山(注2)。
才真文字撕贝叶(注3),
旺姆空行落炊烟(注4)。

注1:《大日经》载"转法轮十二辐",暗合一纪之数,喻轮回不息
注2:拉萨三大寺辩经如沸,雪山指代藏地宗教语境
注3:拆"才真"为文殊智(才)与真如(真),《文殊真实名经》云"文字即般若",贝叶经暗喻名相执着
注4:藏语"旺姆"即度母化身,"空行母坠红尘"典出《密续》,炊烟喻俗世生活。全偈拆解"才真旺姆"四字,以"果还俗否"作机锋转语,实指《中论》"不落二边"义。

是否对头呢,我无从判断,感觉有一点扯。扯,就是AI给人的感觉嘛,一本正经地胡说八道。

Principles of Chemical Nomenclature

异氟烷


The unit is in Angstrom.

Quite simple tool powered by 3Dmol.js. Auto-detects bond formation using bond length database from pymatgen.

Feature requests are welcome by submitting an issue at https://github.com/liwt31/liwt31.github.io.

重度运行GMP库烧毁了两颗Ryzen 9950X CPU

我真傻,真的,我单知道Intel的CPU「出厂即灰烬」,会「缩缸」;我不知道AMD的CPU也会有。

datapasta

复制粘贴数据到R。

🔲 ☆

将扣子空间生成的 jsx 格式网页部署到自己的服务器

<p>扣子空间生成的网页是 jsx 格式的,在扣子空间内可以正常打开,如果想要部署到自己的服务器,则需要经过编译。</p><p>为此,我写了一个模板,只需将扣子空间生成的 jsx 重命名为 <code>coze.tsx</code>(注意后缀要改为 tsx)放入本项目 <code>src</code> 目录,即可编译出可静态部署的 dist 产物。</p><span id="more"></span><h2 id="详细步骤"><a href="#详细步骤" class="headerlink" title="详细步骤"></a>详细步骤</h2><ol><li>从扣子空间下载 jsx 文件</li></ol><p><img src="/gallery/2025/coze-space-jsx/1745907200269.webp"></p><ol start="2"><li>下载 <a href="https://github.com/imaegoo/coze-space-jsx-template">模板工程</a></li><li>安装 Node.js</li><li>进入本项目所在目录,运行 <code>npm install -g pnpm</code> 安装 pnpm 包管理器</li><li>运行 <code>pnpm install</code> 安装依赖</li><li>将扣子空间生成的 jsx 重命名为 <code>coze.tsx</code>(注意后缀要改为 tsx),放入 <code>src</code> 目录,覆盖原有的 <code>coze.tsx</code> 文件</li></ol><p><img src="/gallery/2025/coze-space-jsx/1745898560927.webp"></p><ol start="7"><li>检查 <code>coze.tsx</code> 中的 <code>import</code> 语句,确保所有第三方包都已经安装,举例:如果 <code>coze.tsx</code> 中有 <code>import Mermaid from &#39;mermaid&#39;;</code>,就执行安装 <code>pnpm install mermaid</code></li><li>运行 <code>pnpm run dev</code> 查看效果</li></ol><p><img src="/gallery/2025/coze-space-jsx/1745898552039.webp"></p><ol start="9"><li>运行 <code>pnpm run build</code> 编译</li><li>编译完成后,在 <code>dist</code> 目录下即可找到编译后的产物</li></ol>
🔲 ☆

Midscene.js:AI驱动的自动化测试工具

字节有一个很实用但不怎么火的项目,叫 Midscene.js,Chrome 商店上的安装数仅有 1 万,它是一个由多模态模型驱动的前端自动化测试插件。

Midscene.js 一共就三大 API:Action、Query、Assert

Action 交互

描述步骤并执行交互。例如,在 GitHub 上交互:查找 GitHub 上的 Twikoo 项目,点进详情页,点个 Star——

Query 提取

从 UI 中“理解”并提取数据,返回值是 JSON 格式,想要什么数据结构,它都可以给你。例如,在面试题宝典网站上提取:string[],所有面试题目——

Assert 断言

判断是否符合指定条件。例如,在智能家庭页面断言:电脑是关着的——

大模型支持情况

项目最初仅支持 GPT-4o 模型,跑一行用例的成本在 ¥0.1 左右,还挺贵的,后来支持了 Qwen-2.5-VL 和 UI-TARS,成本就大幅降低了。以下就以千问模型为例,带领大家上手这个神奇的插件。

安装

可以直接从 Chrome 商店安装:
https://chromewebstore.google.com/detail/midscene/gbldofcpkknbggpkmbdaefngejllnief

配置

从浏览器右上角的插件菜单中打开 Midscene.js 的侧边栏,会提示 No config,点击按钮会弹出 Env Config 的设置框,在里面配置以下变量

1
2
3
4
OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
MIDSCENE_MODEL_NAME="qwen-vl-max-latest"
MIDSCENE_USE_QWEN_VL=1

其中的 OPENAI_API_KEY 需要你自己申请,申请的地址是:
https://bailian.console.aliyun.com/?apiKey=1#/api-key

以上链接不包含推广,如果你是首次开通阿里云百炼,新用户是有免费额度的,请注意额度的有效期,避免浪费~

测试

接下来用自然语言随便写一条指令,点击 Run 按钮,见证 AI 开始接管你的浏览器……

代码集成

接下来我们尝试编写爬虫,组合这三大 API,完成复杂的自动化任务。

建一个新的 Node.js 项目,安装所需的依赖——

1
pnpm install @midscene/web tsx --save-dev

编写脚本 main.ts,执行你想要进行的操作,例如,打开必应,输入 iMaeGoo 点击搜索,并输出搜索结果——

main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { AgentOverChromeBridge } from "@midscene/web/bridge-mode";

function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}

async function main() {
process.env.OPENAI_BASE_URL =
"https://dashscope.aliyuncs.com/compatible-mode/v1";
process.env.OPENAI_API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
process.env.MIDSCENE_MODEL_NAME = "qwen-vl-max-latest";
process.env.MIDSCENE_USE_QWEN_VL = 1;
const agent = new AgentOverChromeBridge();
// 这个方法将连接到你的桌面 Chrome 的新标签页
// 记得启动你的 Chrome 插件,并点击 'allow connection' 按钮。否则你会得到一个 timeout 错误
await agent.connectNewTabWithUrl("https://www.bing.com");
// 这些方法与普通 Midscene agent 相同
await agent.ai("输入 iMaeGoo 点击搜索");
const result = await agent.aiQuery(
"{title: string, url: string}[], 搜索结果"
);
console.log("搜索结果", result);
await sleep(3000);
await agent.destroy();
}

main();

启动你的 Chrome 插件,点击 Bridge Mode,再点击 ‘Allow connection’ 按钮——

随后运行脚本——

1
pnpx tsx main.ts

可以看到脚本成功打开必应搜索 iMaeGoo 并打印出了搜索结果——

🔲 ⭐

仅需两个文件,实现在 VS Code 状态栏监控黄金价格

最近金价波动剧烈,要是能一边写代码,一边实时监控金价变动,就不会错过高低点了!

C:\Users\你的用户名\.vscode\extensions 新建文件夹 gold-monitor,在文件夹中创建两个文件 package.jsonextension.js

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"name": "jd-gold-price-monitor",
"displayName": "JD Gold Price Monitor",
"description": "Monitor gold price from JD",
"version": "0.0.3",
"author": "iMaeGoo",
"publisher": "iMaeGoo",
"engines": {
"vscode": "^1.85.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./extension.js",
"contributes": {
"commands": [
{
"command": "gold-price-monitor.start",
"title": "Start Gold Price Monitor"
}
]
}
}
extension.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const vscode = require("vscode");

let statusBarItem;
let intervalId;

function activate(context) {
// 创建状态栏项
statusBarItem = vscode.window.createStatusBarItem(
// 在状态栏左半边显示
vscode.StatusBarAlignment.Left,
// 数字越大,越靠左
-999
);
context.subscriptions.push(statusBarItem);

// 注册命令
let disposable = vscode.commands.registerCommand(
"gold-price-monitor.start",
() => {
startMonitoring();
}
);

context.subscriptions.push(disposable);

// 激活时自动开始监控
startMonitoring();
}

async function updateGoldPrice() {
try {
const price = await getPrice();
statusBarItem.text = price;
statusBarItem.show();
} catch (error) {
console.error("获取价格失败", error);
statusBarItem.text = error.message;
statusBarItem.show();
}
}

async function getPrice() {
// 获取京东金融民生银行积存金价
const response = await fetch(
"https://api.jdjygold.com/gw/generic/hj/h5/m/latestPrice?reqData={}"
);
const data = await response.json();
const price = data.resultData.datas.price;
return price;
}

function startMonitoring() {
// 清除现有的定时器
if (intervalId) {
clearInterval(intervalId);
}

// 立即更新一次
updateGoldPrice();

// 设置定时器,每隔 6666 毫秒更新一次
intervalId = setInterval(updateGoldPrice, 6666);
}

function deactivate() {
if (intervalId) {
clearInterval(intervalId);
}
}

module.exports = {
activate,
deactivate,
};

重新启动 VS Code(Ctrl + Shift + P,输入 reload window,回车)即可看到效果。

如果你想监测其他品牌金价,可以修改 getPrice 方法,具体实现如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
async function zheshang() {
// 获取京东金融浙商银行积存金价
const response = await fetch(
"https://api.jdjygold.com/gw2/generic/jrm/h5/m/stdLatestPrice?productSku=1961543816"
);
const data = await response.json();
const price = data.resultData.datas.price;
return price;
}

async function zhoushengsheng() {
// 获取周生生首饰金价
try {
const res = await fetch("https://cn.chowsangsang.com/gold-info");
const data = await res.text();
const gold_data_match = data.match(/:gold_data='(.*)'/);
const gold_data = gold_data_match
? JSON.parse(decode(gold_data_match[1]))
: [];
const price = gold_data.find(
(i: any) => i.description === "黄金金价"
).price;
return price;
} catch (e) {
console.error("zhoushengsheng", e);
}
}

async function laofengxiang() {
// 获取老凤祥首饰金价
const res = await fetch("http://lfx1848.com");
const data = await res.text();
const price_match = data.match(/<span id="labContent">(.*)<\/span>/);
const price = price_match ? price_match[1] : "";
return price;
}

async function zhouliufu() {
// 获取周六福首饰金价
const res = await fetch("https://price.zlf.cn/index_35.aspx");
const data = await res.text();
const price_match = data.match(/<span class="fr">(.*)<\/span>/);
const price = price_match ? price_match[1] : "";
return price;
}

async function liufuzhubao() {
// 获取六福珠宝首饰金价
const res = await fetch("https://www.lukfookeshop.com.cn");
const data = await res.text();
const price_match = data.match(/>:(.*?)元\/克/);
const price = price_match ? price_match[1] : "";
return price;
}

async function zhoudafu() {
// 获取周大福首饰金价
const res = await fetch(
`https://api2.ctfmall.com/gateway//ctfmall-common2-server/common/ctfTodayGoldPrice?timestamp=${Date.now()}`
);
const data: any = await res.json();
const price = data.data.todayPriceHK;
return price;
}

async function laomiao() {
// 获取老庙首饰金价
const res = await fetch(
"https://vip.laomiao.com.cn/index.php/m/home-gold_price.html"
);
const data: any = await res.json();
const price = data.data.price_list.find(
(i: any) => i.name === "足金饰品"
).price;
return parseInt(price);
}
🔲 ⭐

【笔记】“xx packages are looking for funding”——npm fund命令及运行机制

“xx packages are looking for funding”——npm fund 命令及运行机制

在 Node.js 和 npm 生态系统中,开源项目的持续发展和维护常常依赖于贡献者的支持和资助。为了让开发者更容易了解他们依赖的项目哪些有资金支持选项,npm 在6.13.0版本起引入了 npm fund 命令并默认在npm install安装依赖时触发。本文将详细介绍 npm fund 的作用、运行机制、触发时机、如何避免触发以及相关的副作用和改进建议。

什么是 npm fund 命令?

在日常npm install安装依赖的过程中,我们可能都忽略了 command 最后输出的一些信息,比如本文相关的 funding 信息,如:

1
2
3 packages are looking for funding.
Run "npm fund" to find out more.

npm fund 命令是在 npm 6.13.0 版本中首次引入的,旨在帮助开发者识别其项目依赖中可以资助的开源包。运行该命令时,npm 会列出所有包含资助选项的包及其相关链接,便于开发者快速访问这些页面并提供支持。

命令起源

在 2019 年 8 月份时,Standard JS 在其开源项目中内置广告的事件引发热议,这些广告通过一个名为 Fundingnpm 软件包展示在终端,该软件包包含在 Standard 的代码库中。之后 npm 公司宣布将禁止此类终端广告行为。

此事件后,npm 公司表示,它打算在年底前为开源开发人员开发一个众筹平台,于是乎在npm 6.13.0版本上提供了相应支持,这就是npm fund命令的主要由来。

命令作用

  • 显示资助信息npm fund 会扫描项目中的 node_modules 目录,查找每个包的 package.json 文件中是否包含 funding 字段。它会将有资助选项的包及其资助链接列出。
  • 支持开源生态:通过此功能,npm 提高了对开源项目资助的透明度,鼓励开发者参与到开源项目的资助中,帮助维护者获得资金支持。

使用方法

基本用法非常简单,只需在项目根目录中运行:

1
npm fund

命令将输出形如:

1
2
xx packages are looking for funding
run `npm fund` for details

再运行 npm fund,就会显示类似如下的详细信息:

1
package-name   https://example.com/donate

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├─┬ https://opencollective.com/typescript-eslint
│ │ └── @typescript-eslint/eslint-plugin@4.28.5, @typescript-eslint/experimental-utils@4.28.5, @typescript-eslint/types@4.28.5, @typescript-eslint/typescript-estree@4.28.5, @typescript-eslint/visitor-keys@4.28.5, @typescript-eslint/scope-manager@4.28.5, @typescript-eslint/parser@4.28.5
│ └─┬ https://opencollective.com/eslint
│ │ └── eslint@6.8.0
│ ├── https://github.com/sponsors/epoberezkin
│ │ └── ajv@6.12.6
│ ├── https://github.com/sponsors/sindresorhus
│ │ └── globals@12.4.0, import-fresh@3.3.0, strip-json-comments@3.1.1, ansi-escapes@4.3.2, type-fest@0.21.3, figures@3.2.0, onetime@5.1.2, globby@11.0.4
│ ├── https://github.com/sponsors/isaacs
│ │ └── glob@7.2.3
│ ├─┬ https://github.com/chalk/chalk?sponsor=1
│ │ │ └── chalk@4.1.2
│ │ └── https://github.com/chalk/ansi-styles?sponsor=1
│ │ └── ansi-styles@4.3.0
│ └── https://github.com/sponsors/ljharb
│ └── minimist@1.2.8, is-generator-function@1.0.9, qs@6.11.2, side-channel@1.0.4, call-bind@1.0.2, get-intrinsic@1.1.1, has-symbols@1.0.2, object-inspect@1.11.0
├── https://github.com/sponsors/RubenVerborgh
│ └── follow-redirects@1.15.2
└── https://ko-fi.com/tunnckoCore/commissions
└── formidable@1.2.2

npm fund 的运行机制和触发时机

运行机制

  • 依赖扫描npm fund 会读取 node_modules 中每个依赖包的 package.json 文件,寻找 funding 字段。如果找到了该字段,它会提取并显示相关的资助信息。

    • 字段格式:funding 字段可以是 URL 字符串或更复杂的对象,指向资助页面。例如:
      1
      2
      3
      {
      "funding": "https://example.com/donate"
      }

    或者

    1
    2
    3
    4
    5
    6
    {
    "funding": {
    "type": "individual",
    "url": "https://example.com/donate"
    }
    }

触发时机

  • npm install 提示:在安装项目依赖时,如果项目中存在可以资助的包,npm 会显示类似“xx packages are looking for funding”的提示,提醒开发者可以运行 npm fund 查看详细信息。(这也是我们日常主要触发的时机)
  • 显式调用:开发者可以手动运行 npm fund 命令,以查看当前项目中支持资助的所有包和资助链接。

如何避免 npm fund 的触发?

在某些情况下,开发者或企业可能希望在 npm install 过程中避免看到这些资助提示。以下是两种实现方式:

1. 在安装时使用 --no-fund 参数

直接在运行 npm install 时添加 --no-fund 参数:

1
npm install --no-fund

2. 修改 .npmrc 配置文件

.npmrc 文件中加入以下配置来永久禁用资助提示:

1
fund=false

此配置可以放在项目的根目录下(项目下的.npmrc文件),仅作用于当前项目;也可以放在用户主目录(~/.npmrc文件),作用于全局。

*可以通过npm config ls -l查看当前项目的npm配置,默认情况下fund配置会被设置为true

禁用 npm fund 的副作用

优点:

  • 简洁输出:禁用 npm fund 提示可以减少 npm install 的输出信息,使终端显示更加清晰。
  • 减少干扰:在企业级项目中,开发者可能更专注于安装过程和依赖的调试,不需要额外的资助提示。

缺点:

  • 支持意识减弱:禁用该提示后,开发者不再会注意到可以资助的依赖,可能错失支持有价值的开源项目的机会。
  • 透明度降低:新加入的团队成员或不熟悉项目的开发者可能不知道项目中有哪些包有资助选项。
  • 开源支持意识降低:从长远来看,减少对资助信息的提示可能会让开发者对支持开源项目的重要性淡化,从而减少对依赖项目的贡献和支持。

npm fund 源码

源码文件:https://github.com/npm/cli/blob/latest/lib/commands/fund.js

以目前(2024-11)的源码内容来看,其源码机制概括来说是先使用 npm 的内部模块库函数来遍历 node_modules 目录,读取 package.json 并检查是否有 funding 字段,最后将所有符合条件的包信息格式化输出到终端。

代码流程总结:

  • 1.读取依赖树:使用 Arborist 加载项目的依赖树。
  • 2.解析资助信息:通过 libnpmfundreadTree 方法提取资助信息。
  • 3.输出格式化:根据用户配置输出 JSON 格式或使用 archy 进行可读格式的输出。
  • 4.链接打开:当提供了包名时,openFundingUrl 会尝试在浏览器中打开该包的资助链接。

最后

npm fundnpm 引入的一个有用的命令,帮助开发者支持开源项目并维持开源生态的可持续发展。虽然在某些情况下禁用它有其合理性,但在默认情况下保留该提示可以提高团队对开源项目支持的意识。根据项目和团队的实际需求,开发者可以灵活选择是否禁用 npm fund 提示。


相关链接

🔲 ☆

使用 HTTP API 从 WPS 在线表格中增删改查数据示例

使用 HTTP API 从 WPS 在线表格中获取数据 中,我介绍了如何查询数据,本篇将介绍如何利用 AirScript 实现完整的增删改查。

首先还是在金山文档中创建一个智能表格,在智能表格中创建一个数据表,这里以 “汇率表” 为例。

点 “高级功能” - “高级开发” - “AirScript 脚本编辑器”,在文档共享脚本中,点击 + 号右边的下拉按钮,创建一个 AirScript 1.0 脚本。

AirScript 2.0 脚本暂时还不支持数据表

输入以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**
* WPS 智能表格数据表 AirScript 增删改查示例
* @author iMaeGoo <hello@imaegoo.com>
* @copyright iMaeGoo 2024
* @license MIT
*/

// 请求参数
const request = {
// 操作 ("create" | "read" | "update" | "delete")
action: Context.argv.action,
// 数据表名
sheet: Context.argv.sheet,
// 筛选条件,写法参考 https://airsheet.wps.cn/docs/api/excel/databook/%E9%99%84%E5%BD%95.html#%E9%99%84%E5%BD%95-3-%E7%AD%9B%E9%80%89%E6%9D%A1%E4%BB%B6%E8%AF%B4%E6%98%8E
filter: Context.argv.filter,
// 更新的字段信息
fields: Context.argv.fields,
};

// 响应参数
const response = {
success: false,
data: undefined,
message: "",
};

if (!request.action) {
response.message = "action 参数不能为空";
return response;
}

if (!request.sheet) {
response.message = "sheet 参数不能为空";
return response;
}

// 获取工作表
const sheet = Application.Sheets.Item(request.sheet);

switch (request.action) {
case "create":
response.data = create();
response.success = true;
response.message = "创建成功";
break;
case "read":
response.data = read();
response.success = true;
response.message = "查询成功";
break;
case "update":
response.data = update();
response.success = true;
response.message = "更新成功";
break;
case "delete":
response.data = del();
response.success = true;
response.message = "删除成功";
break;
default:
response.message =
'action 参数错误,应为 "create" | "read" | "update" | "delete"';
}

// 增
function create() {
return sheet.Record.CreateRecords({
Records: [
{
fields: request.fields,
},
],
});
}

// 查
function read() {
let all = [];
let offset = null;
while (all.length === 0 || offset) {
let records = sheet.Record.GetRecords({
Offset: offset,
Filter: request.filter,
});
offset = records.offset;
all = all.concat(records.records);
}
return all;
}

// 改
function update() {
const records = read();
const updateRecords = records.map((record) => {
return {
id: record.id,
fields: request.fields,
};
});
return sheet.Record.UpdateRecords({
Records: updateRecords,
});
}

// 删
function del() {
const records = read();
const deleteIds = records.map((record) => record.id);
return sheet.Record.DeleteRecords({
RecordIds: deleteIds,
});
}

return response;

按 Ctrl + S 保存脚本,然后点击工具栏上的 “脚本令牌” 按钮,生成一个脚本令牌,复制这个令牌保存(以后将无法再次查看),令牌有效期是半年,但放心,令牌可以随时延期,延期后令牌不变。

点击脚本名称旁边的菜单,点击 “复制脚本 webhook” 并保存。

接下来只需 POST 这个 webhook 地址,在请求头中带上 AirScript-Token: 脚本令牌,即可实现增删改查功能。

查询

查询美元的汇率

请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"Context": {
"argv": {
"action": "read",
"sheet": "汇率表",
"filter": {
"mode": "AND",
"criteria": [
{
"field": "货币名称",
"op": "Contains",
"values": [
"美元"
]
}
]
}
}
}
}

响应参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
"data": {
"logs": [
{
"filename": "<system>",
"timestamp": "15:42:07.539",
"unix_time": 1725867727539,
"level": "info",
"args": [
"脚本环境(1.0)初始化..."
]
},
{
"filename": "<system>",
"timestamp": "15:42:07.544",
"unix_time": 1725867727544,
"level": "info",
"args": [
"已开始执行"
]
},
{
"filename": "<system>",
"timestamp": "15:42:07.678",
"unix_time": 1725867727678,
"level": "info",
"args": [
"执行完毕"
]
}
],
"result": {
"data": [
{
"fields": {
"中行折算价": 709.89,
"发布日期": "2024/09/09",
"发布时间": "15:25:28",
"现汇买入价": 710.11,
"现汇卖出价": 712.94,
"现钞买入价": 704.34,
"现钞卖出价": 712.94,
"货币名称": "美元"
},
"id": "VN"
}
],
"message": "查询成功",
"success": true
}
},
"error": "",
"status": "finished"
}

新增

创建一条 “萌币” 的汇率记录

请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"Context": {
"argv": {
"action": "create",
"sheet": "汇率表",
"filter": {},
"fields": {
"中行折算价": 26.05,
"发布日期": "2024/09/09",
"发布时间": "15:55:28",
"现汇买入价": 21.05,
"现汇卖出价": 22.05,
"现钞买入价": 23.05,
"现钞卖出价": 25.95,
"货币名称": "萌币"
}
}
}
}

响应参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
"data": {
"logs": [
{
"filename": "<system>",
"timestamp": "15:56:57.857",
"unix_time": 1725868617857,
"level": "info",
"args": [
"脚本环境(1.0)初始化..."
]
},
{
"filename": "<system>",
"timestamp": "15:56:58.650",
"unix_time": 1725868618650,
"level": "info",
"args": [
"已开始执行"
]
},
{
"filename": "<system>",
"timestamp": "15:56:58.805",
"unix_time": 1725868618805,
"level": "info",
"args": [
"执行完毕"
]
}
],
"result": {
"data": [
{
"fields": {
"中行折算价": 26.05,
"发布日期": "2024/09/09",
"发布时间": "15:55:28",
"现汇买入价": 21.05,
"现汇卖出价": 22.05,
"现钞买入价": 23.05,
"现钞卖出价": 25.95,
"货币名称": "萌币"
},
"id": "VP"
}
],
"message": "创建成功",
"success": true
}
},
"error": "",
"status": "finished"
}

修改

修改 “萌币” 的备注

请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"Context": {
"argv": {
"action": "update",
"sheet": "汇率表",
"filter": {
"mode": "AND",
"criteria": [
{
"field": "货币名称",
"op": "Contains",
"values": [
"萌币"
]
}
]
},
"fields": {
"备注": "很萌但并不存在的货币"
}
}
}
}

响应参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
"data": {
"logs": [
{
"filename": "<system>",
"timestamp": "16:00:41.894",
"unix_time": 1725868841894,
"level": "info",
"args": [
"脚本环境(1.0)初始化..."
]
},
{
"filename": "<system>",
"timestamp": "16:00:42.683",
"unix_time": 1725868842683,
"level": "info",
"args": [
"已开始执行"
]
},
{
"filename": "<system>",
"timestamp": "16:00:42.955",
"unix_time": 1725868842955,
"level": "info",
"args": [
"执行完毕"
]
}
],
"result": {
"data": [
{
"fields": {
"中行折算价": 26.05,
"发布日期": "2024/09/09",
"发布时间": "15:55:28",
"备注": "很萌但并不存在的货币",
"现汇买入价": 21.05,
"现汇卖出价": 22.05,
"现钞买入价": 23.05,
"现钞卖出价": 25.95,
"货币名称": "萌币"
},
"id": "VP"
}
],
"message": "更新成功",
"success": true
}
},
"error": "",
"status": "finished"
}

删除

删除 “萌币” 记录

请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"Context": {
"argv": {
"action": "delete",
"sheet": "汇率表",
"filter": {
"mode": "AND",
"criteria": [
{
"field": "货币名称",
"op": "Contains",
"values": [
"萌币"
]
}
]
}
}
}
}

响应参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
"data": {
"logs": [
{
"filename": "<system>",
"timestamp": "16:01:47.265",
"unix_time": 1725868907265,
"level": "info",
"args": [
"脚本环境(1.0)初始化..."
]
},
{
"filename": "<system>",
"timestamp": "16:01:47.271",
"unix_time": 1725868907271,
"level": "info",
"args": [
"已开始执行"
]
},
{
"filename": "<system>",
"timestamp": "16:01:47.526",
"unix_time": 1725868907526,
"level": "info",
"args": [
"执行完毕"
]
}
],
"result": {
"data": [
{
"deleted": true,
"id": "VP"
}
],
"message": "删除成功",
"success": true
}
},
"error": "",
"status": "finished"
}

🔲 ⭐

Node.js 版本与 ABI 版本对照表

因为下载 node-sass 的 binding.node 预编译二进制包时,发现网上竟然搜不到 Node.js 版本与 ABI 版本的对应关系,所以自己整理一份吧,数据来自三个地方,我做了整合:

  1. https://github.com/sass/node-sass/blob/master/lib/extensions.js
  2. https://github.com/electron/node-abi/blob/main/abi_registry.json
  3. https://github.com/electron/node-abi/blob/main/index.js
ABIRuntimeTarget
1Node0.2.0
0x000ANode0.9.1
0x000BNode0.9.9
11Node0.10.4
11Node0.10.x
0x000CNode0.11.0
13Node0.11.8
14Node0.11.11
14Node0.12.x
42Node1.0.0
42io.js1.x
43Node1.1.0
43io.js1.1.x
44Electron0.30.0
44Node2.0.0
44io.js2.x
45Electron0.31.0
45Node3.0.0
45io.js3.x
46Electron0.33.0
46Node4.0.0
46Node.js4.x
47node-webkit0.13.0
47Electron0.36.0
47Node5.0.0
47Node.js5.x
48node-webkit0.15.0
48Electron1.1.0
48Node6.0.0
48Node.js6.x
49Electron1.3.0
49Electron1.3.x
50Electron1.4.0
50Electron1.4.x
51node-webkit0.18.3
51Electron1.5.0
51Node7.0.0
51Node.js7.x
53Electron1.6.0
53Electron1.6.x
54Electron1.7.0
57node-webkit0.23.0
57Electron1.8.0
57Electron2.0.0
57Node8.0.0
57Node.js8.x
59node-webkit0.26.5
59Node9.0.0
59Node.js9.x
64Node10.0.0
64Node.js10.x
64Electron3.0.0
64Electron4.0.0
67Node11.0.0
67Node.js11.x
69Electron4.0.4
70Electron5.0.0-beta.9
72Node12.0.0
72Node.js12.x
73Electron6.0.0-beta.1
75Electron7.0.0-beta.1
76Electron8.0.0-beta.1
76Electron9.0.0-beta.1
79Node13.0.0
79Node.js13.x
80Electron9.0.0-beta.2
82Electron10.0.0-beta.1
82Electron11.0.0-beta.1
83Node14.0.0
83Node.js14.x
85Electron11.0.0-beta.11
87Electron12.0.0-beta.1
88Node15.0.0
88Node.js15.x
89Electron13.0.0-beta.2
89Electron14.0.0-beta.1
89Electron15.0.0-alpha.1
93Node16.0.0
93Node.js16.x
97Electron14.0.2
98Electron15.0.0-beta.7
99Electron16.0.0-alpha.1
101Electron17.0.0-alpha.1
102Node17.0.0
102Node.js17.x
103Electron18.0.0-alpha.1
106Electron19.0.0-alpha.1
107Electron20.0.0-alpha.1
108Node18.0.0
108Node.js18.x
109Electron21.0.0-alpha.1
110Electron22.0.0-alpha.1
111Node19.0.0
111Node.js19.x
113Electron23.0.0-alpha.1
114Electron24.0.0-alpha.1
115Node20.0.0
115Node.js20.x
116Electron25.0.0-alpha.1
116Electron26.0.0-alpha.1
118Electron27.0.0-alpha.1
119Electron28.0.0-alpha.1
120Node21.0.0
121Electron29.0.0-alpha.1
123Electron30.0.0-alpha.1
123Electron31.0.0-alpha.1
125Electron31.0.0-beta.7
127Node22.0.0
128Electron32.0.0-alpha.1
129Node23.0.0
130Electron33.0.0-alpha.1
🔲 ☆

VSLite 原理解析与本地化部署

VSLite 是一个开源的、完全运行在浏览器中的开发环境,它的亮点在于,不需要联网也可以直接在浏览器中使用 Node.js、Python、Git 等工具。你可以在 vslite.dev 体验到在线 demo。

传统的基于云的开发环境,例如 GitHub Codespaces,依赖于后端服务,前端所执行的命令,实际上是通过网络请求发送到后端对应的云虚拟机(或容器)中执行的,这就需要消耗后端的计算资源;而 VSLite 的思路完全不同,它在浏览器中启动了一个容器,做到了不再依赖后端。

想要在浏览器中跑起来一个容器,第一次听说这样的想法时,我说:不可能!绝对不可能!就浏览器这也受限那也受限的环境,还能跑起来一个操作系统?然而 StackBlitz 真的做到了,他们开发出了 WebContainers,WebContainers 是一个基于浏览器的运行时,用于执行 Node.js 程序和操作系统命令,完全在浏览器选项卡中。以前需要云虚拟机才能执行的应用,现在可以完全在浏览器端运行。

经过两天的研究,我开始对 WebContainers 的原理有了初步的了解。

真假离线环境?

一开始我对浏览器能运行容器持怀疑态度,为了证明真是浏览器在运行容器,而不是某台远程的虚拟机,我拉取了 VSLite 项目,本地启动,然后断开网络访问 localhost:5101,果然根本无法加载,哈哈!露馅了吧?

重新连接网络,打开浏览器开发者工具能发现,容器启动过程需要向 staticblitz.com 发送数百个请求,难道它其实还是在线运行的?

改进一下步骤,先在联网状态下打开本地环境,等待加载完成之后断网,然后在终端中执行一段命令。诶?执行成功了?再瞅瞅操作系统,也能正常打印,居然还是 Ubuntu。

这下破案了,容器在启动过程需要从 staticblitz 的域名获取一系列资源,在启动完成后断网则不影响使用。浏览器运行容器,还真让他给实现了。

但我既然选择 VSLite,是因为它不需要连接云服务,这下还是要连接 staticblitz,没满足我的需求啊,我想本地部署。

查阅文档,根据 WebContainers 文档上的许可协议,本地化部署是需要商用授权的,所以文档上也无法找到任何有关本地部署的指导。

本地部署!

没有文档,那就读源码,自己研究原理,开搞!

等等,WebContainers 好像没开源……

等等,我可以看到 @webcontainer/api 这个包的代码,而且好在他们没给代码做混淆。

打开 node_modules/@webcontainer/api/dist/index.js,发现了这一段代码:

1
2
3
4
5
const DEFAULT_IFRAME_SOURCE = 'https://stackblitz.com/headless';
// ......
function getIframeUrl() {
return new URL(window.WEBCONTAINER_API_IFRAME_URL ?? DEFAULT_IFRAME_SOURCE);
}

这个网址就是首个发送到 staticblitz 的请求,它被设置到一个 iframe 的 src 中,这个请求返回了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<title>StackBlitz</title>
<script src="https://w-corp-staticblitz.com/webcontainer.9e2d28a3.js"></script>
<link rel="preload" as="script" href="https://w-corp-staticblitz.com/webcontainer.9e2d28a3.js" fetchpriority="low">
<link rel="preload" as="script" href="https://w-corp-staticblitz.com/fetch.worker.9e2d28a3.js" fetchpriority="low">

<script src="https://c.staticblitz.com/assets/vite/headless-BNfRU0p-.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="https://c.staticblitz.com/assets/vite/semver-Zyv2pDaP.js" as="script" crossorigin="anonymous">
</head>
<body>
<script type="application/json" id="webcontainer-context">{"options":{"baseUrl":"https://w-corp-staticblitz.com","initOptions":{"server":"https://local-corp.webcontainer-api.io","isolationPolicy":"require-corp","version":"9e2d28a3","flattenedServer":false},"systemBinaries":{},"git":{"proxy":"https://p.stackblitz.com"},"turboBaseUrl":"https://t.staticblitz.com","registryProxy":"https://nr.staticblitz.com","registryMaxConcurrency":""},"embedder":"https://vslite.dev/","shortAppId":false}</script>

</body>
</html>

本地化部署的核心在于把请求公网的资源,全部在本地 mock 一遍,也就是把上面看到的所有网址全部指向本地。

我们先把这个文件保存到 VSLite 项目的 public/wc/headless 中(这里的 wc 是我随便取的,取自 WebContainers 的首字母)。

刚才注意到容器启动会先判断有无 window.WEBCONTAINER_API_IFRAME_URL,所以只需要在容器启动前修改这个值就可以了。

修改 src/vite-env.d.ts

1
2
3
4
declare module globalThis {
// ......
var WEBCONTAINER_API_IFRAME_URL: string;
}

阅读 WebContainers 文档得知,启动容器的命令是 WebContainer.boot,全局搜一下,只有一处,在 src/hooks/useShell.ts,在这行代码前面加一行

1
window.WEBCONTAINER_API_IFRAME_URL = window.location.origin + '/wc/headless';

这样,第一个请求就本地化成功了。

我们继续看 headless 文件,发现里面还涉及到多个 js 文件和后端地址,然而我无法确定具体发往这些地址的请求,咋办?

有一个偷懒的办法,浏览器先打开一个标签,打开开发者工具切到 Network 标签,然后访问 vslite.dev,加载完成之后,右键任意一个请求,点击 Copy - Copy all URLs,嘿嘿,所有用到的请求就都复制出来了。

根据这些请求的文件,我也对 WebContainers 的启动过程有了一个认识,full_bin_index.9e2d28a3 和几个 wasm 文件组成了一个精简版的操作系统镜像,这个镜像在 WebAssembly 环境中运行,并通过 Service Worker 和前端页面通讯。

找个批量下载工具(例如迅雷、aria2…)依次下载所有涉及到的资源,全部放到 public 里面。然后修改 headless 文件,把所有请求地址都改成本地地址。

改完后发现还有一部分 monaco-editor 的资源请求,这部分就比较简单了。

先用 npm info 查一下 monaco-editor 的下载地址,看输出的 tarball 就是下载地址

1
npm info monaco-editor@0.36.1

下载下来

1
curl -LO https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.36.1.tgz

再解压

1
tar -xzf monaco-editor-0.36.1.tgz

解压 monaco-editor 后,只需把里面的 min 目录移动到 public/monaco-editor-0.36.1,其他的都不需要,删除。

然后看一下项目怎么引入 monaco 的,发现用的是 @monaco-editor/react,看一下这个包的文档,里面说可以通过 loader-config 来自定义 monaco 文件地址。

1
2
import { loader } from '@monaco-editor/react';
loader.config({ paths: { vs: window.location.origin + '/monaco-editor-0.36.1/min/vs' } });

把这行放在 src/hooks/useStartup.tsuseMonaco() 之前,monaco 的本地化就完成了。

最后 public 目录中一共放了这么些文件。

至此,VSLite 的本地化工作已经初步完成,断网访问 localhost:5101,容器正常加载,创建一个 js 文件用 node 执行试试,也可以正常执行。

以上所有的改动都可以在 我的 GitHub 找到。

你可能会意识到,headless 里面还有 p.stackblitz.comt.staticblitz.comnr.staticblitz.com,这三个地址没有本地化,他们的作用是反向代理 Git 请求和 npm 请求(可能是为了跨域?),这部分请求无法简单通过静态 mock,经过实测,这不妨碍 WebContainers 加载,但会影响 git clonenpm install,有兴趣的朋友可以试试自己搭建 proxy 来解决这个问题!

🔲 ⭐

将 Node.js 项目打包为一个可执行文件

简介

Node.js 从v18.16.0,v19.7.0版本开始原生支持了打包为可执行文件(Single executable applications), 常用的打包工具pkg也因此不在更新,下面介绍一下我在使用 NodeJs Single executable applications功能时的一些经验和问题。

使用

创建并打包你的项目文件

因为目前该功能只能将单个 js 文件封装为可执行文件,所以我们要借助打包工具(如 webpack, rollup 等)将 js 项目大包围一个 js 文件。由于 wenpack 的配置过于繁杂,这里介绍使用 rollup 工具进行打包。

  1. 安装 rollup: npm install --global rollup;

  2. 在项目根目录新建rollup.config.js文件,内容如下(根据项目内容进行调整):

    const commonjs  = require('@rollup/plugin-commonjs'); // commonjs支持,使用es模块可不使用此插件,安装:npm install @rollup/plugin-commonjs -Dconst { nodeResolve } = require('@rollup/plugin-node-resolve');const json = require('@rollup/plugin-json'); // 将静态json文件作为模块导入,按需安装,安装:npm install @rollup/plugin-json -Dconst { string } = require('rollup-plugin-string'); // 将静态文件文本作为模块导入,按需安装,安装:npm install @rollup/plugin-json -Dconst terser = require('@rollup/plugin-terser');// 压缩打包后的文件大小,按需安装,安装:npm install @rollup/plugin-json -Dmodule.exports = {  input: 'dist/main.js', // 项目入口文件  output: {    dir: 'output', // 输出文件目录    format: 'cjs' // 输出文件格式  },  plugins: [terser({    format: {      comments: false    }  }), nodeResolve({    preferBuiltins: true,    exportConditions: ['node']  }),  commonjs(),  json(),  string({    include: ['**/*.html', '**/*.yml']  })]};
  3. 打包项目:rollup -c

封装为可执行文件

  1. 在项目根目录新建your-project-config.json文件,内容如下(根据项目需求进行调整,官方说明):

    {  "main": "output/main.js", // 打包后的项目入口文件  "output": "your-project.blob",  "disableExperimentalSEAWarning": true,  "useCodeCache": true,  "disableExperimentalSEAWarning": true, // 禁用Nodejs的试验性更能警告  "useSnapshot": false,  // 使用快照  "useCodeCache": true // 使用代码缓存}
  2. 封装文件:

    • Windows:
    node --experimental-sea-config your-project-config.jsonnode -e "require('fs').copyFileSync(process.execPath, 'your-project.exe')"npx postject your-project.exe NODE_SEA_BLOB your-project.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
    • Linux:
    node --experimental-sea-config your-project-config.jsoncp $(command -v node) your-projectnpx postject your-project.exe NODE_SEA_BLOB your-project.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
  3. 等待封装完成。

🔲 ☆

VSCode中打开NodeJS项目自动切换对应NodeJS版本的配置

这几年搞了不少静态站点,有的是Hexo的,有的是VuePress的。由于不同的主题对于NodeJS的版本要求不同,所以本机上不少NodeJS的版本。

关于如何管理多个NodeJS版本,很早之前就写过用nvm来管理的相关文章,这里就不赘述了,有需要的可以看这篇Node.js环境搭建

虽然有了多版本管理,但是默认版本只有一个,所以很多时候,在用VSCode打开不同项目的时候,还需要用nvm use来切换不同的版本使用。显然一直这样操作很麻烦,而且容易忘记什么项目用什么版本。

所以,最好就是能打开项目的时候,自动就切换到对应的NodeJS版本。

要实现这样的效果只需要下面两步:

第一步:安装VSCode插件vsc-nvm

第二步:在项目根目录下创建文件.nvmrc,文件内容为版本号,比如:

v10.13.0

完成配置后,关闭VSCode,再重新打开,可以看到终端自动打开,并执行了nvm use命令,实现了NodeJS版本的自动切换

好了,今天的分享就到这里,希望对您有用。码字不易,欢迎转载,但请附上本文链接~

🔲 ☆

Puppeteer 踩坑笔记

Puppeteer 是最常见的服务端 Node.js 网页转 PDF 工具库之一,原理是启动 Chromium 无头模式,打开网页,输出PDF,由于其原理是直接操控浏览器,导出的 PDF 几乎和网页效果一致。本篇记录一下踩坑经历。

我的 Puppeteer 版本:19.4.1

无法安装 puppeteer

  1. Node.js 要求 >=14.1.0
  2. Puppeteer 安装过程中默认访问 Google 服务器下载 Chromium 安装包,建议切换到阿里源进行安装。
1
npm config set PUPPETEER_DOWNLOAD_HOST=https://registry.npmmirror.com/-/binary

启动 Chrome 没反应

puppeteer.launch 指定了 Chrome 浏览器路径,启动后任务管理器进程中有 chrome.exe 但代码不往下走

必须使用 Chromium 或者 Chrome Canary(金丝雀)版本,不要使用 Chrome 正式版。

Linux 系统服务器无法启动 Chromium

安装 Chromium 所需的运行依赖,以下是 RedHat Enterprise 需要安装的依赖。

1
2
yum install -y alsa-lib.x86_64 atk.x86_64 cups-libs.x86_64 gtk3.x86_64 ipa-gothic-fonts libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXrandr.x86_64 libXScrnSaver.x86_64 libXtst.x86_64 pango.x86_64 xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-fonts-cyrillic xorg-x11-fonts-misc xorg-x11-fonts-Type1 xorg-x11-utils
yum install -y at-spi2-atk libxkbcommon-x11 libgbm

另外,有些 Linux 内核不支持 sandbox,还需要在 Chromium 启动参数里加 --no-sandbox --disable-setuid-sandbox

Linux 系统服务器导出的 PDF 缺字、口口、字体异常

从 Windows 复制宋体、黑体、微软雅黑等常用字体到 Linux /usr/share/fonts/ 下,然后执行

1
2
3
4
5
yum install -y fontconfig mkfontscale
cd /usr/share/fonts
mkfontscale
mkfontdir
fc-cache

导出的 PDF 是空白

通常是单页面应用还没有加载完就触发了导出造成的,可以通过等待页面指定元素出现再导出来解决。

1
2
3
4
5
6
7
await browserPage.goto('http://localhost:8080', { waitUntil: 'networkidle2' });
// 解决页面未加载完成时,直接导出的 PDF 空白的问题
await browserPage.mainFrame().waitForSelector('.xxx-container');
await browserPage.pdf({
format: 'A4',
// ...
});

性能优化

容器运行 Chromium 无头浏览器推荐添加的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const browser = await puppeteer.launch({
args: [
// 解决 brwoser.newPage() 卡死的问题
'–-disable-gpu',
'–-no-zygote',
// 容器的 /dev/shm 分区默认只有 64M 不够用,禁用这个分区
'–-disable-dev-shm-usage',
// 解决某些服务器 Linux 发行版不支持沙盒启动的问题
'–-no-sandbox',
'–-disable-setuid-sandbox',
// 加快启动速度
'–-no-first-run',
// 单进程启动,好维护
'–-single-proces',
]
});

最后放一个完整的代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
console.log('Starting Chromium...');
const browser = await puppeteer.launch({
// 如果是手动安装的 chromium,需要传路径
// 如果是自动安装的 Chromium,不需要传 executablePath
executablePath: 'D:\\Software\\Chromium\\chrome.exe',
// 在某些Linux发行版下需要加以下参数才能启动
args: [
// 解决 brwoser.newPage() 卡死的问题
'–-disable-gpu',
'–-no-zygote',
// 容器的 /dev/shm 分区默认只有 64M 不够用,禁用这个分区
'–-disable-dev-shm-usage',
// 解决某些服务器 Linux 发行版不支持沙盒启动的问题
'–-no-sandbox',
'–-disable-setuid-sandbox',
// 加快启动速度
'–-no-first-run',
// 单进程启动,好维护
'–-single-proces',
],
});
console.log('Started chromium');
try {
const browserPage = await browser.newPage();
browserPage.setDefaultNavigationTimeout(0);
// 传需要转 PDF 的页面地址
await browserPage.goto('http://localhost:8080', {
waitUntil: 'networkidle2',
});
// 解决页面未加载完成时,直接导出的 PDF 空白的问题
await browserPage.mainFrame().waitForSelector('.xxx-container');
// 传 PDF 的存放位置等选项
await browserPage.pdf({
path: 'filename.pdf',
format: 'A4',
});
console.log('Generated PDF');
} finally {
await browser.close();
console.log('Closed Chromium');
}

总之,遇到问题时多打日志,更容易分析具体是哪一步出的问题。

❌