Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

架构与进程模型

Proton 将原生 MoonBit 应用与 Chromium 浏览器引擎组合为桌面应用。应用进程管理窗口、应用状态和操作系统功能;Web 页面负责界面,通过消息调用宿主提供的操作。

组成与职责

组成职责
MoonBit 原生应用执行应用入口、业务逻辑和异步任务
Proton提供窗口、生命周期、浏览器内容及前后端通信 API
CEF(Chromium Embedded Framework)将 Chromium 嵌入原生应用,提供浏览器创建、回调及进程通信接口
Chromium执行 Web 平台功能,包括页面渲染、JavaScript、网络和浏览器存储
Web 前端用 HTML、CSS 和 JavaScript 实现应用界面

应用通过 moonbit-community/proton 使用公开 API。Proton 的原生绑定随应用从源码编译,CEF/Chromium 则作为独立运行时加载,分发时与应用一起打包。页面使用这套随包分发的浏览器引擎,不依赖操作系统 WebView。

前端可以使用普通 JavaScript,也可以由 MoonBit 编译器编译为 JavaScript。Rabbita 是可选的 MoonBit UI 库,proton_rabbita 为其提供命令与订阅集成;两者都不是 Proton 运行时的必需组成。

进程模型

应用可执行文件既运行 MoonBit 宿主代码,也承载 CEF 的 browser process。这个进程管理原生窗口和浏览器实例,并协调 Chromium 子进程。页面脚本在 renderer 进程执行;GPU 和 utility 等子进程承担图形及其他浏览器服务。

应用进程
  MoonBit 应用逻辑
       │
  Proton ── 原生窗口
       │
  CEF browser process
       │ Chromium 进程间通信
       ├── renderer 进程:页面、JavaScript、worker
       ├── GPU 进程:图形处理
       └── utility 等进程:浏览器服务

cef_process 是供 CEF 启动子进程的辅助可执行文件。CEF 在启动时选择进程角色,同一个 helper 可用于不同子进程;helper 不执行应用的 MoonBit 入口。它与应用使用匹配的 Proton 版本和 CEF 运行时。

进程数量由 Chromium 根据页面和运行状态管理。一个 renderer 可以承载多个 task,例如页面和 worker。因此,窗口数、浏览器实例数、task 数与操作系统进程数不是一一对应的,task 的资源指标也不能直接相加作为应用总用量。

窗口与浏览器内容

一个 Proton 应用运行时可以管理多个原生窗口。每个窗口拥有主浏览器内容,也可以承载额外的子浏览器视图:

应用运行时
  ├── 窗口 A
  │    ├── 主浏览器内容
  │    └── 子浏览器视图
  └── 窗口 B
       └── 主浏览器内容

窗口负责标题、位置、尺寸和原生装饰;浏览器内容负责文档、导航、脚本和页面事件。子视图是窗口内容区内的独立浏览器,有自己的文档和边界。它的导航不改变主页面,移除它也不等于关闭整个窗口。

这些对象的生命周期不同:页面可以重新加载,renderer 可以终止,而原生窗口仍然存在。关闭窗口会进一步关闭它承载的浏览器内容。相关创建、关闭和恢复约定见应用 API。

执行与通信边界

原生 UI 对象由创建它们的线程管理。Proton 将原生事件处理接入 moonbitlang/async 的外部事件循环:原生回调排队记录并唤醒调度器,MoonBit 再处理事件和应用回调。应用异步任务可以让出执行,但同步的耗时工作仍会占用宿主事件循环。

renderer 与宿主有独立的执行环境。Proton 在页面中注入 bridge,将前端请求送到宿主注册的处理器,再把结果或错误交回页面;宿主也可以发送事件通知。载荷经过序列化,共享类型定义不会使两端共享内存。

proton_contract 描述类型化命令和事件,proton_client 供 MoonBit 前端调用;JavaScript 前端直接使用页面 bridge。宿主通过命令绑定和 capability 决定向哪些页面开放哪些操作。具体协议、取消和授权行为见命令、事件及能力。

开发与分发

开发和分发使用同一套原生宿主与浏览器架构,变化的是页面及运行时资源的来源。

场景页面来源原生运行时来源
内联 HTML编译进应用的字符串setup 安装的共享运行时与 helper
使用前端开发服务器CLI 配置的开发 URLsetup 安装的共享运行时与 helper
打包应用内联内容或打包的静态资源;URL 入口也可使用远程页面产物携带的运行时与 helper

前端开发服务器只提供页面资源,不承担宿主业务逻辑。通过普通浏览器访问同一个 URL 时,没有 Proton 注入的原生 bridge。打包将应用可执行文件、所需页面资源、CEF 运行时和 helper 组装成可分发产物,文件职责与输出路径见 Configuration 章节,打包格式及前提见 Command Line Interface 章节。

运行环境要求

以下安装命令面向 Proton 0.4.0。API 迁移方法见 Release Notes。

Proton 0.4.0 支持以下宿主构建环境。原生构建与打包面向当前操作系统,Proton CLI 不提供跨操作系统交叉编译流程。

平台架构构建要求
macOSApple SiliconXcode Command Line Tools、clang
Windowsx64Visual Studio C++ Build Tools、MSVC、Windows SDK;开发者终端
Linuxx64C/C++ 构建工具、pkg-config、X11/RandR 和 GTK 开发库;执行时需要图形环境

Ubuntu 24.04 的依赖包括 build-essential、pkg-config、libx11-dev、libxrandr-dev、libgtk-3-dev、libwebkit2gtk-4.1-dev、libnotify-dev、libnss3、libgbm1 和 libasound2t64。不同发行版的包名及运行兼容性可能不同。

NSIS 仅用于 NSIS 格式产物。签名身份和公证凭据属于分发要求,不是本地开发的前置条件。

工具命令

工具职责安装或检查
MoonBit编译器、包管理器和运行工具官方安装;moon version
Proton CLI项目开发与打包moon install moonbit-community/proton_cli@0.4.0;proton_cli --version
Warrenisomorphic 前端开发服务器与构建moon install moonbit-community/warren@0.4.5;warren --help
Node.js / npmWarren 的 JavaScript 构建工具Node.js 安装;node --version、npm --version

MoonBit 二进制目录必须在 PATH 中。使用内联 HTML 的 minimal 应用不需要 Warren 或 Node.js。本文档的前端命令不依赖已弃用的 native moonx 模式。

运行时安装

proton_cli cef setup 解析该版本的 Chromium 运行时及从源码构建的匹配 helper。首次执行需要网络,可能耗时数分钟。任意下载的 CEF 不能替代版本匹配的安装。

位置内容
~/.proton/store/不可变、共享的 CEF SDK/运行时安装
~/.proton/helpers/按平台和 Proton 版本选择的 helper
PROTON_RUNTIME_STORE可选的运行时存储绝对路径覆盖

运行时和 helper 不写入项目源码,而是在打包时组装到应用产物中。CLI 和 Proton 模块版本应保持一致。proton_cli doctor 检查项目配置、工具、运行时和 helper,不修改它们。

工具安装完成后,可按最小应用教程创建自己的应用。下面的源码检出步骤用于运行本书的仓库示例,创建应用不需要检出 Proton 仓库。

源码与运行时

示例使用仓库工作区。检出对应发布提交,安装所需工具,并先安装该版本的运行时:

git clone https://github.com/moonbit-community/proton.git
cd proton
git checkout bdb169302952db553deda6de015887c7a6a19831
moon update
proton_cli cef setup

安装完成后,在此仓库根目录执行相应示例页面中的运行命令。这些命令使用仓库工作区,不应在自己创建的应用目录中执行。

应用 API 参考

根包 moonbit-community/proton 是公开应用入口。运行前使用 App 构建器;运行中使用上下文和句柄。CLI 项目元数据不能代替运行时配置。

领域所属对象/入口参考
启动、ready、退出与任务清理App、ApplicationContext、生命周期钩子生命周期
窗口声明、实例与子浏览器App、WindowManager、WindowHandle、ViewHandle窗口
请求/响应通信契约、注册器、客户端命令
前端通知与订阅有效期emitter、客户端订阅事件
宿主操作与 renderer 授权App.capability、扩展范围能力
Cookie、浏览器存储与代理SessionHandle、启动构建器浏览器会话
签名应用更新App.update_channel、PendingUpdate自动更新
进程所有权与激活单实例构建器、上下文方法进程控制
路径、文件与构建元数据项目配置、应用路径Configuration

完整方法、类型化错误和默认参数见版本化 API 索引。本页说明各入口的适用场景、状态所有权,以及启动、取消和退出期间的行为。

应用生命周期

App 描述启动前的应用配置,通过 Proton 根包创建,在 MoonBit 异步运行时中执行。Proton 在初始化期间接入原生事件循环,应用代码不负责安装或轮询另一个 UI 循环。

入口来源

构造函数入口
html(title, html, ...)内联 HTML 文档
url(title, url, ...)URL
file(title, path, ...)本地 HTML 文件
asset(title, path, ...)托管应用资源入口

各函数返回 App 构建器,共同选项包括宽度、高度、调试模式和是否允许调整大小。entry_html、entry_url、entry_file、entry_asset 可修改已有构建器的入口。

配置应用

config、debug、add_window、on_* 回调注册等配置方法原地修改同一个 App,返回 Unit。使用瀑布语法 .. 在连续配置时保留应用值:

let app = @proton.html("Hello", "<h1>Hello</h1>")
  ..config(@proton.load_config())
  ..debug()

app.single_instance()
app.run()

直接调用 app.single_instance() 返回 Unit。构造函数仍返回 App;app.name() 等查询保留各自的返回类型,run() 和 run_or_abort() 仍是普通异步调用。

迁移原有构建链时,将配置调用前的 . 改为 ..,查询和最终的 run() / run_or_abort() 保留单点。配置校验、注册顺序和生命周期行为不变。

应用身份

@proton.load_config() 返回完整的类型化 AppConfig,加载失败时立即抛出错误。直接从该值读取 identifier、backend、frontend 和 package_config。通过 app.config(config) 安装配置,它与 app.identifier(...) 互斥。identifier 是稳定的应用身份,与窗口标题、产品名称和版本不同。

app_path()、resource_dir() 和 is_packaged() 描述执行环境。开发路径与打包资源的位置不同,前端 URL 也不是后端文件系统路径。

钩子与状态所有权

钩子含义
app_lifecycle(on_start, on_shutdown)应用级状态;启动钩子的返回值传给退出钩子
window_lifecycle(on_ready, on_close)窗口级状态;ready 返回值传给 close
on_window_close_request窗口关闭前的异步允许/拒绝决策

应用和窗口上下文提供任务组与窗口管理器。窗口上下文还提供窗口句柄和事件发送器。窗口状态不应比其引用的资源存活更久。

运行应用

调用 run() 启动已配置的应用,并等待其生命周期结束。它是异步方法,失败时抛出 AppRunError;run_or_abort() 则报告失败并中止。下面分别说明启动过程、任务所有权和退出行为。

启动顺序

启动先校验配置、解析身份与路径、初始化日志,并在启用单实例时获取锁。次实例成功转发激活请求后返回,不会创建自己的应用运行时。

主实例先建立会话,再执行应用启动钩子。启动钩子成功建立应用状态后,Proton 打开尚未由启动钩子打开的启动窗口,等待所需 bridge 初始化,再通过 ready 钩子激活初始窗口。初始激活成功后应用才进入 ready;声明窗口或收到原生创建通知都不代表已经 ready。

启动钩子可以等待 context.windows().open(id)。已提交的退出会停止启动;被拒绝的退出请求会保留应用运行,提出 quit 请求本身不等于已提交退出。

任务与清理所有权

应用任务放入应用 task group,窗口任务放入窗口 task group。退出时相应作用域取消并等待收尾;仅仅持有窗口句柄,不会使作用域之外的后台工作自动拥有正确的生命周期。

每个成功返回状态的生命周期钩子会安装配对清理钩子。若钩子在返回前失败,就没有可传给配对清理的状态,但此前成功建立的作用域仍会清理。清理钩子受取消保护;清理失败可以与原始错误一起出现在 AppRunError 中,而不会简单覆盖原始诊断。

ready/close 钩子管理窗口状态;创建通知和浏览器事件用于观察。renderer 导航或崩溃时,原生窗口仍可能存活,页面任务与订阅还必须遵守命令和事件说明的页面有效期。

退出决策与强制退出

默认的 LastWindowClosedPolicy::Quit 在最后窗口关闭后请求退出;KeepRunning 保留应用运行,直到显式调用 quit 或 exit。浏览器和子视图必须完成销毁,运行时才能正常退出。

操作契约
context.quit(exit_code=0)请求正常退出,可以被取消
context.exit(exit_code=0)强制退出并终止进程,包括退出码为零时
handle.close()请求关闭该窗口,受其关闭拦截器约束

正常退出依次执行 on_before_quit、各窗口的关闭决策、窗口关闭后的 on_will_quit。ApplicationQuitDecision::Prevent 取消退出;窗口拒绝关闭也会取消整个退出请求。取消不会重新创建已经关闭的窗口。on_quit 在运行时清理前观察最终退出码,不能再否决退出。

强制退出跳过可取消的决策,包括已经等待中的决策,但仍执行最终退出通知与运行时清理,因此不同于从外部杀死进程。已经安排的 relaunch 在清理后、进程终止前启动。

