当在 GUI 界面的时候,通常是左右布局, 或上下布局。当点击左边的内容的时候,右边是怎么样变化的?这可能是写 GUI 刚学的人的一个难题。 如果没写过前端的同学,更加会难以理解。另外,就右边内容中是怎么更新整个桌面 GUI 的这个显示,也是本文介绍的重点。
本文档旨在帮助初学者,特别是拥有前端或其他编程语言背景的开发者, 因为这个和传统浏览器开发差别比较大,快速了解如何使用 Sciter.js 构建桌面 GUI 应用程序的结构。整体 GUI 界面怎么样实现切换。单独的一个布局怎么样更新显示。
1. 项目基本结构
一个典型的 Sciter.js 项目通常包含以下部分:
- HTML 文件 (
.htm
): 定义应用程序窗口的基本结构和布局。通常有一个主 HTML 文件作为入口点。 - CSS 文件 (
.css
): 负责应用程序的样式和外观。 - JavaScript 文件 (
.js
): 实现应用程序的逻辑、交互和组件。
示例 (samples.app/classic/main.htm
):
html
<html window-resizable>
<head>
<title>Test</title>
<style src="main.css" />
<script type="module">
import {Application} from "application.js";
// 启动应用
document.body.patch(<Application/>);
</script>
</head>
<body></body>
</html>
<style src="main.css" />
: 引入 CSS 文件。<script type="module">
: 引入 JavaScript 模块。现代 Sciter.js 推荐使用 ES6 模块。document.body.patch(<Application/>)
: 这是 Reactor 的核心用法,将<Application/>
组件渲染到<body>
元素中。
2. 路由(整体应用GUI层面)与视图(组件层面)切换
路由可以理解成整体应用GUI层面的切换, Sciter.js 没有内置像 React Router 那样的标准路由库,但可以通过不同的方式实现视图切换,其实也是组件,只是这为了区别,我们叫了路由。
局部的界面修改,就是局部的组件内部状态更新来切换不同的显示。
2.1. 界面内基于 href
的路由 (示例: samples.reactor/routing
)
这种方法通过定义特殊的 href
属性(如 route:routeName
)和中心化的路由逻辑来实现。我们要先创建几个对应的组件的 js, 如例子中的 views/initial.js
, views/quick.js
, views/special.js
:
然后在 main.htm
中引入这些组件, 并定义路由逻辑:
main.htm
:
javascript
import {Initial} from "views/initial.js";
import {Quick} from "views/quick.js";
import {Special} from "views/special.js";
// 定义路由名称到视图组件的映射
const routes = {
"initial" : <Initial/>,
"quick" : <Quick/>,
"special" : <Special/>,
};
class App extends Element {
routeName = "initial";
routeView = routes["initial"];
render() {
return <body>
<nav>
{/* 导航链接 */}
<a href="route:initial">Home</a>
<a href="route:quick">Quick</a>
<a href="route:special">Special</a>
</nav>
{/* 显示当前路由对应的视图, 如上面的几个 initial quick special*/}
{this.routeView}
</body>;
}
// 导航逻辑
// 当点击导航链接时, 调用这个方法, 会改变让主应用程序组件的状态, 从而切换显示的视图
// 如点击 quick 会切换到 quick 视图, 点击 special 会切换到 special 视图
navigateTo(routeName) {
let routeView = routes[routeName];
if(routeView) {
this.componentUpdate({ routeView, routeName });
return true;
}
}
// 监听导航链接点击事件
["on click at [href^='route:']"](event, hyperlink) {
const routeName = hyperlink.attributes["href"].substr(6);
return this.navigateTo(routeName);
}
}
document.body.patch(<App/>);
routes
对象映射路由名称和对应的 JSX 组件。App
组件维护当前路由名称 (routeName
) 和视图 (routeView
)。navigateTo
方法根据路由名称更新App
组件的状态,从而切换显示的视图。- 通过监听
a[href^='route:']
的点击事件来触发导航。 - 各个视图组件(如
Initial
,Quick
)可以包含自己的导航按钮 (<button href="route:...">
)。
2.2. 组件内基于状态的条件渲染 (示例: samples.app/classic
)
这种方法不显式使用"路由"概念,而是通过改变主应用程序组件的状态来控制显示哪些子视图。这适用于固定布局、部分区域内容切换的场景。
javascript
class Application extends Element {
navigationViewShown = true;
propertiesViewShown = true;
render() {
return <body>
{/* ... 固定组件 ... */}
<main>
{/* 根据状态显示/隐藏视图 */}
{ this.navigationViewShown && <NavigationView /> }
<ContentView />
{ this.propertiesViewShown && <PropertiesView /> }
</main>
{/* ... 固定组件 ... */}
</body>;
}
// 通过事件处理改变状态
["on click at .view-toggle[name=navigation]"]() {
this.componentUpdate({navigationViewShown: !this.navigationViewShown});
}
// ...
}
Application
组件维护navigationViewShown
等状态。render
方法中使用条件渲染 (&&
) 来决定是否挂载NavigationView
和PropertiesView
。- 点击工具栏或菜单按钮时,调用
componentUpdate
改变状态,从而实现视图的显示/隐藏切换。 - 这种方式下,通常布局是固定的(如
MenuBar
,ToolBar
,StatusBar
,ContentView
始终存在),只有部分区域(NavigationView
,PropertiesView
)根据状态变化。
4. 组件化开发 (Reactor)
上面提到的组件,可能很多人还不理解是什么东西,这有一个简单教学。Sciter.js 的 Reactor 支持类似 React 的组件化开发模式,主要有两种组件类型:
4.1. Class Component (类组件)
类组件继承自 Element
,并实现 render()
方法来返回组件的 JSX 结构。它们可以拥有自己的状态 (state
) 和生命周期方法。
示例 (samples.app/classic/application.js
):
javascript
import {MenuBar} from "menu/menu-bar.js";
// ... 其他导入 ...
export class Application extends Element {
// 应用状态
navigationViewShown = true;
propertiesViewShown = true;
render() {
// 使用 JSX 定义界面结构
return <body>
<MenuBar app={this} />
{/* ... 其他组件 ... */}
<main>
{/* 条件渲染:根据状态决定是否显示导航视图 */}
{ this.navigationViewShown && <NavigationView app={this} /> }
<ContentView app={this}/>
{ this.propertiesViewShown && <PropertiesView app={this} /> }
</main>
{/* ... 其他组件 ... */}
</body>;
}
// 事件处理,更新状态
["on click at .view-toggle[name=navigation]"]() {
// 使用 componentUpdate 更新状态并触发重新渲染
this.componentUpdate({navigationViewShown: !this.navigationViewShown});
}
// ... 其他事件处理 ...
}
render()
: 返回组件的 UI 结构。this.componentUpdate({...})
: 用于更新组件状态并触发界面重新渲染。- 条件渲染: 使用
&&
操作符根据状态 (navigationViewShown
) 动态显示或隐藏子组件。
4.2. Function Component (函数组件)
函数组件是简单的 JavaScript 函数,接收 props
和 kids
(子元素) 作为参数,并返回 JSX 结构。它们通常用于展示型组件。
示例 (samples.reactor/window/trayicon-test.htm
):
javascript
// 生成弹出菜单窗口内容的函数组件
function PopuMenuWindow(props, kids) {
function onWindowActivate(evt) {
if(!evt.reason)
this.close(); // 窗口失去焦点时关闭
}
return <html window-frame="solid-with-shadow"
window-width="max-content"
window-height="max-content"
window-onactivate={onWindowActivate}>
<style src={__DIR__ + "menu-window-style.css"} />
<body>
<menu.popup.visible>
<li onClick={revealWindow}>Show window</li>
{/* ... 其他菜单项 ... */}
</menu.popup.visible>
</body>
</html>;
}
// 在事件处理中使用函数组件创建新窗口
const popup = new Window({
type: Window.POPUP_WINDOW,
html: <PopuMenuWindow />, // 将函数组件的 JSX 作为窗口内容
x: screenX,
y: screenY,
// ... 其他选项 ...
});
4.3 状态管理与界面更新
在 Reactor (类组件) 中,界面更新通常由状态变化驱动。
-
状态定义 : 在类组件的属性中定义状态变量。
javascriptclass Application extends Element { navigationViewShown = true; // 状态变量 // ... }
-
更新状态 : 使用
this.componentUpdate({ stateName: newValue })
方法更新状态。这会自动触发render()
方法,重新计算 UI。javascriptthis.componentUpdate({navigationViewShown: !this.navigationViewShown});
-
条件渲染 : 在
render()
方法中,根据状态值决定渲染哪些元素或组件。jsx{ this.navigationViewShown && <NavigationView /> }
4.4 组件内事件处理
Sciter.js 提供多种事件处理方式:
-
全局事件监听 (
document.on
) : 监听整个文档上的事件,通常使用 CSS 选择器来指定目标元素。javascript// 监听 ID 为 'set' 的按钮点击事件 document.on("click", "button#set", async function(evt, button) { // ... 事件处理逻辑 ... });
-
元素内联事件处理 (JSX) : 在 JSX 标签上直接绑定事件处理器。
jsx<button onClick={this.handleClick}>Click Me</button> <li onClick={revealWindow}>Show window</li>
-
特定元素/窗口事件 (
Window.this.on
) : 监听特定窗口或元素的事件,例如系统托盘图标点击。javascriptWindow.this.on("trayiconclick", evt => { if(evt.data.buttons == 2) { // 右键点击 // ... 处理逻辑 ... } });
-
类组件中的特定选择器事件 : 在类组件内部,可以使用类似 CSS 选择器的语法来绑定事件。
javascriptclass MyComponent extends Element { ["on click at .my-button"](event, element) { // 处理 .my-button 元素的点击事件 } render() { return <button class="my-button">Click</button>; } }
7. 最佳实践
整个 GUI 应用的层面基于 routing 的方式来路由更新组件,内部使用组件的方式来更新内容。
路由的方式有两种:
- 基于
href
的路由(类似samples.reactor/routing
) : 这种方式更接近传统 Web 应用的页面路由,适用于应用包含多个相对独立的、需要整体切换的视图或"页面"的场景。它通常涉及一个顶层组件来管理当前路由状态,并根据href
链接(如route:routeName
)来加载和显示不同的视图组件。这可以看作是应用层面的路由。 - 基于状态的条件渲染(类似
samples.app/classic
) : 这种方式更侧重于在同一个主布局内,根据组件的内部状态来动态显示或隐藏某些子组件或视图区域。例如,点击菜单或按钮改变一个状态变量,然后render
方法根据这个状态决定是否渲染某个面板。这可以理解为在组件内部通过状态控制局部内容的显示切换,虽然不完全是"路由",但实现了类似的效果,特别适用于固定布局下的局部视图更新。
- 组件化: 将 UI 拆分成小的、可复用的组件(类组件或函数组件)。
- 状态管理 :
- 对于简单应用或局部状态,直接在类组件中使用
this.componentUpdate
。 - 对于复杂应用,考虑将共享状态提升到共同的父组件中,并通过 props 向下传递。
- 对于简单应用或局部状态,直接在类组件中使用
- 路由选择 :
- 对于多页面、独立视图切换的应用,使用基于
href
的路由方式(如samples.reactor/routing
所示)更清晰。 - 对于固定布局、局部内容更新的应用,使用基于状态的条件渲染(如
samples.app/classic
所示)更简单直接。
- 对于多页面、独立视图切换的应用,使用基于
- 模块化 : 使用 ES6 模块 (
import/export
) 来组织代码。
希望这份文档能帮助你开始 Sciter.js 的 GUI 开发之旅!建议多参考 SDK 中的 samples.reactor
和 samples.sciter
目录下的示例代码。**