☰
tinyjs 原生菜单与对话框实战:菜单栏、右键菜单、文件选择与系统弹窗一篇搞定
2026/10/16 20:40:04 网站建设 项目流程

【免费下载链接】tinyjsapp

Build native apps in JS. Backend, frontend. ~5Mb.

项目地址:https://gitcode.com/gh_mirrors/ti/tinyjsapp
点击查看免费下载

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…)带着浓浓的浏览器味。两步换成你自己的原生菜单:

  1. 在 tinyjs.json 中设置"contextMenu": false,彻底隐藏 WebKit 默认菜单;
  2. 用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.

项目地址:https://gitcode.com/gh_mirrors/ti/tinyjsapp
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询