普通退出以零状态成功完成时,App.run() 返回;非零状态的普通退出会以该状态终止进程。强制退出即使状态为零也会终止。必需的收尾不能只写在 run() 后面。

命令

命令是跨渲染器与宿主边界的类型化请求/响应操作。载荷经过序列化;共享 MoonBit 类型不会共享内存,也不会让前端直接执行后端代码。

契约与绑定

API约定
proton_contract.command[Request, Response](name)声明类型化应用路由,不注册处理器
App.commands(register, targets?)为指定渲染器目标安装应用命令绑定
CommandRegistrar.bind(command, handler)绑定接收 (CommandContext, Request)、返回 Response 的异步处理器
proton_client.invoke(command, request)前端异步调用,返回 Response 或抛出 ClientFailure
proton_rabbita.invoke(...)将成功与失败映射到 Rabbita 命令

后端要求 Request 实现 FromJson、Response 实现 ToJson;前端要求相反方向的转换。契约与注册错误不同于运行中请求的失败。应用路由名称必须合法且唯一;绑定和调用时都会验证描述符。

处理器上下文标识调用方,并提供 emit_to_caller。处理器可以等待后端异步工作。校验不通过等业务结果可以建模为响应类型,而不是通信失败。

JavaScript 接口

注入的 bridge 将应用方法暴露为 window.__MoonBit__.app.<name>(request)。调用返回 Promise,失败时 reject。应用路由使用 app:,扩展操作使用 ext:。普通浏览器页面没有注入的原生 bridge。

请求作用域与页面失效

每个获准执行的请求有自己的命令作用域,等待的工作、子任务和延迟清理都属于该请求。处理器或作用域内清理失败属于请求失败(handler_failed),不表示应终止应用。真正的运行时基础设施失败仍是应用级错误。

导航、renderer 终止或 bridge 失败会使旧页面的待响应请求失效。一次 bridge 尝试失败后,该失败页面不能继续发起新的应用命令,直到建立新的有效尝试。正常初始化期间允许请求,不要求所有请求都等到 ready,以免阻断初始化工作。

调用方取消及页面失效会停止等待并请求取消未完成工作,都不会回滚已经执行的业务副作用。重试不能重复执行的操作,应使用业务操作标识或事务。消息成功提交也不能被当作业务执行成功。

取消

proton_client.invoke_with_callbacks 返回取消函数,用于取消响应观察并请求取消通信;迟到的响应会被忽略。异步 invoke 所在任务取消时,也会取消待完成请求。取消不保证撤销后端已经执行的操作。

客户端错误

变体含义
BridgeUnavailable原生 bridge 不存在
InvalidContract命令或事件描述符不合法
RemoteFailure后端拒绝,携带 code、message 和可选 detail
TransportFailure通信失败
ResponseDecode响应无法解码为声明的类型
RequestCancelled待完成请求被取消

命令错误码

RemoteFailure.code 用于识别失败类型,无需解析错误消息:

错误码含义
invalid_payload请求不符合命令的输入类型
unknown_op请求的操作未注册
handler_failed命令处理函数抛出错误
host_closed命令宿主已关闭
permission_denied页面没有调用该操作的权限

根据错误码处理失败,并将未识别的错误码作为其它远程失败处理。完整后端诊断保留在应用日志中;开发模式下也会包含在 detail 中,message 始终是面向调用方的说明。预期业务结果应放在命令的响应类型中。

资源限制

Proton 不对命令载荷设置固定的大小上限。载荷通过 JSON 序列化,仍受可用内存与底层传输约束。大消息会增加序列化、复制和解析开销。

0.4.0 没有固定的全局并发请求准入上限。应用仍应根据资源和顺序要求限制昂贵工作;bridge 不会将彼此独立的业务操作自动串行为事务。

事件

事件是从宿主发往渲染器的类型化通知,没有响应值。proton_contract.event[Payload](name) 仅声明描述符,不安装监听器,也不保留历史通知。

发送目标

API目标与有效期
CommandContext.emit_to_caller(event, payload)发起当前命令的页面
WindowContext.events()获取窗口事件发送器
WindowEventEmitter.emit(event, payload)关联窗口仍存活时向其发送

宿主载荷要求实现 ToJson。窗口发送器的有效期受窗口生命周期约束;保存在应用状态中的发送器应在窗口关闭时移除。发送事件不意味着向全部窗口广播。

订阅

proton_client.subscribe(event, listener, failure) 通过 FromJson 解码载荷,返回 Subscription。Subscription.close() 释放监听器。订阅建立可能抛出 ClientFailure;事件解码失败通过 failure 回调报告 EventDecode。

proton_rabbita.subscribe 将订阅所有权接入 Rabbita,选项包括订阅 key、重试次数、ready 命令和 client 覆盖。完整签名见 Rabbita API。

JavaScript 接口为 window.__MoonBit__.app.on(name, callback)。回调接收包含 payload 的事件对象;注册返回取消订阅函数。

传递语义

  • 订阅之前错过的通知不会重放。
  • 命令响应与事件是独立传递,二者的相对顺序不是应用同步约定。
  • 释放监听器后停止观察;事件不是持久队列或确认协议。
  • 状态变化通知可以使前端查询失效;权威快照通过命令获取。

订阅有效期与状态同步

订阅属于当前 renderer 文档或 UI 组件,所属对象销毁时应关闭订阅。重新加载会创建新文档,需要重新订阅;原生窗口继续存在不代表 JavaScript 监听器跨 reload 保留。

同步状态时,先订阅再读取初始快照,用事件使快照失效,并忽略已经被新查询替代的响应。先订阅消除了初次读取前的监听空档,但不会让两条独立消息成为原子事务。需要识别旧快照或遗漏变更时,应在业务数据中加入 revision。

on_window_created、on_render_process_gone 等应用生命周期通知是宿主回调,不是 proton_contract 前端事件。它们注册在 App 构建器上;前端也需要相关信息时,再通过命令或显式事件传递。

事件教程演示订阅与清理;Todo 教程演示通知失效后重新查询快照。

窗口与浏览器视图

窗口声明属于 App,运行中的窗口操作属于 WindowHandle。浏览器导航和开发者工具属于 WebContentsHandle,通过 WindowHandle.web_contents() 获取。

标识与声明

主窗口 ID 为 main。App.add_window(id, config, open_on_start?) 声明附加窗口,其 ID 必须非空、唯一,且不能为 main。标题是显示文本,不是窗口标识。

附加窗口默认在启动时打开。open_on_start=false 将打开推迟到 WindowManager.open(id)。ApplicationContext.windows() 和 WindowContext.windows() 提供窗口管理器。所有窗口属于同一个应用运行时。

WindowConfig(title, entry, ...) 保存尺寸、尺寸提示、主题、标题栏样式、红绿灯位置及初始 views。使用 App(config) 定义主窗口,App.main_window(config) 替换其启动配置,App.add_window(id, config, ...) 声明附加窗口。注册时会复制 views 数组。简单主窗口仍可使用 html、url、file、asset 快捷入口。

打开操作与实例身份

WindowManager.open(id) 是异步操作,返回已激活的 WindowHandle。id 选择窗口声明,不是以后每次原生实例的永久身份。关闭并重新打开后,应通过 open 或 find 获取新句柄,旧句柄不会重新有效。

激活提交前取消调用,会丢弃排队的打开操作,或关闭该操作已经创建的实例。激活成功提交后,窗口属于应用,随后取消调用方不会把它关闭。未知声明和无效窗口状态产生窗口会话错误;任务取消遵循 async 的取消语义。

hide() 保留窗口实例、浏览器和相关任务。close() 发起销毁流程且可能被拒绝,不能把关闭请求当作清理完成。窗口状态应由生命周期清理释放,而不是刚请求关闭就释放。

窗口查找与浏览器就绪

WindowManager.find(id) 返回当前活动实例,其中包括仍在启动的窗口。它不会等待原生浏览器初始化、页面加载或命令桥接就绪。获取 WebContentsHandle 同样不会等待这些条件。

这三个条件各有不同含义:

  • 原生浏览器初始化完成后,才能进行 WebContentsHandle.state() 等浏览器查询。启动期间,查询可能因浏览器尚未初始化而抛出 WindowSessionError::OperationFailed。不能假定所有浏览器操作都会排队等待初始化完成。
  • 页面加载针对当前导航。需要已加载页面的操作,应观察对应窗口或视图的 on_web_contents_event,再核对目标 URL 和页面状态。DidFinishLoad 表示加载状态转为空闲,本身不能证明目标导航成功;还需要单独处理 LoadFailed。
  • 命令桥接就绪针对与当前页面的通信。拥有浏览器句柄或收到加载完成事件,都不能单独证明桥接已经就绪。

操作应与其实际需要的条件同步。在 find() 后等待固定时长不能保证就绪,导航或关闭也可能使此前的观察结果失效。

运行时操作

领域WindowHandle 操作
显示与焦点show、hide、focus、is_visible、is_focused
几何bounds、set_bounds、position、set_position、content_size、set_content_size
窗口状态minimize、maximize、restore、set_fullscreen
外观set_theme、set_background_color、set_title、set_menu
生命周期close

原生操作可能抛出 WindowSessionError。句柄不是永久有效的:窗口关闭时,应释放窗口所属状态以及保留的事件目标。隐藏的窗口仍然存活。

关闭语义

App.on_window_close_request 注册异步回调;回调接收窗口句柄,返回 WindowCloseDecision::Allow 或 Deny。window_lifecycle.on_close 是关闭后的清理钩子,不能否决关闭。最后窗口关闭策略默认为 Quit;KeepRunning 让应用在无窗口时继续运行,需要应用提供显式退出入口。参见应用生命周期。

子浏览器视图

WindowConfig.views 在主窗口或附加窗口声明初始视图;WindowHandle.add_view 动态创建视图并返回 ViewHandle。视图是窗口内部承载的子浏览器,不是第二个顶层窗口。其边界以左上角为原点,显示状态和 z-order 独立于主页面。remove_view 移除子视图。关闭父窗口也必须完成子浏览器销毁。

浏览器与子视图边界

WindowHandle.web_contents() 和 ViewHandle.web_contents() 都返回 WebContentsHandle。导航、脚本执行、缩放、音频、开发者工具、打印、下载及会话访问使用这个共同句柄。子视图几何、显示、层级和移除保留在 ViewHandle,原生窗口控制保留在 WindowHandle。子页面导航不会导航主页面。

使用 App.on_web_contents_event 订阅主页面和子页面事件。回调携带共同句柄:window_id() 标识所属窗口,主页面的 view_id() 为 None。句柄绑定特定原生实例,复用声明 id 不会使旧句柄恢复有效。子视图不获得应用命令桥接。

on_render_process_gone 同时覆盖主页面和子视图,无需额外订阅 on_web_contents_event。renderer 终止会使页面工作失效,但不等于正常关闭窗口。应用根据终止详情决定重新加载还是展示恢复界面,并在导航后重新建立页面订阅。

视图声明只设置初始几何信息,不提供自动布局。父窗口内容布局变化时,由应用更新子视图边界。

PDF 输出

WebContentsHandle.print_to_pdf(path, options?) 是异步方法,成功完成后返回 Unit。在异步上下文调用并就地处理错误,无需订阅完成事件或关联请求编号。主页面和子页面的并发调用分别匹配完成结果。页面关闭、renderer 终止或应用退出会唤醒待完成的调用。取消只停止等待,不会终止底层原生打印任务,目标文件仍可能生成。

平台行为

WindowThemePreference 控制窗口主题,system_appearance() 返回系统外观,二者是不同概念。标题栏样式和原生控件因平台而异。红绿灯位置设置适用于 macOS;使用叠加标题栏时,前端布局需要考虑原生控件占用的空间。

TitlebarStyle::Overlay 适用于 macOS、Windows 和 Linux。叠加标题栏页面通过 await window.__MoonBit__.getTitlebarArea() 查询可用区域,返回 CSS 像素单位的 { x, y, width, height },不可用时返回 null。在布局时及窗口缩放后查询,由应用将结果应用到 CSS。拖动区域内的交互控件需要设置 -webkit-app-region: no-drag。

无障碍与无窗口执行

Web 内容使用 Chromium 原生无障碍树。页面应提供语义化 HTML、ARIA、键盘导航和可见焦点。默认遵循平台无障碍策略;App.accessibility(AccessibilityMode::AlwaysEnabled) 使无障碍树在整个运行期间保持可用。参见各平台无障碍行为。

App.headless() 或 PROTON_HEADLESS=1 使用离屏渲染,不创建原生顶层窗口。原生对话框、菜单、标题栏等依赖窗口的 UI 不可用。Linux 仍需要 X11 display,例如 Xvfb。因此,浏览器自动化与原生窗口行为需要不同的验证环境。

扩展与能力

扩展注册可复用的宿主操作。渲染器能力安装扩展后端,并向指定渲染器目标授予权限范围。添加 proton_ext 模块依赖仅使代码可用,不安装处理器,也不授予访问权限。

注册与目标

App.capability(capability, targets?) 配置安装与访问范围。不指定 targets 时,授权作用于主入口。RendererTarget.entry(window=...) 和 RendererTarget.bundled(window=...) 选择命名窗口的入口或打包页面目标。窗口属于同一个应用,不代表其权限自动变为全局。

应用命令使用 app: 路由,扩展操作使用 ext:<extension>/<operation>。JavaScript 通过 window.__MoonBit__.core.invokeOp(route, request) 调用已安装操作,返回 Promise。

