Gaubee's Feed
A collection of articles and events.
我前几天做Chrome-EXT开发的时候,遇到一些坑。
Chrome扩展是有好几个沙盒环境一起协同工作的,如果你之前做过 IOS-WebView 的二次开发,应该有印象:
JavaScript的注入,是有“World”概念的,默认是MAIN-World,这个就是网页的默认js执行环境。
然后还能自定义World,这种属于ISOLATED-World,和MAIN-World在同一个线程里面,但是内存是完全隔离的,类似于在一个独立的iframe环境中。
MAIN-World 有非常严格的安全限制。比如我就不能再MAIN-World里面去发起 ws://localhost 的链接。因为Google的网页非常注重安全,都会开启“内容安全协议限制”,比如不能Eval、new Function;以及MAIN-World的资源来源会有域名限制;甚至不可以和Chrome扩展的Servical-Worker通讯。
所以必须是这样的通讯路径:
“网页脚本(MAIN-World) ⇄ content-script(ISOLATED-World) ⇄ background/service-worker”
“网页脚本(MAIN-World) ⇄ content-script(ISOLATED-World) ⇄ backend-server/websocket”
看了一下 Cap’n Web 的源代码,相比Comlink它更专注于“网络协议”的优化。
Comlink的特性是参考 MessageChannel 去做Endpoint设计,而这种设计非常原始纯粹, 也就导致很多东西是有心智负担的。
比如MessageChannel 是有对象所有权传输的能力,实际写代码的时候,你得用Comlink提供的接口来对U8A对象标记成“直接传输到另外一个线程”。
但这种设计并不是所有适配器都会去实现(参考 GitHub - kinglisky/comlink-adapters )所以用起来的时候,使用者得注意,自己现在在什么环境里面,要传输什么东西?传输的东西是一个代理对象?还是克隆对象?
而 Cap’n Web 的定位是 RPC,其实就是类似执行一次http-req,这意味着没有后续的副作用。这个概念很重要!比如说,Comlink默认返回的对象还是一个Comlink对象,可以继续链式调用,除非主动标记Comlink.clone(returnValue)让它强制走克隆的逻辑。因此这里可以看出它们的定位和策略上的偏差。Cap’n Web 更适合网络环境。
但是总的来说 Cap’n Web 为RPC这种网络场景做了定向优化。同时还补充了一些特别的功能,比如Promise流水线化。以及它的 RpcTransport 适配器设计实现起来非常简单。
试用了 Aider ,这东西的定位就类似 Trae 这样的编辑器。主打就是一个协同开发。
坑是一个没少:
- 不支持MCP
不过这个难度不高,早晚是能支持的
- 好像没有使用RAG,而是提供了一个目录结构给LLM,让LLM自己判断要读取哪些文件。
这种做法效率并不高。架构如果不好,或者遇到特殊开发需求,就得靠开发者自己来做文件选取。
我之前看过openAI的另外一种做法,也不使用RAG,而是将所有内容和“选取条件”喂给小模型,让小模型来判断文件中的哪些内容是和“选取条件”有关系的,这个过程是可以拆分并发执行的。
然后再用标准模型来对最终选取出来的内容做任务。
2025年了,还是要吐槽一下compose(CMP)写桌面,目前体验并不好。
- 目前没有pure-native方案,只有jvm,因此起步体积就不小(社区中有老哥自己在搞linux-native的方案,但还在alpha阶段)。
- 然后没有成熟的开源webview方案(不是没有,而是不成熟),只有一个闭源付费的jxbrowser(有着高性能的compose渲染支持;还有线程安全的支持;对接口的提供也非常丰富)。
- 再有因为底层基于swing,因此一些先进的原生窗口功能就没有,社区也不活跃,非要的话只能自己搞jni。(比如说右键菜单,文件选择器,系统托盘都是swing那套,有兴趣的找个网图就知道有多丑了,而且连一些基本的字体适配也不是很好)
我个人建议:
- 如果是轻量的compose应用,那么可以考虑直接走compose-web+electorn的方向,至少底层工具链是现代化的。compose-web虽然也很糟心……
- 但我是说“轻量compose应用”,如果你的应用有原生图层混合,或者使用了复杂的kotlin协程,那就不算轻量。这时候还是只能老实走compose-jvm。那么我上头提到的那些问题一个都逃不掉。
这两天正式接触了一下 MoonBit,尝试将一个大约 1000 行的 C 语言编写的 JS Parser 转译成 MoonBit 语言。
聊一下我的客观感受:
- 前期最大的困境其实是文档的完备性。我不仅阅读了官方文档,还研究了官方 GitHub 上的
core
包源码,但仍然觉得其中有很多细节没有完整地整理出来。
- 但总体来说,MoonBit 给我的感觉像是 Rust 的一个更“干净”的版本。
- 语法挺“干净”的。之所以会这样说,是因为在开始接触之前,我断断续续关注官方发布的新闻或博文,当初曾疑惑这门语言为何一直在增加语法糖。但实际使用起来发现并不繁琐,反而觉得这些语法糖设计得恰到好处,且整体规则清晰。
- MoonBit 号称是“AI 友好的编程语言”,但在官方的(VS Code)插件中,我看到的内置 AI 提示词内容还比较基础(大约 500 行主要用于介绍基本语法)。相比之下,直接将官方 Markdown 文档喂给大型语言模型可能更有效率。在官方 AI 提示词的基础上尝试修复我遇到的一些问题时,往往只能解决表面现象,更深层次的转换仍需依赖 AI 模型自身的能力,而非这部分预设的提示词。
- 最大的差异在于“模式匹配”的运用。C 语言编写的 JS Parser 通常采用移动指针、逐个字符判断的风格,这与 MoonBit 所推崇的模式匹配风格有所不同。虽然 MoonBit 也能像 C 语言那样进行字符级解析,但要发挥编译器的优势,还是应该多使用模式匹配。目前,AI 在将这种指令式代码自动翻译成更偏声明式的模式匹配风格方面表现不佳,因此多数情况下,这种转换仍需要人工进行。
- LSP 的速度和稳定性没有达到我的预期。当使用 AI 生成代码并进行快速修改时,LSP 服务偶尔会卡死(例如,VS Code 插件长时间无响应)。对于我尝试的这个千行级别的项目,LSP 的分析速度也未如想象中迅速,或许是项目体量还不够大,未能充分体现其性能优势。
这两天我搞了 @gaubee/shim 这个包,主要是基于现有的 tc39 提案做的一些垫片,但目标不是为了搞polyfill,我的目标仍然是跟util包一样追求“无副作用”的、追求对Tree-Shaking的最佳支持。
目前提供两部分的功能:
- @gaubee/shim/decimal
- 基于big.js做的ts+fp(类型安全+函数式编程)化的改造。
- 单元测试我也完全搬过来了,并且做了一些优化,理论上性能会比big.js更好,但我没具体去测试。
- 可能是目前是最小的的 big-float 包了,因为它只包含了最基本的 parse+stringify(toString/valueOf)的能力(本质就是拆包和封包,起步200行左右的注释和代码)。
但因为是fp编程的风格,而不是链式调用,所以用起来会比较麻烦,
比如big.js是这样的:new Big(‘12.345’).round(2).toString()
@gaubee/util
包中的 delay 函数,相比于市面上的 delay 函数,有着很特别的能力,就是它不仅仅是可以传递一个数字(毫秒),还可以传递一个 timmer 对象。
以下是详细的能力介绍:
delay(0)
0 毫秒,那么它不会使用 setTimeout 来计时,而是会使用 queueMicrotask 来创建延迟队列。然而你知道 await 关键词本身就是在创建一个 queueMicrotask 队列,不同的是,const delayer = delay(0)
这里的 delayer 对象是可以进行取消的 delayer.cancel(reason?)
。
delay(10)
等同于 setTimeout/clearTimeout
delay(timmers.raf)
等同于 requestAnimationFrame/cancelAnimationFrame
delay(timmers.eventTarget<AnyEvent>(window,'scrollend'))
等同于 addEventListener/removeEventListener(浏览器 EventTarget)
delay(timmers.eventEmitter<AnyArgs>(event,'scrollend'))
等同于 addEventListener/removeEventListener(nodejs 的 EventEmitter)
delay(pureEvent<AnyType>().once);
可以将一个 pureEvent 的 once 函数直接传递进去
Minimal CSS-only blurry-image-placeholders(LQIPs)
这是一个天才般的想法,它把原本需要用 js 解码的工作,直接放到 css 表达式里,不仅仅是计算加快了,而且消除了很多中间成本。
我们项目有用到类似的需求。在我们项目中,图片名称(url)的一部分包含了 blurhash。
简单来说,我们使用文件名来存在 blurhash,然后将这个 blurhash 字符串解码成图片,但这是有代价的,需要用一个小 canvas 绘制:
- 首先用算法绘制出模糊图片然后将它绘制到 canvas 上(消耗 CPU);
- 然后将图片导出数据(消耗 CPU 和内存,这里做一次编码);
- 最后将数据转成 blob-url(消耗 IO);
上次咱们聊了 View Transitions 的基础,那感觉就像发现新大陆,丝滑得不行。但真正在复杂的 SPA(单页应用)场景里用起来,尤其是想模拟原生 App 那种细腻的转场效果时,你可能会发现,这“丝滑”背后,可能藏着一些“蛋疼”的细节。
核心矛盾点:View Transitions Level 1
的设计哲学是针对单个文档内 DOM 状态变化的视觉过渡。而 SPA 的常见模式是在单个文档里模拟多个“页面”的导航切换。这种模式上的错位,是许多复杂问题的根源。Level 1 并没有“页面”或“路由”的概念,它只关心“变化前”和“变化后”的 DOM 快照。
接下来,咱们通过一些实践案例,深入探讨在 SPA 中应用 View Transitions 的复杂性、局限性以及特定场景下的思考。
一、SPA 导航模拟:看起来很美,做起来费心
首先给出 DEMO 链接: ios-navigation demo by view-transition
咱们来看一个常见的需求:在 SPA 里模拟类似 iOS 的导航栏切换效果。这个效果细节不少:
- 页面整体:新页面从右侧滑入,覆盖旧页面。
- 返回按钮图标 (backIcon):在切换过程中,位置保持不动(视觉上像钉在那里)。