资讯详情

gpui-shell 官方示例全解析:从 Todo 应用到原生动效,掌握完整脚本运行时

📅 2026/9/15 11:44:25 | 华诺云谱 👁 阅读
gpui-shell 官方示例全解析:从 Todo 应用到原生动效,掌握完整脚本运行时
gpui-shell 官方示例全解析从 Todo 应用到原生动效掌握完整脚本运行时【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit本篇指南以 gpui-kit 仓库website/shell/examples.md为核心逐一拆解随仓库发布的四个示例——独立 Todo 应用、可停靠工作区、Gallery 中的行情看板以及原生动效演示。读完你可以掌握如何用 JavaScript 编写一个完整、带类型检查、支持持久化的 GPUI 应用如何让脚本层与 Rust 宿主共享同一份数据以及如何让动画帧完全脱离 JavaScript 在 GPUI 侧本地采样。示例总览一条覆盖全部脚本面仓库自带的示例遵循“一个示例只讲一件事”的原则四个示例拼起来正好覆盖了脚本运行时的全部表面示例运行形态演示内容Todo list待办列表独立应用完整脚本面保留输入、对话框、Toast、受限存储、资源、类型Workspace工作区独立应用可停靠布局重启后依旧存活的面板全部界面装饰由脚本绘制Quote board行情看板Gallery 内的一个面板宿主侧HostModule 注册、一个实体被两种语言读取、实时开销计数Native motion原生动效独立的 Gallery 脚本视图像素级目标过渡与弹簧由 GPUI 保留并采样文档还特别指出如果你想看“一个产品级应用”的完整形态——OAuth、WebSocket 实时行情、虚拟化自选列表、价格图的保留嵌套视图、自带 Rust 宿主二进制——可以参考longbridge/longbridge-lite它是一个数千行 JavaScript 的只读桌面客户端也是目前针对该运行时编写的最大的项目。完整应用The todo list运行方式在仓库根目录cargo run -p gpui-shell -- examples/js_todolistexamples/js_todolist/的定位是“锻炼整个运行时”而非“最小化”——gpui-shell里任何一环坏了这里最先暴露出来。它的文件构成如下main.js 视图状态、过滤、全部事件处理器 ui.js 表现层以函数形式导出 storage.js 持久化以及未获授权时的处理 confirm.js 确认对话框一个独立的视图 icons/ 四个 SVG按应用根目录解析 gpui-kit.d.ts 生成产物jsconfig.json 和 types.d.ts 负责接线类型其中有四个值得抄走的做法。ui.js 是一个由函数构成的组件库ui.js导出了label、muted、title、button、iconButton、checkbox、field、row、surface、rule、emptyState等一系列函数让main.js读起来就像在使用组件库export const label (value, cx) div().text_size(12).line_height(1).text_color(cx.theme().colors.foreground).child(value); export const surface (cx) v_flex().flex_1().bg(cx.theme().colors.surface).border(1).border_color(cx.theme().colors.border).overflow_hidden();main.js把当前的cx传给这些辅助函数它们直接通过cx.theme()读取语义化 token。这在性能上毫无代价因为“一个全新的描述description正是函数调用产生的东西”参见 Elements 文档。同时这正是对“基础层不提供任何带样式的控件”这一事实的回应把带样式的层写一次、写在自己的文件里之后就不用反复复制了。从源码可以看到ui.js的完整约定ui.js间距遵循语义刻度SPACE { xxs: 2, xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32 }字号保持在 12/13/16/20视觉语言对齐crates/base/examples/showcase中性灰阶、1px 边框、方角、小字号、28px 高控件但 showcase 只能写死颜色base 不带调色板这里改读 shell 的语义 token因此同一份代码能跟随主题按钮只有两个变体实心 primary 与描边 secondary外加 ghost 和 danger 两个安静变体共四种variant图标按钮必须携带无障碍标签accessibility_label(description)因为单独的图标对屏幕阅读器毫无信息量输入框由运行时框定为一个点击聚焦的居中行高度、内边距与颜色仍归应用自己控制。存储吸收拒绝而不是检查权限store这里的localStorage在宿主未授权时会直接抛错这是关于宿主的事实而不是应用的错误。所以storage.js在边界处吸收异常export function load() { try { const saved localStorage.getItem(KEY); if (saved null) return []; const items JSON.parse(saved); return Array.isArray(items) ? /** type {Todo[]} */ (/** type {unknown} */ (items)) : []; } catch (/** type {any} */ error) { console.warn(todolist: storage unavailable, starting empty (${error.message})); return []; } }save()返回写入是否落盘而页脚会如实告知用户——未授权时显示 “Not saved — this host did not grant storage, so the list lasts for this run only”。做法是在边界吸收拒绝然后把真相告诉用户。main.js中的commit()正是用this.persisted save(this.items)记录保存结果并cx.notify()触发重绘。对话框是函数不是元素confirm.js默认导出一个“返回内容函数”的函数main.js用window.open_dialog(confirmClear(count, cx, onConfirm))打开它。计数和回调通过闭包捕获而不是作为props对象跨通道传递——因为元素属于构建它的那次渲染而对话框会活得比那次调用更久。参见 Overlays。关闭则调用window.close_dialog()确认后的清理动作删除已完成项、推送 Toast都收在回调里window.push_toast({ title: Deleted ${count} ${count 1 ? item : items}, level: info, id: cleared });类型就位而且只用了三个文件jsconfig.json开启checkJsgpui-kit.d.ts由gpui-shell types生成types.d.ts存放应用自己的形状——Todo、Filter、Variant、ButtonOptions。编辑器补全和checkJs报错由此生效且没有任何构建步骤。三份配置的要点jsconfig.jsonnoImplicitAny开启每个参数与状态都带类型以 JSDoc 而非 TypeScript 书写——编辑文件后窗口立刻变化、无需中间编译这正是脚本层的全部意义strictNullChecks关闭且这是运行时的形态而非偏好视图在init中赋值状态TypeScript 无法像看待构造函数那样认定其已确定赋值开启只会带来无意义的?.噪音gpui-kit.d.ts之外的形状手写放在 types.d.ts让调用点的标注压缩到每个名字一个词。可停靠工作区The workspacecargo run -p gpui-shell -- examples/js_dockexamples/js_dock/是一个可停靠的工作区——左侧文件列表、中央文档、以及一个“你离开时什么样回来还是什么样”的布局。文件只有两个main.js 工作区面板、停靠、持久化 ui.js 界面装饰标签页、停靠框、拖放提示三个要点值得记住。base 不画任何装饰所以装饰全在 ui.js标签栏、停靠框的标题条、折叠控件、调整大小手柄、拖放提示全部是用普通样式面写成的普通元素ui.js。没有任何装饰的区域依然能停靠、拖拽、调整大小并持久化——它只是除了面板什么都不画。装饰层约定了BAR 30的高度、每块面板的panelBorder描边、dockTab标签页、dockBar标题条、dockHandle调整手柄、emptyGroup空组提示与dropHint落点提示main.js通过dock_area(...)的tab_bar、empty_group、drop_indicator、dock四个槽位注入这些皮肤main.js。标签携带命令而不是处理器chrome 描述在其原生状态变化前是缓存的因此内部的脚本事件处理器没有可靠的生存期。select_tab(group, tab.index)、close_panel(group, tab.id)、drag_tab(group, tab.index)完全不携带脚本值——它们只是指名一个容器以及要向它请求什么。拖拽标签时携带的是 base 自己的面板负载所以把标签丢到另一个组就是移动面板。面板是带两个额外方法的 ViewDocument是普通视图serialize()返回标题与编辑次数deserialize(data)在重启后把它们取回serialize() { return { caption: this.caption, edits: this.edits }; }关于面板的其他一切——它坐落在哪、是否显示——都是布局的事永远不会触及脚本。首次启动时Workspace.init通过DockArea.register_panel(document, Document)等注册面板类必须先注册再加载否则保存的布局无法找回类并用DockArea.new(workspace, { version: 1 })建 dock随后this.dock.add_panel(cx.new(Files), { name: files, placement: left, size: 200 }); this.dock.add_panel(cx.new(Document, { caption: main.js }), { name: document }); this.dock.add_panel(cx.new(Document, { caption: ui.js }), { name: document }); this.dock.add_panel(cx.new(Outline), { name: outline, placement: right, size: 220 });左右两个侧边 dock 都是有意的它们排在同一行的两侧只有左侧的示例无法区分“放对位置的 dock”和“碰巧排在第一的 dock”。持久化走layout_changed事件事件在拖拽的每一步都会触发所以写入落在 500ms 定时器上而不是事件上——this.dock.dump()序列化整棵树、dock 尺寸和每个面板自己的负载this.dock.load(JSON.parse(saved))一次性还原。完整停靠表面见 Dock and Panels。行情看板The quote boardcargo run -- shellGallery 的 Shell story 并排运行两个面板左侧由shell_story.rs用 Rust 绘制右侧由crates/story/js/quotes/main.js用 JavaScript 绘制读取同一份数据。脚本不拥有任何状态。看板是 Rust 的EntityMarket从 story 在运行时启动前注册的 HostModule 导入import { quotes, ticks, watch, watch_all } from market;主题值来自调用作用域的cx.theme()Snapshot而不是第二个 HostModule。从 Rust 侧看crates/story/src/stories/shell_story.rs的install_host_modules()调用gpui_shell::export_module(...)注册名为market的HostModule——注释写明“只授予脚本 market 模块此外一无所有”未注册模块的导入会直接失败并提示该宿主未授予。main.js中的读取都是同步快照readQuotes()、readTicks()只有summary()是异步的它返回 Promise其工作在离主线程处执行表现为请求在途时看板依旧在跳动调用侧用cx.spawn(async (cx) {...})等待结果并cx.notify()。因为两个面板读取同一个实体两者任何不一致都会立刻暴露——这正是它作为“测试”而非“演示”的原因。编辑main.js会改变右侧面板且中间不需要cargo buildstory 在面板旁放了一个 “Reload script” 按钮。其下就是本文档反复引用的计数器读数脚本每秒运行次数对比每秒帧数并带一个 feed 选择器让两者互不干扰——这就是 性能声明 在运行窗口中的可视化。渲染侧同样克制rows()不做任何cx.notify()因为宿主调用让 Rust 修改看板Rust 通知其观察者两半从同一次变更重新渲染。原生动效Native motioncrates/story/js/motion/main.js刻意做成一个与行情基准相互独立的ScriptView这样动画活动不会污染渲染频率的测量。它让你在.transition(...)与.spring(...)之间切换然后重设 opacity 与像素值的 width、height、left、top 目标。motion(element) { if (this.policy spring) { return element .spring(left, { response: 360, damping: 0.72 }) .spring(width, { response: 300, damping: 0.8 }) .spring(opacity, { response: 220, damping: 1 }); } return element .transition(left, { duration: 340, easing: ease-in-out }) .transition(width, { duration: 260, easing: ease-out }) .transition(opacity, { duration: 180, easing: ease-out }); }脚本只运行一次来发布新目标。之后的每一个动画帧都由 GPUI 在本地调度与采样没有 JavaScript 重新进入。示例只用数值型像素目标——没有rem、百分比或auto——并采用稳定 idmotion-runner这样保留的通道在描述重建后依然存活。舞台使用overflow_hidden裁剪配合relative()定位与固定的REST_LEFT/TRAVEL行程常量远边落在 348px适配最窄的面板落点之间由刻度线标注 OPEN 与 LIVE TICK 两站。从哪里开始把examples/js_todolist复制到你自己的目录里直接运行——它是一个完整应用类型已经接好线。把main.js精简回一个带init和render的View保留ui.js然后在此基础上逐步搭建。如果你要做宿主侧crates/story/src/stories/shell_story.rs是可参考的实现它构建运行时、导出 HostModule 注册、挂载ScriptView、并按需重载。对应的调用说明见 Hosting。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。