文件系统权限范围

proton_ext/fs.capability 接收 PermissionRoot,每项将一个宿主目录与允许的操作绑定。范围由后端配置,渲染器请求不能扩大它。

属性行为
相对根路径或请求路径相对于 resource_dir() 解析
超出授权根目录的路径拒绝,包括符号链接逃逸
根目录授权中没有列出的操作不被该授权允许
文本载荷UTF-8

文件系统操作包括 read_file、write_file、mkdir、readdir、remove、rmdir、rename、realpath、exists、kind 和 size。扩展内部将规范路径检查和操作串行执行。

可用性与错误

缺少能力声明时,路由不可用。扩展已安装时,仍可能因权限范围、参数、平台限制或操作系统错误拒绝请求。这些错误通过命令 bridge 报告;安装不意味着所有原生操作必然成功。

各能力的权限范围类型见扩展 API,主要平台差异列于下表。

平台差异与选择

以下是 0.4.0 已实现能力的主要边界,不承诺缺少桌面服务时仍可用:

能力macOSWindowsLinux
文件、路径、宿主 HTTP、子进程支持支持支持
原生对话框、文本剪贴板支持支持依赖桌面会话/GTK 等后端
系统通知扩展支持未实现未实现
托盘菜单及平台事件点击、右击、双击和菜单依赖 AppIndicator/Ayatana 与桌面会话
桌面来源与缩略图显示器;缩略图需要屏幕录制权限可见有标题窗口及 GDI 缩略图X11/RandR 显示器;缩略图为 null

capability 授权与操作系统授权不同。声明屏幕或媒体能力不会自动获得系统隐私权限;系统拒绝、后端不可用与应用未授权应分别处理。托盘等提供 support 查询的能力应先检查支持情况,菜单事件也比各平台鼠标手势更可移植。

宿主网络与子进程

net 扩展在宿主执行 HTTP 请求,返回状态、响应头及 UTF-8 文本;它不等于浏览器 fetch,不共享浏览器 cookie jar,也不自动跟随重定向。二进制数据不是该文本响应接口的无损用途。process 扩展创建的进程属于扩展应用生命周期,wait 回收句柄,kill 后仍需 wait,应用退出会取消并回收剩余子进程。

完整扩展清单及各范围类型见扩展 API。访问某个 API 前分别确认:目标授权、操作系统授权、平台后端支持。

浏览器会话

浏览器会话保存 Cookie、缓存、认证状态和 Web storage。窗口创建前即可通过 ApplicationContext.session() 获取句柄,也可通过任意 WebContentsHandle.session() 获取。两者访问同一个应用会话。页面关闭不使会话失效,清理共享数据也会影响使用该 profile 的其它页面。

启动配置

App.session_partition(name) 选择应用 sessionData 目录下的持久 profile。这是应用启动配置,不是每窗口独立的无痕开关;运行时中的窗口使用该配置的 profile。保持应用 identifier 和 partition 稳定才能跨启动保留数据。

App.proxy(server, bypass?) 配置 Chromium 启动级代理,运行中不能修改。它控制浏览器流量,不会自动配置宿主内每一个 MoonBit 网络库。

on_session_created 在应用启动钩子创建窗口前提供 partition 和解析后的数据路径。打包资源与浏览器 profile 是不同目录,不应将可写 profile 放入应用 bundle。

操作

API效果
get_cookies异步读取匹配的 Cookie,可选择 URL 及是否包括 HttpOnly
set_cookie以 URL、名称、值及可选 domain/path/安全属性设置 Cookie
delete_cookies、flush_cookies删除选定 Cookie,或将持久 Cookie 存储刷盘
clear_cache、clear_storage_data清缓存或选定类型的 Web storage
clear_auth_cache清除 HTTP 认证缓存
clear_certificate_exceptions清除证书例外状态
close_all_connections关闭会话连接

操作要求应用运行时存活,不要求页面存活;错误类型为 SessionError。KeepRunning 无窗口期间仍可使用句柄;应用退出会唤醒待完成的 Cookie 查询并使保留的会话句柄失效。并发 Cookie 查询相互独立。清理浏览器存储不会删除应用自己管理的文件,也不等于完成业务登出;后端凭据和 UI 状态需单独处理。

完整属性及 StorageDataKind 变体见API 参考。

应用更新

应用更新替换分发中的应用,不更新开发 CLI 或 Mooncakes 依赖。运行前通过 App.update_channel(endpoint, public_keys, check_on_launch?, freshness_days?) 配置。

更新渠道与检查

endpoint 必须是 HTTPS,至少配置一个受信任 RSA 公钥。默认开启自动检查,manifest 新鲜度默认为 30 天且必须为正数。公钥格式由 updater public-key 校验。

自动检查在启动成功后进行,失败写入日志而不使应用启动失败。显式检查使用 ApplicationContext.check_for_update(),处理结果与异常。NotConfigured 与 UpToDate 不同;应用结束后,保留的上下文不再拥有活动更新渠道。

PendingUpdate 表示 manifest 已通过信任、新鲜度和 revision 检查。它提供 version、revision、size 和可选 notes URL,此时尚未下载产物。

下载与安装

PendingUpdate.download() 将产物写入私有暂存区域,并校验大小、摘要和签名。所有平台上都不会替换应用文件或退出。相同 revision 的下载已准备好时,重复调用不会再次下载。

下载完成后,在用户选择应用更新时调用 ApplicationContext.quit_and_install():

match context.check_for_update() {
  Available(update) => {
    update.download()
    context.quit_and_install()
  }
  UpToDate | NotConfigured => ()
}

请求遵循退出生命周期。拒绝退出会取消本次安装请求,保留下载结果供稍后重试。退出获准后,Proton 先关闭运行时并完成清理,再开始安装。如果启用了单实例模式,替换期间仍保留独占权。Windows 在旧进程退出前将独占权交给 NSIS 安装器,由安装器在安装完成后释放;macOS 和 Linux 持锁替换应用产物,完成后释放锁并启动新版。交接成功后旧进程终止,退出码为零时也不会继续执行 App::run() 后的代码。

方法返回仅表示请求被接受,不代表新版已经启动成功。没有下载结果,或存在冲突的退出、重启请求时立即报错;后续安装或启动失败由 App::run() 报告。强制 exit() 会覆盖待执行的更新请求。更新退出请求待处理时,不允许主动释放已有的单实例锁;已释放所配置单实例锁的应用不能请求安装。普通退出丢弃下载结果而不安装;应用清理失败也不会开始安装。旧的受管理更新产物在新版成功启动后清理。

发布者责任

应用 identifier 保持稳定,revision 单调递增。CLI package 的更新元数据选项指定产物位置、发布时间和 revision。updater public-key 只格式化公钥,不生成密钥、签署更新 manifest 或托管服务器。系统签名/公证与更新器签名验证承担不同检查,配置其中一项不会自动配置另一项。

进程控制与指标

进程控制决定哪个应用实例拥有运行时,以及后续启动如何将请求传给它。窗口是否可见、Chromium 任务的资源用量属于不同的状态,应分别查询。

单实例所有权

App.single_instance() 按应用身份选择一个持有进程。后续实例转发 URL、文档或 reopen 激活,主实例事件循环接受后返回;接受不代表异步激活处理器已经完成。转发有五秒期限,失败不会终止主实例,也不会另起一个持有者。

ApplicationContext.has_single_instance_lock() 查询所有权,release_single_instance_lock() 在运行期间释放锁。释放不终止进程;应用应明确决定是否允许另一个进程成为持有者。激活处理需要同时考虑初次启动和后续转发。

应用状态与窗口状态

应用的 focus、hide/show、active/hidden 查询及 readiness 描述整个应用;窗口句柄控制一个窗口。隐藏窗口仍然存活,KeepRunning 应用也能在没有窗口时继续运行。后台应用必须提供明确的退出入口。

ApplicationContext.focus(steal=...) 在 macOS 激活应用,steal 决定是否允许替换另一个前台应用;在 Windows 和 Linux 聚焦第一个可见窗口。应用级 hide、show、is_active 和 is_hidden 仅适用于 macOS,其它平台抛出 AppControlError::UnsupportedPlatform。@proton.is_ready() 报告应用就绪状态。

CEF 通过 on_render_process_gone 报告 renderer 终止,但不提供同等的其它子进程退出通知,因此不支持 Electron 的 child-process-gone。浏览器操作映射及其它不支持的 Electron 对应项见页面控制参考。

任务指标

ApplicationContext.task_metrics() 返回 Chromium task 的 AppTaskMetric 列表。renderer 和多个 worker 可以共享进程。task id 标识任务;多个条目的 process_cpu_percent 和 process_memory_bytes 可能完全相同,因为它们属于同一个进程。不能将各行相加作为进程或应用总用量。

采样间隔尚未完成时 CPU 为零,100% 表示一个核心满载;内存测量前为 -1,不是零字节。这些观测不赋予应用对进程的所有权,不应借此绕过 Proton 生命周期单独杀死 helper。

API 参考

创建项目

本章用 minimal 模板创建一个桌面窗口,运行它、修改页面,再构建可执行文件。开始前请完成环境准备。

1. 创建应用

在存放项目的目录中执行,不要放在另一个 MoonBit 工作区内部:

proton_cli new hello-proton --template minimal --yes
cd hello-proton

这里需要明确指定 --template minimal:非交互模式的默认模板是 isomorphic。--yes 接受生成器的默认值。准备分发的应用可以在创建时用 --identifier com.example.hello-proton 指定标识。

模板文件

hello-proton/
  moon.mod
  proton.project.json
  app/
    moon.pkg
    main.mbt
  • moon.mod 定义模块名称和带版本的依赖。导入库中的包之前,先在这里添加该库。
  • app/moon.pkg 导入入口使用的包,并将其声明为原生可执行包。
  • app/main.mbt 定义 async 入口、HTML 和应用构建器。
  • proton.project.json 告诉 CLI 要运行哪个包,以及如何标识和打包应用。

模块依赖和包导入是两个层次。模块依赖使模块可用;包导入选择该包需要的 API 与别名。

2. 准备并运行

在 hello-proton/ 中执行:

moon update
proton_cli cef setup
proton_cli dev

应用会打开原生窗口,显示模板中的欢迎信息。应用运行期间终端会被占用。继续下一步前,请先关闭窗口。

CLI 构建原生可执行文件,并携带项目配置启动它。此模板使用内联 HTML,没有需要启动的前端服务器。

3. 修改页面

将 app/main.mbt 的全部内容替换为:

///|
async fn main {
  let html =
    #|<!doctype html>
    #|<html lang="en">
    #|<meta charset="utf-8">
    #|<title>Hello Proton</title>
    #|<style>
    #|  body { font: 18px system-ui; padding: 32px; }
    #|</style>
    #|<h1>Hello from MoonBit</h1>
    #|<p>This page lives inside a desktop window.</p>
    #|</html>
  @proton.html("Hello Proton", html, width=900, height=700)
  ..config(@proton.load_config())
  .run_or_abort()
}

再次执行 proton_cli dev,应看到“Hello from MoonBit”、一段说明文字,以及初始大小为 900 × 700 的窗口。

@proton.html 创建应用构建器,第一个参数是窗口标题,第二个参数是 HTML 文档。#| 开头的各行组成 MoonBit 多行字符串。

..config(@proton.load_config()) 加载完整应用配置(包括必需的应用标识),并将它安装到应用。仅有显示标题并不能标识应用。

.run_or_abort() 持续运行到应用退出,并报告启动或运行错误。入口是 async 函数,所以需要保留模板中的 moonbitlang/async 导入。

4. 检查并构建

关闭窗口后执行:

moon check --target native
proton_cli build

构建成功会生成原生可执行文件,但还没有组装成可分发的应用。构建与分发会说明后续步骤。

如果应用无法启动,执行 proton_cli doctor 并查看终端中的完整错误。如果构建成功却仍显示旧页面,先停止旧进程,再重新运行 dev;此模板没有提供前端热更新的服务器。

下一步

想为当前页面增加交互,可以阅读从前端调用后端;想同时用 MoonBit 编写界面,可以跟随完整前后端示例。

从前端调用后端

命令让前端请求原生后端执行操作。本章为最小项目添加一个可运行的问候表单,先用普通 JavaScript 展示原生通信边界;Todo 教程则使用共享 MoonBit 类型和 Rabbita。

添加契约依赖

在 moon.mod 已有的 import { ... } 块中加入下面一项,保留其他依赖:

"moonbit-community/proton_contract@0.4.0",

将 app/moon.pkg 替换为:

import {
  "moonbitlang/core/json",
  "moonbitlang/async",
  "moonbit-community/proton",
  "moonbit-community/proton_contract",
}

supported_targets = "native"

pkgtype(kind: "executable")

在项目根目录运行 moon update。模块依赖让库可用,包导入则让入口能使用 @proton_contract。

定义、注册并调用命令

将 app/main.mbt 替换为下面的完整示例:

///|
struct GreetRequest {
  name : String
} derive(FromJson, ToJson)

///|
let greet : @proton_contract.Command[GreetRequest, String] =
  @proton_contract.command("greet")

