bash
作者: 一了 <1liao3@funlang.org>
日期: 2026-10-24
一句话总结: fun-ui 1.0 以 MIT 开源:Fun 进程持有窗口和业务逻辑,界面交给 Chromium,用 jCAL/jRET 与 fCAL/fRET 双向调用,WebView2 与 WebSocket 两种传输共用一套消息格式。
fun-ui 开源:把浏览器界面接回来
10 月 24 日,fun-ui 1.0 开源,MIT。仓库在 github.com/funlang/fun...,release 挂在 v1.0.0。
这东西不画按钮,也不做布局。它把 Fun 进程和 Chromium 接起来:窗口和业务逻辑留在 Fun 这边,界面用普通的 HTML、CSS、JavaScript 写,跑在浏览器内核里。两边互相调用,各干各的活。
Fun 一直有自己的窗口,lib-ui.fun 那套原生控件还能用。问题是现在的界面对排版、图表、动画的要求越来越高,原生控件追不动。浏览器现成的能力摆在那里,不用白不用。
一、界面归浏览器,逻辑归 Fun
先说清楚边界。Fun 进程持有窗口,负责算数、读文件、连数据库、调本地库;页面只负责显示和收集输入。页面里不塞业务逻辑,Fun 里只下发数据,两边通过四条消息说话。
JavaScript 调 Fun,叫 jCAL,Fun 回一条 jRET。反过来,Fun 调 JavaScript,叫 fCAL,JavaScript 回一条 fRET。四条消息都是 FD 文档,靠 type 字段分通道,字段也简单:id、name、args、timeout,返回时换成 id、val、err。
Fun 侧的宿主类继承 Ui2Form,覆盖 onReady,在里面决定加载哪个页面。处理函数按名字找,默认前缀是 fd_:
bash
class DemoForm = Ui2Form()
fun onReady()
Navigate('<h1>Hello, fun-ui</h1>');
end fun
fun fd_load_profile(args)
result = new [];
result.id = args[0];
result.name = '一了';
end fun
end class;
页面这边只加载两个脚本,fd.js 和 fun-bridge.js,然后就能直接调:
bash
<script src='fd.js'></script>
<script src='fun-bridge.js'></script>
<script>
Fun('load_profile', { id: 42 }).then(function (profile) {
document.querySelector('#name').textContent = profile.name;
});
</script>
window.Fun 支持两种写法:Fun(name, ...args) 返回 Promise,或者传一个带 name、args、callback、timeout 的对象。名字里的连字符和空格会换成下划线,找不到再去掉分隔符重试,load_profile 和 load-profile 都能落到同一个方法上。
二、四条消息,两个方向
协议本身不复杂,麻烦的是二进制。图片、压缩包、文件流这些都可能在消息里过,不能当字符串传。
fun-ui 的办法是给每个载荷加 4 字节小端长度头,第 31 位为 1 表示这帧压过。压缩用 raw DEFLATE,Fun 侧调 deflate,浏览器侧用 CompressionStream('deflate-raw')。默认不压,超过 512 字节才考虑;两边都能单独开关,阈值也各自可调。
还有个小地方:桥接脚本给 FD 文本用 gbk 编码标签,不是 UTF-8。原因很实际,旧的 Windows 代码页和现有 Fun 字符串要能原样往返。Chromium 支持 gbk 解码,所以浏览器这边得是 Chromium 内核,这一条写在兼容性里,不藏着。
fCAL 的返回值另走一条近路。Fun 侧先分配共享缓冲区,把句柄交给 JavaScript;JavaScript 把 fRET 写进去,只回传句柄。大块数据不用来回拷两遍。
三、两个传输,一套协议
第一种是 WebView2,直接把 Chromium 嵌进 Fun 窗口。中间隔着一个很小的 Delphi DLL,libwv2.dll,32 位,Delphi 2006 编的,底下用 WebView4Delphi。导出只有几个:Init、Show、Navigate、PostData、ExecScript、Close、Free。DLL 不打包 Chromium,用系统里的 WebView2 Runtime,所以下载体积还是 Fun 那个量级。
(吐槽:都 2026 年了,还在用 Delphi 2006 编 32 位 DLL。没别的原因,WebView4Delphi 这条路最短,能跑就先跑。)
第二种是 WebSocket。Ui2Proxy 在本地开 HTTP 页面,默认端口 8278,WebSocket 走同一套帧格式。页面加载 fun-bridge-ws.js,window.Fun 的用法一模一样,调度器和页面代码都不用改。
当前这个 WebSocket 版本还有点将就:页面仍然显示在 WebView2 窗口里,只是消息从共享内存换成了套接字。把它和 Ui2Form 拆开,让外部浏览器直接连,是路线图的第二步,也是把界面带出 Windows 的最短路径。
四、规模与边界
代码不大。Fun 侧五个模块,lib-ui2.fun、lib-ui2-proxy.fun、三个调度器,一共 549 行;浏览器侧四个脚本,fd.js、fd2.js、两个桥接,一共 1631 行;Delphi 宿主 375 行。三样加起来 2555 行,算上中文 README 和 CHANGELOG 才到 2853 行。
边界也一起说清楚:
- 首个版本只支持 Windows。
libwv2.dll和Ui2Form要 Windows 加 WebView2 Runtime。 - WebSocket 方式还没脱离 WebView2 窗口,外部浏览器要等第二步。
- 需要 Fun 9.x 运行时,以及前面列的那几个标准库模块。
- 浏览器必须是 Chromium 内核,原因就是
CompressionStream和gbk标签。
这些不是遗漏,是第一版故意收的口子。协议和 JavaScript API 先稳住,平台相关的代码关在宿主层里,以后加 Linux、macOS 不必动页面。
五、路线图
README 里写了四步,照抄如下:
- Windows 基线。WebView2 传输、WebSocket 传输、FD 序列化、调度器和桥接,就是现在仓库里的东西。
- 可移植的 WebSocket 传输。把
Ui2Proxy从 WebView2 窗体里拆出来,让它服务外部浏览器;然后在 Linux 上用lib-winsock-lnx,在 macOS 上用lib-winsock-unx。HTTP/WebSocket 核心已经在lib-winsock-base.fun里共享了。 - 原生 webview 后端。在
Ui2Form的 API 后面加 Linux 和 macOS 宿主,用 WebKitGTK 和 WKWebView,消息协议和 JavaScript 桥接不变。 - 发布工程。各平台构建脚本、CI、打包示例、兼容性矩阵。
线协议和 API 计划保持稳定,平台差异尽量关在宿主层。这话说起来容易,做起来要看后面两步的手感。
结语
Fun 负责算,浏览器负责画。这个分工不新鲜,Electron 早就这么干,只是它把整个 Chromium 塞进每个应用里。fun-ui 选择用系统现成的 WebView2,运行时还是那个几百 KB 的 fun.exe,界面就是几个 HTML 和 JS 文件。
第一版只开在 Windows 上,窗户不大,先透口气。代码是 MIT,问题、补丁、用例都收。地址再说一遍:github.com/funlang/fun...。