【免费下载链接】tinyjsapp
Build native apps in JS. Backend, frontend. ~5Mb.
tinyjs 用不到 6MB 的运行时,把原生菜单与系统对话框完整带进 JavaScript 桌面应用:菜单栏、右键菜单、文件选择、alert/confirm 系统弹窗,全部由操作系统原生渲染,无需 Electron、Node 或捆绑 Chromium。本文从「跑起来一个应用」讲起,带你用tiny.menu与tiny.dialog四组 API,一次搞定桌面应用最常用的原生交互。
一、为什么用原生菜单和系统弹窗
用网页写桌面应用时,有两件事 HTML 天生做不好:
- 菜单栏:浏览器不会给你一条系统级菜单栏,自己用 CSS 画又容易显得「假」;
- 文件对话框:
<input type="file">拿不到真实路径,跨平台体验也不一致。
tinyjs 的做法是让前端的tiny全局对象(自动注入,无需<script>引入)把请求交给后端与原生 launcher:macOS 上跑 NSOpenPanel / NSAlert,Windows 上跑公共项对话框 + MessageBox,Linux 上跑 GTK 选择器与消息框。你在页面上写的只是一行 JS,用户看到的却是「长得像系统的一部分」的菜单和弹窗。
二、一分钟跑起来:tinyjs 应用基础 🚀
官方提供的一键安装脚本即可完成安装(详见 README.md 的 Install 一节),然后用脚手架命令创建并运行一个项目:
tinyjs new myapp cd myapp tinyjs dev窗口打开后,项目结构长这样:
myapp/ tinyjs.json # 应用配置 src/backend/main.js # 后端(有完整系统权限) src/frontend/ # 前端(index.html + js/css)前端页面里可以直接使用tiny.*,完整的 TypeScript 定义随框架附带,在 template/types/tiny.d.ts 中可以看到每个菜单项、对话框参数的精确签名。
三、菜单栏:tiny.menu.set 快速定义完整菜单
一个数组描述整条菜单
tiny.menu.set接收「菜单 → 菜单项」的数组,支持分隔线、勾选态、禁用态、快捷键和子菜单:
tiny.menu.set([ { title: '文件', items: [ { id: 'open', label: '打开…', key: 'o' }, // key 对应 ⌘/Ctrl + 按键 { id: 'save', label: '保存…', key: 's' }, { separator: true }, { id: 'quit', label: '退出', key: 'q' }, ]}, { title: '工具', items: [ { id: 'mute', label: '静音', checked: true }, // ✓ 勾选态 { id: 'later', label: '稍后', enabled: false }, // 灰色禁用 { id: 'more', label: '更多', submenu: [ // 嵌套子菜单 { id: 'a', label: '子项' }, ]}, ]}, ]);两个小知识点:
- 每个菜单项建议给一个唯一
id,后续事件回调和更新都靠它; tiny.menu.set设置的是应用菜单——之后打开的所有窗口都会显示它,tiny.menu.update一次修补所有窗口的副本。
处理点击事件与动态更新
tiny.menu.on((id) => { if (id === 'open') openDoc(); if (id === 'mute') toggleMute(); }); // 就地修补,无需重新声明整条菜单 tiny.menu.update('mute', { checked: false, label: '取消静音' }); // 读取当前状态 const st = await tiny.menu.get('mute'); // { exists, label, checked, enabled }如果希望在后端处理点击(比如需要读写文件),导出onMenu(id, app)即可,对应app.setMenu/app.updateMenuItem等同名方法。
标准菜单角色:Edit、应用菜单与帮助
不同系统对「标准菜单」的约定不同,tinyjs 用role字段帮你各归其位:
| role | 作用 |
|---|---|
'edit' | macOS 的标准 Edit 菜单(撤销/剪切/复制/粘贴),默认占首位,可手动放到任意位置 |
'app' | 仅 macOS:把项放进应用菜单(About 与 Quit 之间,Settings… 的规范位置) |
'help' | 帮助菜单,macOS 上永远排在最末并自带搜索框 |
tiny.menu.set([ { title: '文件', items: [{ id: 'open', label: '打开…', key: 'o' }] }, { role: 'edit' }, // Undo / Cut / Copy / Paste / Select All { title: '视图', items: [{ id: 'full', label: '全屏', key: 'f' }] }, ]);💡 想让三个平台的 Edit 菜单完全一致,可以自己声明标准项:{ role: 'edit', items: [{ role: 'standard' }, { separator: true }, { id: 'find', label: '查找…', key: 'f' }] },Windows/Linux 会显示快捷键但不会抢占 webview 自身的文本输入快捷键。
四、右键菜单:tiny.menu.setContext 自定义 Context Menu
webview 默认的右键菜单(Reload、Inspect Element…)带着浓浓的浏览器味。两步换成你自己的原生菜单:
- 在 tinyjs.json 中设置
"contextMenu": false,彻底隐藏 WebKit 默认菜单; - 用
setContext声明自定义项,格式与菜单栏完全相同:
tiny.menu.setContext([ { id: 'copy-path', label: '复制路径' }, { id: 'rename', label: '重命名…' }, { separator: true }, { id: 'del', label: '删除' }, ]); tiny.menu.onContext((id) => { if (id === 'del') trashIt(); });- 传
null可以恢复 webview 默认右键菜单; - 标准角色(
{ role: 'copy' }等)同样可用;托盘菜单与右键菜单共用同一套菜单项结构; - 后端处理则导出
onContextMenu(id, app)。
五、文件选择:tinyjs 原生文件对话框一步到位
四个方法覆盖日常 90% 的文件操作,返回值永远是真实文件系统路径(用户取消则为null):
| API | 返回 |
|---|---|
tiny.dialog.openFile() | 单个文件路径 |
tiny.dialog.openFiles() | 路径数组 |
tiny.dialog.pickFolder() | 文件夹路径 |
tiny.dialog.saveFile() | 保存目标路径 |
用types按扩展名过滤(不带点),用name预填保存文件名:
// 只能选 md / markdown / txt const file = await tiny.dialog.openFile({ types: ['md', 'markdown', 'txt'] }); const dir = await tiny.dialog.pickFolder(); // 预填 report.docx,三个平台行为一致 const out = await tiny.dialog.saveFile({ types: ['docx'], name: 'report' });不再需要<input type="file">的各种兼容技巧,也不再需要「把路径偷偷写进 input 再读出来」的 hack。
六、系统弹窗:alert / confirm / prompt 三件套
浏览器里的confirm()在 webview 中体验糟糕,tinyjs 提供原生系统弹窗(应用级模态,三大平台通用):
// 纯提示 await tiny.dialog.alert('导出完成', '共 42 页,已保存到桌面'); // 是 / 否 确认 → true | false const ok = await tiny.dialog.confirm('删除全部笔记?', { ok: '删除', cancel: '保留', }); // 输入框 → string | null(取消为 null) const name = await tiny.dialog.prompt('你的名字?', { default: 'world' });实现层面,这些弹窗由 launcher 在 UI 线程直接执行(macOS 的 NSAlert、Windows 的 MessageBox、Linux 的 GTK 消息框),并通过webview_return直接回应页面的 Promise——没有消息绕路,体验就是系统原生弹框。
七、进阶:托盘菜单与多窗口菜单
菜单栏应用(tray app)是「系统托盘 + 下拉菜单」的组合,菜单项结构与tiny.menu完全一致:
tiny.tray.set({ icon: 'sf:cup.and.saucer.fill', // 或 png 路径 menu: [ { id: 'show', label: '显示窗口' }, { separator: true }, { id: 'quit', label: '退出' }, ], }); tiny.tray.on((id) => { /* 菜单点击 */ }); tiny.tray.onClick(() => toggle()); // 无 menu 时的左键点击搭配"activation": "accessory"(无 Dock 图标)或"mainWindow": false(彻底无主窗口),就能做出常驻菜单栏的小工具,完整可运行的示例见 test/windowless-tray/。
多窗口场景下,tiny.win.menu.set / update / reset可以给某一个窗口单独换一条菜单栏;想彻底隐藏菜单栏则用setChrome({ menu: false })(快捷键依然有效)。
八、常见坑与排查清单 ⚠️
types写扩展名时不带点:['md', 'txt'],写成['.md']过滤会失效;- 弹窗取消返回
null:openFile等四个文件对话框都要显式判断取消,别假设一定有值; { role: 'app' }只在 macOS 有效:Windows/Linux 没有应用菜单,这些项会被丢弃——需要「设置」入口时在文件菜单里也声明一份;- 快捷键抢占:如果你的菜单项声明了
key: 'c',它会在整个应用(包括文本框)抢走 ⌘C,请慎重; - 右键菜单优先级:
setContext永远优先于 tinyjs.json 里的"contextMenu": false配置; - Linux 托盘:底层是 AppIndicator/StatusNotifier,纯图标点击由合成菜单项模拟,行为与 macOS 略有差异。
九、延伸阅读 📚
- README.md:安装、项目结构与完整 API 总览(Menus、Dialogs 章节)
- skill/references/api.md:菜单与对话框 API 逐参数详解
- skill/references/recipes.md:托盘/菜单栏应用等可抄配方
- template/types/tiny.d.ts:
tiny.menu/tiny.dialog的 TypeScript 类型定义 - test/windowless-tray/:无主窗口托盘应用完整示例
【免费下载链接】tinyjsapp
Build native apps in JS. Backend, frontend. ~5Mb.
相关推荐
Godot 4 PopupMenu 实战指南:从工具栏下拉菜单到右键上下文菜单与原生菜单集成
Godot 4 PopupMenu 实战指南:从工具栏下拉菜单到右键上下文菜单与原生菜单集成 PopupMenu 是 Godot 4 中用于展示选项列表的模态窗
文档教程NodeGui QMenu 开发指南:菜单栏、右键菜单与弹出菜单的构建及事件处理
NodeGui QMenu 开发指南:菜单栏、右键菜单与弹出菜单的构建及事件处理 QMenu 是 NodeGui(基于 Node.js + Qt 构建跨平台原生
桌面应用跨平台LobeHub Desktop 菜单体系实战指南:App 菜单、右键菜单与托盘菜单的配置原理
LobeHub Desktop 菜单体系实战指南:App 菜单、右键菜单与托盘菜单的配置原理 本篇基于 LobeHub 仓库中的 Desktop 菜单配置指南
人工智能大模型AI AgentAI 应用多智能体前端后端本地部署工具调用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考