///|
async fn main {
  let html =
    #|<!doctype html>
    #|<html lang="en">
    #|<meta charset="utf-8">
    #|<title>Greeting</title>
    #|<style>body { font: 18px system-ui; padding: 32px; }</style>
    #|<label>Name <input id="name" value="MoonBit"></label>
    #|<button id="greet">Greet</button>
    #|<p id="result" role="status"></p>
    #|<script>
    #|  document.querySelector("#greet").onclick = async () => {
    #|    const result = document.querySelector("#result");
    #|    try {
    #|      result.textContent = await window.__MoonBit__.app.greet({
    #|        name: document.querySelector("#name").value
    #|      });
    #|    } catch (error) {
    #|      result.textContent = String(error);
    #|    }
    #|  };
    #|</script>
    #|</html>
  @proton.html("Greeting", html)
  ..config(@proton.load_config())
  ..commands(fn(registrar) raise {
    registrar.bind(greet, (_context, request) => {
      "Hello, " + request.name + "!"
    })
  })
  .run_or_abort()
}

执行 proton_cli dev。输入“Ada”,点击 Greet,页面应显示“Hello, Ada!”。

这里有三个相互连接的部分:

  1. GreetRequest 描述 JSON 请求,greet 声明路由及响应类型。
  2. ..commands(...) 通过 registrar.bind 注册处理器,处理器接收调用方上下文和解码后的请求。
  3. window.__MoonBit__.app.greet(...) 返回一个表示响应的 JavaScript promise,页面等待结果并处理拒绝。

仅声明描述符并不会注册处理器。应用中的命令名称需要保持唯一。

如果在普通浏览器打开页面,window.__MoonBit__ 不存在。请使用 proton_cli dev 启动桌面应用,而不是仅打开前端 URL。

参数与返回值

对象属性 name 对应请求字段。操作需要更多输入时,在请求中添加可序列化字段。返回值可以是字符串、数字、数组,也可以是实现 ToJson 的结构体。

原生处理器运行在后端,适合执行业务校验和原生操作。渲染器不能借此直接调用任意 MoonBit 函数。

错误处理

请求格式错误、命令不存在或处理器失败,都会使 JavaScript promise 被拒绝。保留调用外层的 try/catch,并显示有用的错误信息。

预期的业务结果应作为响应数据。例如 Todo 模板返回 Changed、InvalidTitle 或 MissingTodo,前端将它们与描述 bridge、传输和解码失败的 ClientFailure 分开处理。

不要因为前端按钮回调已经执行,就假定请求成功。依赖操作成功的界面更新,应等待返回结果。

异步操作与调用方上下文

命令处理器可以执行异步操作。上下文用于识别调用方,也可以向其发送类型化事件。向前端发送事件会在这个示例上继续修改。

向前端发送事件

后端需要通知前端状态变化时,可以使用事件。本章继续修改从前端调用后端中的问候示例:按钮仍然调用命令,但页面显示的消息改为来自事件。

声明事件

在 app/main.mbt 中,将下面的声明放在已有的 greet 描述符附近、main 函数之外:

///|
let greeted : @proton_contract.Event[String] =
  @proton_contract.event("greeted")

事件名为 greeted,载荷是字符串,没有响应类型:发送事件并不要求前端返回一个值。

发送给调用方

在 ..commands(...) 中,将已有的 registrar.bind(greet, ...) 调用替换为:

registrar.bind(greet, (context, request) => {
  let message = "Hello, " + request.name + "!"
  context.emit_to_caller(greeted, message)
  message
})

context.emit_to_caller 将事件发送给调用此命令的页面。最后的 message 仍是命令响应。响应和事件是两次独立交付,不要依赖它们之间的特定到达顺序。

先监听,再触发操作

将 HTML 字符串中 <script> 与 </script> 之间的内容替换为下面的 JavaScript。在 MoonBit 多行字符串里,与前例一样为每行加上 #| 前缀:

let stopListening;
document.querySelector("#greet").onclick = async () => {
  const result = document.querySelector("#result");
  const app = window.__MoonBit__.app;
  try {
    if (!stopListening) {
      stopListening = app.on("greeted", ({ payload }) => {
        result.textContent = "Event: " + payload;
      });
    }
    await app.greet({ name: document.querySelector("#name").value });
  } catch (error) {
    result.textContent = String(error);
  }
};
window.addEventListener("pagehide", () => {
  stopListening?.();
});

启动应用,输入名字后点击 Greet,预期显示“Event: Hello, Ada!”。再次点击会更新内容,而不会新增一个监听器。

第一次点击先注册监听,再调用命令,避免示例在订阅前就发送第一条通知。app.on 返回取消订阅函数。这里保留到页面离开;基于组件的界面则应在所属组件销毁时释放它。

在命令之外通知窗口

后台工作有时需要在发起它的命令已经结束后继续通知窗口。Todo 模板在 window_lifecycle(on_ready=...) 中保存 context.events(),在 on_close 中移除目标。保存的 WindowEventEmitter 可以在窗口存活期间发送类型化事件。

需要明确选择接收目标,不要保留已经关闭窗口的 emitter,也不要把每个事件都当作全局广播。建立和移除关联的完整过程见 Todo 示例。

事件与状态

监听器不在时可能错过事件。对于应用状态,应通过事件使数据失效,再查询最新快照。Todo 应用会先订阅,再做首次查询,并在 todos_changed 时刷新。

需要确认请求的操作结果时,使用命令响应;需要通知观察者时,使用事件。如果业务要求持久交付或重放,应在应用状态模型中实现,不应假定事件 bridge 提供这些保证。

管理窗口

本教程在最小应用中增加辅助窗口,并控制窗口的显示和关闭行为。请从该项目开始,保留已有包导入,用下面的代码替换应用入口。

创建第二个窗口

将 app/main.mbt 替换为:

///|
async fn main {
  @proton.html("Main", "<h1>Main window</h1>", width=900, height=700)
  ..config(@proton.load_config())
  ..add_window(
    "help",
    @proton.WindowConfig(
      "Help",
      @proton.AppEntry::Html("<h1>Help</h1><p>Close this window to return.</p>"),
      width=480,
      height=320,
    ),
  )
  ..window_lifecycle(
    on_ready=context => {
      let id = context.handle().id()
      println("Ready: " + id)
      id
    },
    on_close=id => println("Closed: " + id),
  )
  .run_or_abort()
}

执行 proton_cli dev,应同时打开 Main 和 Help。终端在每个窗口就绪和关闭时输出其 ID。关闭 Help 后 Main 仍然运行,关闭最后一个窗口则退出应用。

主窗口 ID 是 main,辅助窗口 ID 必须非空、唯一,且不能使用 main。ID 在代码中标识窗口,标题则用于显示。

按需打开窗口

add_window 默认在启动时打开声明的窗口。设置 open_on_start=false 后,只声明窗口而不立即打开;之后从应用或窗口上下文调用 context.windows().open("help")。

例如,在上面的 Help 声明中加入 open_on_start=false,然后在 .run_or_abort() 之前添加以下构建器调用,即可从应用启动钩子打开它:

..app_lifecycle(
  on_start=context => { ignore(context.windows().open("help")) },
  on_shutdown=_ => (),
)

后续用户操作也可以通过保留的应用上下文调用相同的窗口管理器操作。第二个窗口不需要第二套 Proton 运行时。

使用与释放句柄

on_ready 中的 context.handle() 返回当前 WindowHandle,可用于 focus()、hide()、show()、close() 等操作。浏览器操作通过 handle.web_contents() 获取。

生命周期回调的返回值会成为对应 on_close 的参数。示例返回窗口 ID,Todo 模板则用这个 ID 移除保存的事件目标。将窗口所属状态放在这一生命周期内管理,关闭后停止使用句柄。

决定应用何时退出

默认策略是在最后一个窗口关闭后退出。托盘和后台应用可以加入:

..last_window_closed_policy(@proton.LastWindowClosedPolicy::KeepRunning)

此时关闭全部窗口不会结束进程。在实际应用采用该策略前,应先提供明确的退出操作。

需要异步决定是否允许关闭时,使用 on_window_close_request,返回 WindowCloseDecision::Allow 继续关闭,返回 Deny 保持窗口打开。确认对话框或未保存文档检查应放在这里,on_close 则用于关闭后的清理。

平台相关的窗口行为

原生窗口装饰和可用控制项因平台而异。依赖平台专属方法前,请查询窗口 API。使用叠加标题栏时,需要一起处理前端标题栏布局与原生配置。

辅助页面也是独立的 capability 目标,只应授予其所需的宿主操作,见原生能力。

使用原生能力

扩展向渲染器提供可复用的宿主操作。capability 会安装扩展后端处理器,并授予特定范围的访问权限。本教程在最小应用中加入文件读取功能:后端授权一个目录,页面请求读取其中的文本文件。

添加文件系统扩展

在 minimal 项目的 moon.mod 中,将以下依赖加入 import { ... }:

"moonbit-community/proton_ext@0.4.0",

在 app/moon.pkg 中使用以下导入,然后执行 moon update:

import {
  "moonbitlang/async",
  "moonbit-community/proton",
  "moonbit-community/proton_ext/fs",
}

supported_targets = "native"

pkgtype(kind: "executable")

准备文件

在项目根目录创建 workspace 目录,并在其中创建 message.txt,内容为:

Hello from the filesystem.

将 app/main.mbt 替换为:

///|
async fn main {
  let html =
    #|<!doctype html>
    #|<html lang="en">
    #|<meta charset="utf-8">
    #|<title>Read a file</title>
    #|<button id="read">Read message.txt</button>
    #|<pre id="result" role="status"></pre>
    #|<script>
    #|  document.querySelector("#read").onclick = async () => {
    #|    const result = document.querySelector("#result");
    #|    try {
    #|      const reply = await window.__MoonBit__.core.invokeOp(
    #|        "ext:fs/read_file", { path: "./workspace/message.txt" }
    #|      );
    #|      result.textContent = reply.content;
    #|    } catch (error) {
    #|      result.textContent = String(error);
    #|    }
    #|  };
    #|</script>
    #|</html>
  @proton.html("Read a file", html)
  ..config(@proton.load_config())
  ..capability(
    @fs.capability([
      @fs.PermissionRoot("./workspace", ["read_file"]),
    ]),
  )
  .run_or_abort()
}

执行 proton_cli dev,点击 Read message.txt,页面应显示文件内容。这是真正的宿主文件读取,不是浏览器文件选择器。

理解授权内容

这个 capability 包含三个相关选择:

  • 操作: 只允许 read_file,页面不能利用此授权写入或删除文件。
  • 根目录: 只允许访问 ./workspace 内的路径。
  • 目标: 未显式传入 targets 时,授权给主窗口入口。

底层路由 ext:fs/read_file 属于文件系统扩展,应用命令则使用独立的 app: 路由空间。无需自行注册文件系统处理器。

相对根目录和请求路径以 CLI 为应用设置的 @proton.resource_dir() 为基准,不应假定它们相对于任意终端工作目录。

主动检查失败情况

将请求路径改为 workspace 中不存在的文件,调用应失败,catch 分支会显示错误。再改为允许根目录之外的文件,此授权应拒绝访问。

删除 ..capability(...) 不会阻止应用启动,但路由变得不可用。仅添加扩展依赖并不等于授予权限。

多窗口与持久化文件

为第二个窗口添加能力时,通过 RendererTarget::entry(window="...") 或 RendererTarget::bundled(window="...") 显式选择目标,只授予页面功能真正需要的权限。

本地 workspace 目录适合这个开发练习。安装资源目录可能只读,持久化文件应放在合适的可写应用数据目录或用户选择的位置。需要随应用分发的文件通过资源配置打包。

其他能力

对话框、剪贴板、shell、托盘等扩展也采用显式安装和授权的方式,但各自定义作用范围与平台支持。此版本的通知扩展面向 macOS,不能从框架的平台列表推断每个扩展都支持全部平台。

需要某项能力时,查阅扩展 API中的 capability 构建器及请求、响应类型。

构建完整 Todo 应用

本教程使用 isomorphic 模板连接共享契约、原生状态和 Rabbita 界面。先运行生成的 Todo 应用,再跨三个模块加入 Complete all 和 Reopen all 操作。

请先完成环境准备,包括 Warren 构建工具需要的 Node.js。无需修改之前的 minimal 应用。

创建并体验应用

在另一个 MoonBit 工作区之外执行:

proton_cli new todo-app --template isomorphic --yes
cd todo-app
moon update
moon install moonbit-community/warren@0.4.5
proton_cli cef setup

生成的配置已经调用安装好的 Warren 可执行文件。启动应用:

proton_cli dev

添加“Alpha”和“Beta”,标记其中一个完成,再搜索它。生成的应用已经支持创建、完成、删除和过滤查询。列表保存在内存中,重启后会清空。

编辑前先关闭应用。保留生成的模块名与包导入,以下修改均在现有模板上追加。

模板组织

isomorphic 模板采用 MoonBit 前端,包含三个模块:

todo-app/
  moon.work
  proton.project.json
  shared/
    moon.mod
    moon.pkg
    todo_contract.mbt
  backend/
    moon.mod
    app/
      moon.pkg
      main.mbt
    todo/
      moon.pkg
      backend.mbt
      commands.mbt
  frontend/
    moon.mod
    main/
      moon.pkg
      main.mbt
    internal/query/
    public/

根目录的 moon.work 连接三个模块:

  • shared 定义可序列化的载荷,以及命令、事件描述符,供两个构建目标使用。
  • backend 编译为原生代码。todo/backend.mbt 保存 Todo 状态和业务操作,todo/commands.mbt 绑定操作,app/main.mbt 启动应用。
  • frontend 编译为 JavaScript。main/main.mbt 定义 Rabbita 界面,public/ 保存 HTML 和样式表。

模板中的 frontend/internal/query 管理查询状态与订阅。它属于生成的应用,可以随应用一起修改,并不是 Proton 公共 API。

应用命令通过普通 bind 调用注册,这个模板无需命令代码生成规则。

理解已有流程

已有的创建操作中,Create 调用 create_todo,后端校验并修改状态,MutationReply 返回成功或业务拒绝,todos_changed 让前端快照失效并触发查询。

前端草稿是本地 UI 状态,后端列表是应用状态的权威来源。新操作应修改后端完整列表,而不只是当前搜索结果中可见的行。

1. 扩展共享契约

在 shared/todo_contract.mbt 末尾追加:

///|
pub(all) struct SetAllCompletedRequest {
  completed : Bool
} derive(ToJson, FromJson)

///|
pub extend SetAllCompletedRequest with ToJson::{to_json}

///|
pub extend SetAllCompletedRequest with FromJson::{from_json}

///|
pub let set_all_completed : @proton_contract.Command[
  SetAllCompletedRequest,
  MutationReply,
] = @proton_contract.command("set_all_completed")

布尔值选择完成或重新打开。复用模板的 MutationReply,可以继续使用现有响应处理逻辑。两个构建目标导入同一个描述符。

2. 实现状态修改

在 backend/todo/backend.mbt 末尾追加:

///|
fn Backend::set_all_completed(
  self : Backend,
  completed : Bool,
) -> @shared.MutationReply {
  let mut changed = false
  for index = 0; index < self.todos.length(); index = index + 1 {
    let todo = self.todos[index]
    if todo.completed != completed {
      self.todos[index] = { ..todo, completed, }
      changed = true
    }
  }
  if changed {
    self.version += 1
  }
  @shared.Changed(version=self.version)
}

操作遍历完整列表,有实际变化时只将修订号增加一次。重复相同操作不会改变修订号;即使列表为空,也返回当前版本。

3. 注册处理器并通知数据失效

在 backend/todo/commands.mbt 的已有 Backend::register_commands 方法内,与其他绑定并列添加:

registrar.bind(@shared.set_all_completed, (_context, request) => {
  let reply = self.set_all_completed(request.completed)
  self.notify(reply)
  reply
})

保留原有绑定。self.notify 是生成后端中已有的辅助方法,会向应用窗口生命周期中保存的事件目标发送 todos_changed。

这里不需要新事件类型。它与其他 Todo 修改一样改变了状态,已有观察者重新执行相同查询即可。

4. 接入前端操作

本步骤全部修改 frontend/main/main.mbt。

在已有的 enum Msg 内添加一个分支:

SetAllCompleted(Bool)

在 update 已有的 match msg 内添加以下分支:

SetAllCompleted(completed) =>
  (
    { ..model, error: None, },
    @proton_rabbita.invoke(
      @shared.set_all_completed,
      { completed, },
      reply => emit(MutationReceived(reply)),
      error => emit(CommandFailed(error)),
    ),
  )

model、emit、MutationReceived 和 CommandFailed 已由该函数与模块定义。成功路径复用模板的修改响应处理,错误路径继续显示 bridge 失败。

在 view 中找到 div(class="todo-toolbar", [...]),将下面两个按钮追加到其子元素数组中,放在已有 Refresh 按钮之后:

button(
  type_="button",
  on_click=emit(SetAllCompleted(true)),
  "Complete all",
),
button(
  type_="button",
  on_click=emit(SetAllCompleted(false)),
  "Reopen all",
),

模板已经导入 button,无需增加导入。消息中的布尔值经由请求传到后端。

5. 检查行为

在 backend/todo/backend_wbtest.mbt 中追加这个回归检查:

///|
test "bulk completion changes the whole list once" {
  let backend = Backend()
  ignore(backend.create("Alpha"))
  ignore(backend.create("Beta"))
  assert_true(backend.set_all_completed(true) is @shared.Changed(version=3))
  assert_true(backend.snapshot("").todos.all(todo => todo.completed))
  assert_true(backend.set_all_completed(true) is @shared.Changed(version=3))
  assert_true(backend.set_all_completed(false) is @shared.Changed(version=4))
  assert_true(backend.snapshot("").todos.all(todo => !todo.completed))
}

在项目根目录执行:

moon check --target js,native
moon -C backend test todo --target native
moon -C frontend test --target js
proton_cli dev

添加两个待办项,Complete all 应将两项全部完成,Reopen all 应重新打开两项。搜索到其中一项后重复操作,再清空过滤条件,应看到两项都发生变化。重复点击 Complete all 不应继续增加修订号。

如果后端变化但显示不更新,检查是否保留 self.notify 和窗口 attach/detach 钩子。如果新命令不可用,检查注册代码并重启后端。

界面如何保持最新

模板的 todo_app 创建 list_todos 查询,并用 todos_changed 使其失效。应用内的查询辅助包先订阅,再获取数据,在输入变化时取消被替代的旧请求。

修改响应刷新发起操作的界面,事件则使观察者的数据失效。两者可能重叠,所以辅助包跟踪请求代次,避免旧结果覆盖新的搜索。这些是 frontend/internal/query 中的应用代码,不是另一个服务器,也不是 Proton 的公共状态管理框架。

构建完成后的应用

停止开发进程,先执行 proton_cli build。下一步按宿主平台选择;以下命令都在 todo-app 项目根目录执行。

macOS 与 Windows

本地验收先生成未签名的 app 格式;Windows 的 app 是包含 EXE 和依赖的目录,不是 macOS bundle。

proton_cli package --release --format app --dry-run
proton_cli package --release --format app

打开 CLI 输出的 dist 下的应用路径。macOS 从 .app 启动,Windows 从产物目录中的应用 EXE 启动,不能只复制 EXE。本步骤不需要 NSIS,也没有完成面向用户分发的签名/公证。

Linux

默认模板不附带图标。将一张实际有效的 PNG 图标保存为项目内的 icons/app.png,并在 proton.project.json 的已有 package 对象中加入:

"icons": ["icons/app.png"]

确认 appimagetool --version 能执行。若尚未安装,可在 Ubuntu x64 终端执行以下命令,将工具保存在用户目录中;解包方式无需依赖 FUSE 来运行打包工具:

mkdir -p "$HOME/.local/lib/proton-appimagetool"
cd "$HOME/.local/lib/proton-appimagetool"
curl -fL -o tool.AppImage https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
echo 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0  tool.AppImage' | sha256sum -c -
chmod +x tool.AppImage
./tool.AppImage --appimage-extract
ln -sf squashfs-root/AppRun appimagetool
export PATH="$HOME/.local/lib/proton-appimagetool:$PATH"
appimagetool --version

回到 todo-app 项目根目录,再执行:

proton_cli package --release --format appimage --dry-run
proton_cli package --release --format appimage

从图形会话启动输出的 AppImage。运行环境无法挂载 AppImage 时,可先对产物执行 --appimage-extract,再运行生成目录中的 AppRun。

验收

在开发服务器已经停止的情况下启动产物,添加两个项目并验证 Complete all、Reopen all 和搜索操作。打包失败、启动失败与按钮行为失败是不同阶段;仅 dry-run 成功不算完成。

本教程的列表保存在内存,重启清空是预期行为。持久化应在后端实现。图标与分发要求见打包参考。

Command Line Interface

本页列出 proton_cli 的全部命令及其参数。应用的 IPC 命令见命令,配置文件字段见项目配置。

调用形式与全局选项

proton_cli [--cwd <directory>] <command> [options]
选项含义
-C、--cwd <directory>工作目录,默认 .;对子命令也有效
-h、--help显示所选命令的帮助
-V、--version显示 CLI 版本,用途与 version 相同

--config、--moon-target-dir 和 doctor 报告路径以所选工作目录为基准;dev --frontend-path 和打包图标、输出路径以项目配置目录为基准。配置文件内的路径以配置文件目录为基准,frontend.dist 以 frontend 目录为基准。显式命令选项覆盖对应的配置值。

命令索引

命令用途
help显示命令帮助
version输出 CLI 版本
new创建应用项目
dev构建并以开发模式运行应用
build构建前端资源和原生后端
package构建并组装分发产物
doctor输出环境和项目诊断
cef setup安装匹配当前发行版的运行时和 helper
cef requirements输出内嵌的 CEF 版本要求
updater public-key校验并格式化更新器信任的公钥

help

proton_cli help [command] [subcommand]

显示帮助,不执行所选命令。例如 proton_cli help cef setup 说明运行时安装命令,不执行安装。对应的选项形式是 proton_cli <command> --help。

version

proton_cli version

输出 proton_cli <version>。没有专属选项,不执行更新检查。

new

proton_cli new [path] [options]

创建项目源码。未指定 --yes 且不在 CI 环境时使用交互提示。以下默认值适用于没有交互输入或显式选项覆盖的情况。

参数 / 选项默认值及含义
[path]todo;目标目录
--template <name>isomorphic;可选 minimal、isomorphic
--title <text>根据项目目录名生成;应用及窗口标题
--author <name>username;MoonBit 模块作者
--identifier <id>dev.proton.<规范化项目名>;反向域名形式的应用标识
--width <pixels>960;正整数
--height <pixels>640;正整数
--check、--no-check默认开启;创建后执行 moon check --target js,native
--git、--no-git显式启用或禁用 Git 初始化;非交互默认不初始化 Git
-y、--yes跳过提示并接受默认值
--dry-run打印计划生成的文件,不写入文件

minimal 创建内嵌 HTML 的原生模块;isomorphic 创建 shared、frontend、backend 三个模块。无效标识、非正数尺寸和不支持的模板名会被拒绝。

dev

proton_cli dev [options] [-- <application-arguments>...]

构建并运行原生后端。配置前端命令时,会启动该命令、等待服务就绪,并向应用提供开发 URL。后端修改需要重新构建、启动;前端刷新由前端工具负责。

选项默认值及含义
--config <path>项目配置,默认工作目录下的 proton.project.json
--package <selector>覆盖配置中的后端包;回退值为 app
--moon-target-dir <path>覆盖内部 Moon 调用的输出目录
--url <url>覆盖 frontend.dev_url
--command <command>覆盖 frontend.before_dev
--frontend-path <path>覆盖前端命令的工作目录
--timeout-ms <milliseconds>30000;等待前端就绪的超时时间
--ready-path <path>在此路径使用 HTTP 就绪探测;未指定时检查 TCP 连接
--no-frontend不启动前端命令,使用外部管理的服务
--setup、--no-setup自动安装缺失的运行时和 helper;默认关闭
-- <arguments>...传给应用的参数,不是 Moon 参数

由 CLI 管理的前端必须配置启动命令,且开发地址已被占用时会报错。HTTP 探测需要 curl。等待超时后不会启动应用。CLI 负责自己启动的前端进程,外部服务的生命周期独立。

build

proton_cli build [options] [-- <moon-build-arguments>...]

依次执行前端构建、验证前端输出、构建原生后端。前端命令失败时不会继续构建后端。结果是构建产物,不是完整分发包。

选项默认值及含义
--config <path>项目配置,默认工作目录下的 proton.project.json
--package <selector>覆盖配置中的后端包;回退值为 app
--moon-target-dir <path>覆盖内部 Moon 构建的输出目录
--no-frontend跳过前端构建命令,不会生成缺失的前端资源
-- <arguments>...传给 moon build,例如 -- --release

后端始终使用 native 目标;通过转发参数覆盖 --target 会被拒绝。

package

proton_cli package [options]

为当前主机平台构建并组装应用、前端资源、CEF 运行时、匹配的 helper 和声明的资源。必须有项目配置。本命令不接收尾随的 Moon 参数。

选项默认值及含义
--config <path>项目配置,默认 proton.project.json
--package <selector>覆盖后端包
--release使用 release 构建,默认 debug
--dry-run解析并打印计划,不执行完整构建、打包流程
--product-name <name>覆盖 package.product_name
--version <version>覆盖应用版本,不是选择 Proton 版本
--format <format>可重复;app、zip、dmg、nsis、appimage,受主机平台限制
--output <directory>覆盖输出目录
--icon <path>可重复;覆盖配置的图标
--url-scheme <scheme>可重复;覆盖配置的 URL scheme
--nsis-install-mode <mode>currentUser、perMachine、both;覆盖 Windows 配置
--macos-minimum-system-version <version>覆盖 package.platforms.macos.minimum_system_version
--sign签名应用,默认关闭
--notarizemacOS 签名、公证和 stapling;隐含 --sign
--updater-base-url <https-url>使用此产物地址生成更新元数据;要求同时提供发布时间和 revision
--updater-published-at <instant>YYYY-MM-DDTHH:MM:SSZ 格式的发布时间;要求提供 base URL
--updater-revision <integer>写入应用与更新元数据的单调递增发布序号

--format 等命令行列表覆盖对应配置列表。应用标识来自配置顶层的 identifier,package 没有 --identifier 选项。--release 不隐含签名;dry run 成功不代表产物创建或运行验证成功。平台格式、配置默认值和资源规则见打包。

签名环境

凭据通过环境配置,不是命令选项。

变量含义
PROTON_MACOS_SIGNING_IDENTITYmacOS 签名所需的身份
PROTON_MACOS_ENTITLEMENTS可选 entitlements 文件
PROTON_MACOS_NOTARY_PROFILEmacOS 公证凭据 profile;回退变量为 PROTON_NOTARY_PROFILE
PROTON_MACOS_BUILD_NUMBER可选 bundle version,包含一至三个数字分量
PROTON_MACOS_ALLOW_ADHOC1 允许用于本地诊断的 ad-hoc 签名
PROTON_WINDOWS_CERTIFICATEWindows 签名所需的证书路径
PROTON_WINDOWS_CERTIFICATE_PASSWORD证书密码,默认空字符串
PROTON_WINDOWS_TIMESTAMP_URL时间戳服务,默认 http://timestamp.digicert.com;none 禁用

doctor

proton_cli doctor [options]

检查环境和当前项目,不安装依赖或修复项目文件。诊断失败时返回失败状态。

选项含义
--verbose包含详细项目诊断
--json输出一个 JSON 文档,与 --quiet 冲突
--quiet只输出失败的诊断,与 --json 冲突
--output <path>将报告写入文件

cef setup

proton_cli cef setup

将当前 Proton 发行版指定的 CEF 运行时和 helper 安装到用户级共享存储,并输出运行时根目录。没有专属选项,不写入项目运行时选择文件,也不选择任意 CEF 版本。缺少安装产物时需要网络访问。

cef requirements

proton_cli cef requirements

以 JSON 输出当前发行版内嵌的 CEF 要求。不安装运行时,也不检查正在运行的应用浏览器。没有专属选项。

updater public-key

proton_cli updater public-key --modulus <hex> [--exponent <hex>]

校验受信任 RSA 公钥,输出包含位数的注释及 rsa-sha256:<modulus>:<exponent> 文本,可用于 App::update_channel 的 public_keys。

选项含义
--modulus <hex>必填 RSA 模数;接受 OpenSSL 输出的 Modulus= 前缀
--exponent <hex>公钥指数,默认 010001(65537)

参数会去除首尾空白,并将十六进制规范化为小写。校验使用运行时的公钥解析器,包括模数尺寸和指数约束。本命令不生成密钥对、不读取 PEM 文件、不签名产物,也不发布更新。

退出状态与更新检查

状态码含义
0命令成功
1执行失败,或诊断报告失败
2参数解析失败

除 version 外,命令会尝试检查 CLI 更新。PROTON_NO_UPDATE_CHECK=1 可关闭检查。更新服务不可用不会阻止命令执行。

打包参考

Proton CLI 将项目组装为分发产物,包含可执行文件、前端资源、CEF 运行时、匹配的 helper 和声明的资源。后端可执行文件本身不是完整分发包。打包面向当前宿主平台。

字段类型与默认值统一定义在 Configuration。

平台覆盖

各平台对象接受 formats、resources、sign。Windows 还接受 nsis_install_mode,macOS 还接受 minimum_system_version。平台格式列表替换共享列表,平台资源与共享资源合并。路径值以项目配置文件目录为基准。

平台格式默认值
macOS Apple Siliconapp、zip、dmgapp、zip
Windows x64app、zip、nsisapp、zip
Linux x64appimageappimage

图标与工具前提

产物图标和工具
macOS app / zipICNS 用于应用图标;本机 Apple 工具链用于构建与签名
macOS dmg在 app 基础上使用系统 hdiutil 创建磁盘映像
Windows app / zipICO 由 Windows SDK 的 rc.exe 编入 EXE;不会自动将 PNG 转 ICO
Windows nsis在 Windows 应用基础上还需 NSIS 的 makensis
Linux appimage必须提供 PNG;PATH 中必须有可执行的 appimagetool

默认 scaffold 不提供图标。Linux 缺少 PNG 会在调用 AppImage 工具之前失败。准备图标和工具的完整操作见 Todo 打包步骤。为不同平台提供对应格式的图标,不要只配置 macOS 和 Linux 图标却期待 Windows 自动转换。

NSIS 安装模式

值行为
currentUser默认;当前用户,不要求管理员安装;安装到 %LOCALAPPDATA% 下,使用当前用户注册
perMachine所有用户,需要提权;安装到 Program Files 下,使用机器级注册
both安装器提供范围选择;即使选择当前用户也可能提示提权

修改模式不是安装范围迁移。更新已有的机器级安装时,应保持其预期安装范围。

升级旧版 Proton 安装器: 旧版 Proton 始终生成机器级 NSIS 安装器。已采用该方式分发的应用,后续版本必须显式将 package.platforms.windows.nsis_install_mode 设为 perMachine。当前默认的 currentUser 面向另一种安装范围。同一范围内会复用已有安装路径;固定模式不会将安装从用户级迁移到机器级,反之亦然。

both 使用 NSIS MultiUser,并请求最高可用权限。因此,即使用户选择当前用户安装,也可能先看到 UAC 提示。--nsis-install-mode 覆盖项目配置,参见 CLI 命令参考。

macOS 最低版本

minimum_system_version 只写 macOS 的 LSMinimumSystemVersion 元数据,不能使较新 SDK/CEF 编译出的应用自动兼容旧系统。最低系统支持需要在目标系统上验证整个产物,而不只是编译通过。

签名与公证

--release 控制构建模式,不意味着签名。macOS 上,--sign 请求签名;--notarize 使用配置的凭据请求公证、装订和验证。身份与凭据环境变量见 CLI 命令参考。

未签名或本地签名的产物不代表受信任的发布者身份。凭据属于部署配置,不属于应用源码。

运行时资源行为

打包前端资源不依赖开发服务器。后端资源路径通过 resource_dir() 解析。安装资源可能只读,持久应用数据不应写入其中。

package.resources 提供共享的附加资源,平台资源会追加到共享列表。package.sign.binaries 及其平台覆盖指定额外参与签名的可执行文件。显式 CLI 选项优先于配置。字段类型和合并规则见 Configuration。

--dry-run 校验并展示打包计划,不证明实际打包、签名或产物运行成功。Todo 教程包含应用构建与打包练习。

打包准备命令

package.prepare 在前端和原生可执行文件构建完成后、资源组装前,从项目配置文件目录执行一次。它用于生成已声明的打包专用资源或额外二进制。命令以非零状态退出会中止打包。

proton_cli build 和 proton_cli package --dry-run 不执行此命令。独立的 proton_package 工具只打包已有可执行文件与提供的资源,不发现项目、不执行构建或准备命令,也不组装 CEF。参见独立打包器。

URL 协议与文档类型

package.url_schemes 和 package.document_types 描述操作系统的启动关联。具体效果取决于产物:

产物行为
macOS app将 URL 协议和文档类型写入应用元数据
Windows NSIS在所选安装范围注册 URL 协议,卸载时移除仍属于该应用的注册;尚未实现文档类型关联
Windows app / zip不安装操作系统关联
Linux AppImage在内置 desktop entry 中声明 URL 协议,仍需桌面集成将其注册到系统;尚未实现文档类型关联

声明关联并不实现应用侧处理。启动输入处理器的注册方法见应用生命周期。

诊断参考

诊断来自不同层次:项目与工具链验证、原生启动与生命周期、bridge 请求、前端渲染。前端预览成功不证明原生操作正常,构建成功也不证明打包产物运行正常。

诊断接口

接口范围
proton_cli doctor只读检查项目、工具链、运行时和 helper
proton_cli --version / moon version工具版本
WebContentsHandle.open_devtools()浏览器的 Chromium 检查器
App.run()类型化应用运行错误
App.run_or_abort()报告失败并中止
ClientFailure前端契约、bridge、通信及解码错误

WindowHandle.web_contents() 提供浏览器句柄,DevTools 属于该浏览器的生命周期。在窗口 ready 钩子中打开检查器时,应保留已有钩子的状态和清理逻辑。

CDP 端点发现

启动原生应用时设置 PROTON_REMOTE_DEBUGGING_PORT=0,让 Chromium 选择可用端口。从进程输出读取实际浏览器 WebSocket 端点:

DevTools listening on ws://127.0.0.1:<port>/devtools/browser/<id>

在该进程生命周期内使用它公布的端点;不要先预占再释放端口,也不要假定固定端口。仍可显式指定端口。这是 Chromium 的诊断端点,不是 Proton 的应用命令传输通道。

应用需要通过 App.debug() 启用调试;端口环境变量本身不会开启调试。 使用 native MoonBit 编写自动化程序时,可以参考独立的 proton_cdp 库。

日志

Proton 使用 tonyfettes/xlog。开发输出使用 stderr,打包应用使用平台日志目录。应用分类为 app.*,框架分类为 proton.*,应用设置的级别和分类过滤保持有效。

环境变量作用
MOON_XLOGxlog 过滤
PROTON_LOG_OUTPUT=stderr选择 stderr 输出,也适用于从终端启动的打包应用
PROTON_CEF_LOG临时启用独立的 CEF 内部诊断,默认关闭

文件输出依赖打包元数据。CEF 诊断不是应用日志接口。

找到实际日志

打包应用的默认文件名是 proton-<pid>.log,目录以应用 identifier(不是显示名称)区分:

平台默认目录
macOS~/Library/Logs/<identifier>/
Windows%LOCALAPPDATA%&lt;identifier>\Logs\
Linux$XDG_STATE_HOME/<identifier>/logs/,未设置时为 ~/.local/state/<identifier>/logs/

App.path(AppPathKind::Logs) 返回解析后的路径。通过 set_path 或 set_app_logs_path 自定义过路径时,应使用查询结果。PROTON_LOG_OUTPUT=stderr 可使从终端启动的打包程序输出到终端;未打包应用不能使用 file 模式。

按阶段定位

现象检查与下一步
CLI 找不到运行时比较 proton_cli --version 与应用 proton 依赖版本;运行 cef requirements 查看该 CLI 要求,再执行 cef setup;保留 setup 的完整错误
dev 等不到前端在 frontend.path 中单独执行 before_dev,检查 dev_url 的主机和端口;已有服务用 --no-frontend,不要同时启动第二个
普通浏览器里没有 bridge改为通过 proton_cli dev 启动原生应用;网页预览不注入宿主 bridge
permission_denied / unknown_op检查注册的命令描述符、capability 目标与当前窗口/页面;不要只检查是否添加模块依赖
找不到前端命令检查配置中的工具是否安装并位于 PATH;Warren 需要独立于 Proton CLI 安装
请求或响应解码失败对照命令描述符类型与实际 JSON 载荷,检查 ClientFailure 和后端日志
打包后资源 404检查产物内资源树与 HTML 相对 URL,确认 frontend.dist 与 package.resources;停止开发服务器后重试
关闭窗口后进程仍在先检查 KeepRunning 和被拒绝的 quit;查看最终退出/清理错误。记录关闭操作与进程状态,不把手动 kill 当作退出成功
appimage 打包失败先检查 PNG 文件及 appimagetool --version;区分图标校验失败与 create AppImage 工具错误

报告问题

报告应包含执行命令与完整错误、Proton 和 MoonBit 版本、操作系统及架构,并说明故障发生在开发模式还是打包应用中。附上 proton_cli doctor --json 输出、相关应用日志及最小复现。退出问题还应注明进程自行退出还是被手动终止。分享日志前检查其中是否包含业务数据或凭据。

项目配置

proton.project.json 包含应用身份及 CLI 构建/打包元数据。顶层是 JSON 对象,只接受以下四个字段;未知字段会被拒绝。窗口状态、命令注册和能力声明属于 MoonBit 应用构建器,不属于此配置文件。

默认文件名为 proton.project.json。若使用 moon.proton.json 等其他名称,需给 dev、build 或 package 显式传递 --config moon.proton.json;它不是自动发现的别名。

顶层字段

字段类型要求与含义
identifierstring必填,经过校验的应用标识
backendobject解码时可省略,提供 CLI 后端位置与入口
frontendobject可省略,前端命令及资源配置
packageobject可省略,分发元数据;基于元数据打包时需要

identifier 会去除首尾空白,必须包含至少两个非空的点分段。每段以 ASCII 字母或数字开头,仅允许 ASCII 字母、数字和连字符。

最小配置为:

{
  "identifier": "com.example.app",
  "backend": { "path": ".", "package": "app" }
}

后端

存在 backend 时,两个字段均为必填。

字段类型解释
pathstring相对于配置文件目录解析的目录
packagestring在该目录执行 Moon 时使用的包选择器

前端

解码时,所有前端字段均可省略。具体 CLI 命令会提出额外要求。

字段类型默认值与解释
pathstring省略时为配置文件目录;前端命令的工作目录
dev_urlstring无默认值;开发页面 URL
before_devstring无默认值;前端服务器命令
before_buildstring无默认值;前端构建命令
diststring无默认值;指定 frontend.path 时,相对于该目录解析的输出目录

dev 管理配置的开发 URL 时要求提供前端命令。--no-frontend 连接外部管理的服务器。build 先执行配置的构建命令,再验证前端输出并构建后端。CLI 负责启动服务器时,会拒绝已被占用的开发端点。

路径与资源

后端/前端路径、打包图标、资源和输出目录相对于配置文件目录解析。frontend.dist 相对于前端目录解析。绝对路径保持绝对路径。路径分隔符遵循宿主操作系统的规则。加载配置保留路径原文,不要求所引用的文件或目录已经存在;构建和打包操作检查各自实际使用的输入。

@proton.resource_dir() 是应用资源基准目录:开发时 CLI 提供项目根目录;打包后为应用资源目录;没有托管元数据的直接运行使用启动工作目录。安装资源不属于持久可写数据存储。

package

以下字段属于 proton.project.json 的 package 对象。

字段类型默认值与含义
product_namestring必填,显示名称
versionstring必填,应用版本,独立于 Proton 版本
formatsstring array省略时使用宿主平台默认格式
iconsstring array空;图标路径相对于配置文件目录
preparestring无;准备命令
resourcesstring array空;附加打包资源
sign.binariesstring array空;额外选择签名的二进制文件
url_schemesstring array空;应用注册的 URL scheme
document_typesobject array空;文档关联
outputstringdist,相对于配置文件目录
platformsobject可选的 macos、windows、linux 覆盖

文档类型包含必填的 name、extensions,role 默认为 Viewer。规范应用标识位于顶层 identifier,不是 package 字段。修改产品名称不会改变应用身份。

package.sign

字段类型默认值与含义
binariesstring array[];额外签名的二进制文件,路径相对于配置文件目录

省略 sign 时不添加额外二进制文件。此对象不会启用签名;启用签名使用 package --sign 或 --notarize。

package.document_types[]

字段类型默认值与含义
namestring必填,文档类型显示名称
extensionsstring array必填,非空的文件扩展名列表
rolestringViewer;传递给打包器的文档角色

package.platforms

字段类型默认值与含义
macosobject无;macOS 覆盖配置
windowsobject无;Windows 覆盖配置
linuxobject无;Linux 覆盖配置

各平台对象接受以下字段。未知字段会被拒绝,包括在非 Windows 对象中设置 nsis_install_mode。

字段类型默认值与含义
formatsstring array继承共享的 package.formats;显式列表替换共享列表
resourcesstring array[];追加到共享资源列表并去重
signobject无;只接受 binaries(string array,默认 []),追加到共享签名列表并去重
nsis_install_modestring仅 Windows;currentUser(默认)、perMachine 或 both
minimum_system_versionstring仅 macOS;格式为 major.minor[.patch]。设置 LSMinimumSystemVersion;未配置时省略该字段。不改变编译目标,也不验证二进制兼容性。

合并后的格式列表为空时采用宿主平台默认格式。CLI 选项覆盖解析后的配置。支持的格式、签名和安装模式见打包行为。

配置职责与目录布局

Proton 不要求使用 minimal 或 isomorphic 模板的目录布局。CLI 根据本文件定位后端、前端和资源;各路径的解析规则见本页“路径与资源”。

文件或入口职责
moon.mod模块名称和版本化依赖
moon.pkg包导入、目标及可执行入口声明
moon.work(使用多个模块时)将模块组织为工作区
proton.project.json应用身份、CLI 构建路径与打包元数据
MoonBit App 构建器窗口、命令、capability 和生命周期行为

模块依赖使模块可用,包导入选择当前包使用的 API。项目配置中的 backend.package 选择构建入口,不代替 Moon 的包声明。

构建输出

Moon 默认将构建产物写入 _build/。前端输出位置取决于前端构建工具,应与 frontend.dist 对应;frontend/dist 是 isomorphic 模板的选择,不是框架固定路径。打包器将分发产物写入 package.output,默认 dist。

这些目录保存生成产物,内容由源码、构建命令和项目配置决定。setup 安装的运行时与 helper 位于用户级共享存储,不属于应用源码目录。

完整配置示例

以下路径用于说明字段组织方式;项目必须实际提供引用的图标、资源和命令。

{
  "identifier": "com.example.notes",
  "backend": { "path": "backend", "package": "app" },
  "frontend": {
    "path": "frontend",
    "dev_url": "http://127.0.0.1:4300",
    "before_dev": "warren dev --browser-entry main --direct --port 4300",
    "before_build": "warren build --browser-entry main",
    "dist": "dist"
  },
  "package": {
    "product_name": "Notes",
    "version": "1.0.0",
    "formats": ["app", "zip"],
    "icons": ["icons/app.icns", "icons/app.ico", "icons/app.png"],
    "prepare": "node scripts/prepare.mjs",
    "resources": ["resources"],
    "sign": { "binaries": [] },
    "url_schemes": ["notes"],
    "document_types": [
      { "name": "Note", "extensions": ["note"], "role": "Editor" }
    ],
    "output": "dist",
    "platforms": {
      "macos": { "formats": ["app", "dmg"] },
      "windows": { "formats": ["nsis"], "nsis_install_mode": "currentUser" },
      "linux": { "formats": ["appimage"] }
    }
  }
}

运行时配置

先独立加载配置,再将它安装到应用:

let config = @proton.load_config()
let app = @proton.html("Hello", "<h1>Hello</h1>")..config(config)

if config.frontend is Some(frontend) {
  if frontend.dev_url is Some(url) {
    println(url)
  }
}

load_config() 读取并验证完整的类型化 AppConfig,失败时立即抛出 AppConfigError。开发环境读取 proton.project.json,打包环境读取 proton-package.json。需要指定路径时,使用 @proton.AppConfig::load(path); 也可以直接构造 AppConfig。

从配置值直接读取字段。嵌套章节是可选结构体:config.frontend 包含 dev_url、path、dist、before_dev 和 before_build。 JSON 中的 package 对应 config.package_config,backend.package 对应 backend 的 app_package 字段。

proton_cli dev 提供经过 --url、--command、--frontend-path 和 --package 覆盖的生效配置。打包也会嵌入生效配置,包括格式和平台选项的 命令行覆盖值。相对路径保持原样;读取字段不会解析路径或执行命令。

app..config(config) 安装独立副本,与 app..identifier(...) 互斥。 无效配置或与包内身份不一致的 identifier 会阻止启动。保留 config 即可查询 配置,应用不再提供配置 getter。app.name() 和 app.version() 读取安装的 快照,不再访问文件。

最小应用

本例直接使用 HTML 字符串启动原生窗口,展示应用入口最基本的三个部分:页面内容、应用标识和运行应用。

01_run · 关键文件:main.mbt

行为

单个 800 × 600 窗口显示“01 run”。构建器声明应用身份并启动应用。

async fn main {
  let html =
    #|<!doctype html>
    #|<html>
    #|  <head><meta charset="utf-8"><title>01 run</title></head>
    #|  <body><h1>01 run</h1></body>
    #|</html>
  @proton.html("01 run", html, width=800, height=600, debug=true)
  ..identifier("dev.proton.01-run")
  .run_or_abort()
}

实现说明

HTML 是直接传给应用构建器的 MoonBit 值,本页面无需前端服务器、资源查找或命令通信。显式 identifier 为直接运行仓库示例提供应用身份;scaffold 则通过 load_config() 读取身份。

适合复用为小型自包含页面;需要独立构建的 CSS/JavaScript 时应改用资源入口。debug=true 是示例选择,不是分发要求。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 01_run --target native

限制与使用边界

适合将运行时安装问题与前端工具问题分开排查。此示例没有命令交互或外部资源加载。

嵌入 HTML

本例将 HTML 保存在独立源文件中,构建时嵌入可执行文件并作为 MoonBit 字符串加载。页面内容可以单独维护,运行时不需要再查找这个 HTML 文件。

12_embed · 关键文件:main.mbt, hello.html, hello.mbt, moon.pkg

行为

窗口通过 html() 加载嵌入资源。包的 prebuild 规则从 hello.html 生成 hello.mbt。

async fn main {
  @proton.html("12 embed", resource, width=800, height=600, debug=true)
  ..identifier("dev.proton.12-embed")
  .run_or_abort()
}

实现说明

resource 由包中的 embed 预构建规则从 hello.html 生成。运行时接收的仍是与最小示例相同的字符串,不是在启动时读取磁盘上的 hello.html。

修改 hello.html 后重新构建,不修改生成的 hello.mbt。嵌入一个文档不会自动嵌入它引用的相对图片或脚本;一组资源应使用 asset 入口。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 12_embed --target native

限制与使用边界

修改 hello.html,而不是生成的 hello.mbt。嵌入 HTML 不会自动包含旁边的文件;独立 JS/CSS 见静态资源示例。

文件系统能力

本例通过文件系统扩展让页面请求宿主文件操作。后端声明允许访问的目录和操作,页面通过命令 bridge 使用这份授权。

18_extension_fs · 关键文件:main.mbt, fs.html

行为

页面提供读写和目录操作。原生入口用 PermissionRoot 和操作允许列表注册 fs 能力。

async fn main {
  @proton.html("18 extension fs", resource, width=960, height=720, debug=true)
  ..capability(
    @fs.capability([
      @fs.PermissionRoot(".", [
        "read_file", "write_file", "exists", "kind", "size", "readdir", "mkdir",
        "remove", "rmdir", "rename", "realpath",
      ]),
    ]),
  )
  ..identifier("dev.proton.18-extension-fs")
  .run_or_abort()
}

实现说明

后端安装文件系统扩展,同时选择目录和允许的操作。renderer 提供路径及操作,但不能自行扩大授权根目录。文件不存在和操作未授权是不同失败情况。

此例为了演示允许写入与删除,迁移时应缩小操作列表并选定数据目录。HTML 与 JavaScript 展示直接扩展 bridge,MoonBit 前端也可使用类型化描述符。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 18_extension_fs --target native

限制与使用边界

示例允许操作工作目录内的文件,包括写入和删除。体验时使用临时目录;应用只应声明真正需要的路径和操作。

类型化事件

本例在异步命令执行期间发送进度通知,展示前端如何通过事件订阅观察后端工作,以及进度事件与最终命令响应的区别。

40_event_broadcast · 关键文件:main.mbt, app.html

行为

启动计数器后产生 tick 事件和 done 事件,命令还会返回汇总结果。run_id 用于区分不同运行。

实现说明

以下为入口中的节选,完整上下文见本页源码链接。

async fn run_ticker(
  context : @proton.CommandContext,
  payload : StartPayload,
) -> TickerResult {
  let count = clamp(payload.count, 1, 20)
  let interval_ms = clamp(payload.interval_ms, 100, 2000)
  let ticks : Array[TickEvent] = []
  for index in 1..<=count {
    @async.sleep(interval_ms)
    let tick = TickEvent::{
      run_id: payload.run_id,
      index,
      total: count,
      remaining: count - index,
    }
    ticks.push(tick)
    context.emit(tick_event, tick)
  }
  let done = DoneEvent::{ run_id: payload.run_id, total: count, }
  context.emit(done_event, done)
  TickerResult::{ ticks, done, }
}

类型化扩展把 start 绑定到 run_ticker。处理器在 tick 之间异步等待,发送 tick/done 通知,最后返回 TickerResult。进度事件与命令结果职责不同,显示一次 tick 不代表命令已经完成。

次数与间隔由后端限制。UI 应使用请求标识区分重叠任务,随所属对象关闭订阅,并处理请求取消。目录虽叫 broadcast,这不是持久广播日志。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 40_event_broadcast --target native

限制与使用边界

事件与命令响应职责不同。启动运行前注册监听器;事件不是持久历史记录或确认消息。

多窗口命令

本例让同一应用中的两个窗口调用命令,展示窗口声明、渲染器授权和调用方身份如何配合。增加一个窗口并不意味着该页面自动获得现有操作的访问权限。

45_bridge_multi_window · 关键文件:main.mbt

行为

identify 命令返回传入标签与 CommandContext.window_id()。不同窗口的请求保留各自来源身份。

实现说明

以下为入口中的节选,完整上下文见本页源码链接。

async fn main {
  @proton.html(
    "Bridge Multi A",
    page_html("A"),
    width=520,
    height=420,
    debug=true,
  )
  ..add_window(
    "secondary",
    @proton.WindowConfig(
      "Bridge Multi B",
      @proton.AppEntry::Html(page_html("B")),
      width=520,
      height=420,
    ),
    open_on_start=false,
  )
  ..capability(@proton_extension.capability(multi_window_extension()), targets=[
    @proton.RendererTarget::entry(),
    @proton.RendererTarget::entry(window="secondary"),
  ])
  ..app_lifecycle(
    on_start=async fn(context) { ignore(context.windows().open("secondary")) },
    on_shutdown=fn(_state) {  },
  )
  ..identifier("dev.proton.45-bridge-multi-window")
  .run_or_abort()
}

同一扩展实现服务两个独立授权的 renderer 目标。secondary 声明使用 open_on_start=false,由应用启动钩子显式打开。处理器返回 context.window_id(),说明调用方身份来自宿主上下文,而非页面传来的 label。

可以复用业务处理器,但必须显式列出目标。每窗口 UI 状态留在前端,共享状态由宿主管理。延迟调用可以重叠,不能按当前焦点窗口判断响应归属。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 45_bridge_multi_window --target native

限制与使用边界

可选 delay_ms 便于观察并发请求。逻辑窗口名标识应用窗口,不是可以无限复用的原生句柄。

HTML 与静态资源

本例加载 HTML 入口及独立的 JavaScript、CSS 和 worker 文件,展示页面从应用资源加载时如何组织文件和相对路径。

46_asset_sidecar_resources · 关键文件:main.mbt, app/app.html, app/app.js, app/app.css, app/worker.js

行为

asset() 加载 app.html 及其相邻资源。示例还提供第二个页面以观察导航。

async fn main {
  let app = @proton.asset(
    "Asset Sidecar Resources",
    "46_asset_sidecar_resources/app/app.html",
    width=760,
    height=500,
    debug=true,
  )..capability(@proton_extension.capability(extension()))
  app..identifier("dev.proton.46-asset-sidecar-resources").run_or_abort()
}

实现说明

asset 入口指定资源树中的 HTML 文档。相邻脚本、样式和 worker 源码仍是独立文件,因此打包后必须保持相对 URL 有效。扩展注册与资源入口选择是两件独立的事。

迁移时保留资源目录布局,并配置打包包含它。关闭开发服务器后验证产物;开发 URL 正常不证明附属资源已经打包。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 46_asset_sidecar_resources --target native

限制与使用边界

从 examples 目录运行,使相对资源路径正确解析。分发时必须包含完整资源树,不能只带 HTML。

原生应用菜单

本例创建原生应用菜单,并在窗口运行期间更新菜单项状态。菜单操作通过存活的窗口句柄执行,窗口关闭时清除该引用。

49_app_menu · 关键文件:main.mbt

行为

菜单操作更新 enabled、visible、checked 状态。原生角色与应用命令在 MoonBit 中声明。

实现说明

以下为入口中的节选,完整上下文见本页源码链接。

async fn main {
  @proton.html(
    "App Menu State Review",
    html(),
    width=900,
    height=640,
    debug=true,
  )
  ..menu(menu_bar())
  ..identifier("dev.proton.49-app-menu")
  ..commands(register_commands)
  ..window_lifecycle(
    on_ready=context => {
      let window = context.handle()
      window_slot.val = Some(window)
      window
    },
    on_close=fn(_window) { window_slot.val = None },
  )
  .run_or_abort()
}

菜单是构建器配置的原生 UI。命令处理器通过有效窗口句柄修改或查询状态。ready 钩子保存句柄,close 钩子清空它,避免后续命令继续使用已关闭实例。

稳定的菜单命令 id 应与翻译标签分离。原生 role 项交给平台处理标准行为,应用命令项由应用处理。多窗口应用应将示例的单一 window slot 改为明确的每窗口所有权。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 49_app_menu --target native

限制与使用边界

菜单位置与角色支持取决于桌面平台。菜单事件可能没有聚焦窗口,处理器需要考虑这种情况。

嵌入浏览器视图

本例在主窗口侧栏旁放置一个子浏览器,展示主页面与可独立导航的浏览器视图之间的关系,以及如何显式设置子视图的边界。

53_view_minimal · 关键文件:main.mbt

行为

WindowConfig.views 在 x=288 处添加 832 × 720 子浏览器,独立于宿主 HTML 加载 example.com。

实现说明

以下为入口中的节选,完整上下文见本页源码链接。

async fn main {
  @proton.App(
    @proton.WindowConfig(
      "Minimal View",
      @proton.AppEntry::Html(sidebar),
      width=1120,
      height=720,
      views=[
        (
          "browser",
          @proton.view("https://example.com/", width=832, height=720, x=288),
        ),
      ],
    ),
  )
  ..debug(enabled=true)
  ..identifier("dev.proton.53-view-minimal")
  .run_or_abort()
}

主浏览器渲染侧栏,WindowConfig.views 在同一个原生窗口中创建第二个浏览器。x=288 留出侧栏宽度,子视图独立加载 example.com,适合宿主外壳内嵌页面,而非另开顶层窗口。

示例故意使用固定几何尺寸。实际布局应在 resize 后重新计算子视图边界,并决定如何处理远程导航。对本地主页面授权不表示远程子页面也应拥有相同能力。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 53_view_minimal --target native

限制与使用边界

远程页面需要联网。最小示例使用固定边界,响应式布局需由应用处理。父窗口关闭会结束子视图生命周期。

应用国际化

本例在启动时设置应用语言,并通过命令、Chromium 和原生菜单观察这一设置。它展示框架语言配置;应用自身的文本仍需由应用提供翻译。

56_i18n · 关键文件:main.mbt, app.html

行为

应用选择 zh-CN,同时显示后端语言偏好、navigator.language 和 navigator.languages。

实现说明

以下为入口中的节选,完整上下文见本页源码链接。

async fn main {
  let locale = @proton.Locale::parse("zh-CN") catch {
    error => abort(error.message())
  }
  @proton.html("Proton 国际化", resource, width=860, height=600, debug=true)
  ..locale(locale)
  ..capability(@proton_extension.capability(i18n_extension()))
  ..identifier("dev.proton.56-i18n")
  .run_or_abort()
}

应用显式解析 zh-CN 并传给 locale()。扩展从命令上下文读取解析后的语言偏好,Chromium 与原生框架标签使用相同的启动选择。Windows 有独立入口变体,不创建其他宿主使用的菜单。

这选择框架/浏览器语言,不会翻译任意应用文本;应用字符串需要自己的 catalog。此例配置启动语言,不实现运行中切换语言。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 56_i18n --target native

限制与使用边界

Proton 传递语言环境,不翻译业务文案。此示例在 Windows 上省略原生菜单,不能由其他平台的表现推断其菜单一致性。

后台驻留

本例将后台驻留与单实例激活结合起来。最后一个窗口关闭后应用继续运行,后续启动可以联系已有实例,而不是再创建一个运行时。

57_background_residency · 关键文件:main.mbt

行为

KeepRunning 保留进程。再次启动发送 Reopen,处理器仅在 main 不存在时重建窗口。

实现说明

以下为入口中的节选,完整上下文见本页源码链接。

async fn main {
  @proton.html("Background Residency", page, width=760, height=480)
  ..identifier("com.example.proton.background-residency")
  ..single_instance()
  ..last_window_closed_policy(@proton.LastWindowClosedPolicy::KeepRunning)
  ..on_launch_input(async fn(context, input) noraise {
    if input is Reopen && context.windows().find("main") is None {
      ignore(context.windows().open("main")) catch {
        error => println("failed to reopen main window: " + error.to_string())
      }
    }
  })
  .run_or_abort()
}

KeepRunning 修改最后窗口关闭后的行为,single_instance 将后续启动转发给现有进程。收到 Reopen 后先检查 main 是否存在,不存在时才创建新实例。关闭窗口与退出应用是两个刻意区分的操作。

真实后台应用需要可访问的显式 Quit 入口,例如托盘命令。停止开发进程属于强制终止,不能证明正常生命周期清理。Reopen 创建新窗口实例,不应保留之前的句柄。

运行

完成源码检出与运行时安装后,在仓库根目录执行:

moon -C examples run 57_background_residency --target native

限制与使用边界

关闭窗口有意不退出进程。结束应用需使用平台 Quit 操作或停止开发命令,这与默认的最后窗口关闭策略不同。

Release Notes

0.4.0

0.4.0 统一了窗口与浏览器 API,让应用与 CLI 共用类型化配置,并将更新安装纳入应用退出流程。本版包含不兼容的 API 变更。

破坏性 API 变更与迁移

范围0.3.40.4.0
窗口配置主窗口独立 setter,附加窗口使用 add_window(id, title, entry, ...)App(config)、main_window(config)、add_window(id, config, ...) 共用 WindowConfig
初始子视图App.with_view(...)主窗口和附加窗口都使用 WindowConfig.views
浏览器操作window.browser(),以及 ViewHandle 上的页面方法window.web_contents() 和 view.web_contents(),统一返回 WebContentsHandle
浏览器事件BrowserEvent、ViewEvent 及独立订阅WebContentsEvent 与 on_web_contents_event;用 window_id() 和可选的 view_id() 区分实例
会话依附浏览器的句柄、SessionHandle.window_id()、WindowSessionError应用级句柄、context.session()、SessionError;移除 window_id()
PDF请求编号与 PdfPrinted / PdfPrintResult异步 web_contents.print_to_pdf(path, options?) -> Unit
配置app.load_config()、ProjectConfig、AppMetadataErrorapp.config(@proton.load_config())、类型化 AppConfig、AppConfigError
更新update.install(); update.restart()update.download(); context.quit_and_install()
  • 窗口配置: 将 title、size、theme、titlebar_style、traffic_light_position 和 entry_* builder 设置迁入 WindowConfig。保留 html、url、file、asset 快捷入口;运行中的原生窗口控制仍属于 WindowHandle。窗口注册复制初始 views 数组,附加窗口重新打开时重新创建声明的子视图。#417
  • Web contents: WebContentsHandle 从需要拆分的枚举变为统一的不透明句柄,导航、下载、权限回调也接收该句柄。窗口缩放和子页面操作迁移到它;布局、显示、层级和移除仍属于视图。子页面获得打印、下载和会话访问,但不因此获得应用命令桥接。请求完成结果保持与来源页面对应,macOS 主页面标题更新也能正确送达。#418
  • 会话生命周期: Cookie、缓存、认证、证书例外和连接操作统一使用应用的共享 request context。窗口创建前、页面关闭后以及 KeepRunning 无窗口期间均可使用。并发 Cookie 查询相互独立;运行时退出唤醒待完成查询,并使保留句柄失效。错误处理改为 SessionError。仍是共享 profile,不是每窗口独立会话。#419
  • PDF 完成语义: 在异步上下文直接调用 print_to_pdf 并处理结果,删除请求编号和事件关联。页面关闭、renderer 终止或应用关闭都会结束待完成等待。取消只停止等待,不取消原生打印任务,因此仍可能生成文件。#433
  • 类型化配置: @proton.load_config() 一次读取完整配置,可直接访问 config.frontend.dev_url 等字段。App.config(config) 保存独立快照,name() / version() 不再重新打开文件。CLI 将开发覆盖参数写入启动快照。打包后的 proton-package.json 改为包含嵌套 package 的完整配置结构,需要一起重建应用与打包元数据。Rabbita 及模板默认值升级到 0.16.4,移除已弃用的 moonback 依赖。#435
  • 更新生命周期: 下载只校验并保留产物,不替换文件;安装须经过可取消的退出流程,并在运行时清理成功后执行。拒绝退出保留下载,普通退出丢弃下载,强制退出覆盖安装请求。成功交接后终止旧进程,包括退出码为零的情况。quit_and_install() 返回表示请求已接受,不表示新版已启动。扩展响应改名为 UpdateDownloaded,移除旧 changed 字段。#436

修复

  • 启动或运行期间取消执行 App.run() 的任务,现在也会清理原生窗口、浏览器和 helper;取消仍向调用方传播。#414
  • Linux 和 macOS 打包正确复制路径以 - 开头的文件及目录,并保留执行权限。#415
  • 更新安装在替换应用文件期间保留单实例独占权。Windows 自动更新等待原进程退出,不再强制终止;macOS 新版本更换可执行文件名后仍可重启。配置更新渠道的 macOS/Linux 应用会拒绝被替换的旧可执行文件延迟启动。#436

升级说明

CLI 与应用的 proton_* 依赖应使用相同版本,再执行 proton_cli cef setup 安装匹配的 helper。安装新版 CLI 不会迁移现有应用代码。重新打包应用以生成新版配置格式。

Rabbita 升级至 0.16.4;Warren 仍为 0.3.3,async 仍为 0.22.4,CEF 版本不变。参见应用 API、配置参考和完整变更。

0.3.4 — 2026-09-28

  • **生命周期:**新增应用退出拦截、强制退出、应用级窗口/浏览器/会话事件及焦点和显隐控制。修复 will_quit 中强制退出、exit(0)、子视图 renderer 通知、页面恢复与关闭记录回收。
  • **通信:**保留命令错误码,区分业务命令失败与运行时失败,移除全局待响应请求数量上限。扩展操作清单从绑定生成。
  • **浏览器 API:**增加导航历史和插入 CSS 控制;任务指标明确区分 CEF task 和进程。
  • **平台与打包:**修复 Windows 桌面媒体授权及子视图布局、macOS 查询的 autorelease pool 和错误弹窗布局;完善打包锁,提供显式最低 macOS 版本元数据,保留 AppImage 中 helper 的可执行权限。
  • **CLI:**new 不再对已经显式提供的选项重复询问。async 升级至 0.22.4。

应用升级

CLI 和应用的 proton_* 模块统一使用 0.3.4,并执行 proton_cli cef setup 安装匹配的 helper。重新安装 CLI 不会重写已有项目。

曾短暂引入的 app_metrics 改为 task_metrics 与 AppTaskMetric:任务身份不是进程身份,不同任务的 process_cpu_percent / process_memory_bytes 可能重复,不能直接相加作为应用总用量。参见 PR #412。PR #408删除了未使用的旧命令配置 API;应用命令继续通过绑定注册。

0.3.3 — 2026-09-21

  • **视图生命周期:**移除或显式关闭子视图后,保留投递最终关闭事件所需的实例身份。覆盖浏览器创建前关闭、替换后的旧事件等情况。PR #378
  • **依赖更新:**async 0.22.1、x 0.5.5、lexer/parser/moon_config 0.4.0、Rabbita 0.16.2、Warren 0.3.3。模板默认版本同步更新,取消处理与受保护的清理适配 async 新语义。PR #379

应用升级

应用的 proton_* 依赖和 CLI 统一使用 0.3.3。已有项目保留原配置和源码,安装新版 CLI 不会重写它们。使用 isomorphic 前端时安装 Warren 0.3.3,构建前运行 proton_cli cef setup 安装匹配的 helper。

直接使用 async 的应用需要关注 0.22 的取消语义:取消不再作为普通错误被捕获。等待已取消任务的 wait() 可以报告 TaskCancelled;子进程关闭则可以在完成回收后返回退出结果。

更早的发布历史

更早的变更见 0.3.2 变更和仓库历史。