前面很多地方的文章,其实提到了这个东西,本教程会系统一点来帮助从未接触过 GUI 开发、或者熟悉 JavaScript 但未使用过 Reactor 的开发者快速上手,所以细节看文档,这会全面一些介绍大体上开发过程中用得到的知识。
什么是 Reactor?
简单来说,Sciter.js Reactor 不是一个框架或库,而是 Sciter.js 内置的一组功能,它借鉴了 ReactJS 的核心思想,让你能够用声明式的方式构建用户界面。
它主要包含两个核心特性:
- 原生 JSX 支持: 你可以直接在 JavaScript 代码中编写类似 HTML 的 JSX 语法,无需任何额外的编译步骤。
- 高效的 DOM 更新: 通过
element.patch(jsx)
方法,Reactor 能够智能地比较虚拟 DOM (JSX 表达式的结果) 和真实 DOM 的差异,并只更新必要的部分,从而实现高效的 UI 渲染。
为什么选择 Reactor?
- 原生集成: 无需引入外部库,开箱即用。
- 简洁高效: 语法简洁,性能优异,尤其适合桌面应用开发。
- ReactJS 相似性: 如果你熟悉 ReactJS,你会发现很多概念是相通的,上手会更快。
- 桌面 UI 优化: 针对桌面 UI 的特点进行了优化,例如支持窗口化弹出元素等。
核心概念
1. JSX (JavaScript XML)
JSX 允许你在 JavaScript 代码中编写 UI 结构,就像写 HTML 一样。
javascript
// 一个简单的 JSX 表达式
const greeting = <h1>你好, Sciter!</h1>;
document.body.append(greeting); // 将 JSX 渲染到页面
关键点:
- JSX 标签可以是标准的 HTML 标签 (如
<div>
,<span>
),也可以是自定义组件。 - 可以在 JSX 中使用
{}
嵌入 JavaScript 表达式。 - 组件名称必须以大写字母开头。
2. 组件 (Components)
组件是 Reactor 应用的基本构建块。它们是可重用的 UI 单元,可以接收输入数据 (称为 props
) 并返回要在屏幕上显示的虚拟元素。
函数组件 (Function Components): 最简单的方式是使用函数定义组件。
javascript
// 一个简单的函数组件
function Welcome(props) {
return <h1>你好, {props.name}!</h1>;
}
// 使用组件
document.body.patch(<Welcome name="开发者" />);
类组件 (Class Components): 你也可以使用 ES6 类来定义组件。类组件通常用于需要管理自身状态或使用生命周期方法的场景。
javascript
class Clock extends Element {
// 构造函数,初始化状态
constructor() {
super();
this.time = new Date();
}
// 生命周期方法:组件挂载到 DOM 后调用
componentDidMount() {
this.timerId = setInterval(() => {
// 更新组件状态
this.componentUpdate({ time: new Date() });
}, 1000);
}
// 生命周期方法:组件从 DOM 卸载前调用
componentWillUnmount() {
clearInterval(this.timerId);
}
// 渲染方法:返回要显示的 JSX
render() {
return <p>当前时间: {this.time.toLocaleTimeString()}</p>;
}
}
// 使用类组件
document.body.patch(<Clock />);
关键点:
- 组件可以将 UI 拆分成独立、可复用的部分。
props
是从父组件传递给子组件的数据,是只读的。- 类组件通过
this
访问props
和自身状态。
3. 渲染 (Rendering) 与 element.patch()
Reactor 使用 element.patch(jsx)
方法将 JSX 描述的 UI 更新到真实的 DOM 上。它会进行高效的 diff 算法,只更新变化的部分。
javascript
function tick() {
const element = (
<div>
<h1>你好, 世界!</h1>
<h2>现在是 {new Date().toLocaleTimeString()}.</h2>
</div>
);
document.body.patch(element); // 高效更新 DOM
}
setInterval(tick, 1000);
4. 状态 (State) 与 componentUpdate()
对于类组件,可以使用 this
来存储组件的内部状态。当状态需要改变时,必须 调用 this.componentUpdate(newState)
方法来通知 Reactor 更新 UI。
javascript
class Counter extends Element {
count = 0; // 初始化状态
increment() {
// 使用 componentUpdate 更新状态
this.componentUpdate({ count: this.count + 1 });
}
render() {
return (
<div>
<p>点击次数: {this.count}</p>
<button onclick={() => this.increment()}>点击我</button>
</div>
);
}
}
document.body.patch(<Counter />);
关键点:
- 不要直接修改
this
上的状态属性 (除了在constructor
中初始化)。 componentUpdate()
会触发render()
方法重新渲染。componentUpdate()
是异步的,并且会将多次更新合并为一次。
5. 生命周期 (Lifecycle)
类组件有一系列特殊的"生命周期方法",允许你在组件的不同阶段执行代码:
constructor(props, kids)
: 组件实例创建时调用,用于初始化状态和绑定方法。this(props, kids)
: 组件接收新的 props 或 kids 时调用。render()
: 必须的方法,返回要渲染的 JSX。当状态或 props 改变时会被调用。componentDidMount()
: 组件首次挂载到真实 DOM 后调用。适合执行需要 DOM 节点的操作,如网络请求、设置定时器等。componentWillUnmount()
: 组件从 DOM 中卸载前调用。适合执行清理操作,如清除定时器、取消网络请求等。componentDidUpdate()
: 在componentUpdate()
触发的更新完成后调用。
参考示例:上面的 Clock
组件就使用了 componentDidMount
和 componentWillUnmount
。
6. 事件处理 (Event Handling)
在 Reactor 中处理事件非常直观。可以直接在 JSX 中使用 on<EventName>
属性绑定事件处理器。
javascript
function ActionButton() {
function handleClick(event) {
console.log('按钮被点击了!', event);
}
return <button onclick={handleClick}>点我</button>;
}
对于类组件,通常在类中定义事件处理方法:
javascript
class Search extends Element {
handleSearchClick() {
const query = this.$('input').value; // 获取输入框的值
console.log('搜索:', query);
// 可以触发一个自定义事件,通知父组件
this.post(new Event('do-search', { data: query }));
}
render() {
return (
<search>
<input|text placeholder="输入搜索内容" />
<button onclick={() => this.handleSearchClick()}>搜索</button>
</search>
);
}
}
Sciter 还支持更高效的类事件处理器语法 (详见 component-styles-events.md
), 我个人是非常非常喜欢这种写法的,所见所得,不用自己到处去绑定,好象是 ES2020 的新语法:
javascript
class Search extends Element {
// ... render 方法 ...
// 使用特殊语法处理按钮点击
["on click at button"](evt, button) {
const query = this.$('input').value;
console.log('搜索:', query);
this.post(new Event('do-search', { data: query }));
}
}
7. 样式 (Styling)
你可以像在普通 HTML 中一样使用 CSS 来为组件添加样式。
全局样式: 在 HTML 文件中通过 <style>
标签或链接外部 CSS 文件。
组件级样式 (styleset
): 为了更好地封装组件样式,避免全局污染,Reactor 推荐使用 styleset
属性。
-
外部 CSS 文件:
javascript// component.js class MyComponent extends Element { render() { // 使用 __DIR__ 获取当前脚本文件所在目录 return <div styleset={__DIR__ + "styles.css#my-component"}>内容</div>; } }
css/* styles.css */ @set my-component { :root { /* :root 指向组件根元素 */ display: block; border: 1px solid red; padding: 10px; } :root p { color: blue; } }
-
内联 CSS (使用
CSS.set
):javascript// component.js const myComponentStyles = CSS.set` :root { display: block; border: 1px solid green; padding: 8px; } `; class MyComponent extends Element { render() { return <div styleset={myComponentStyles}>内联样式内容</div>; } }
开始第一个 Reactor 应用
让我们创建一个简单的"计数器"应用。
-
创建 HTML 文件 (
counter.htm
):html<html> <head> <title>Reactor 计数器</title> <style> body { padding: 20px; font-family: sans-serif; } button { margin-left: 10px; padding: 5px 10px; } </style> <script type="module"> // 导入或定义 Counter 组件 (见下一步) import { Counter } from './counter.js'; // 将 Counter 组件渲染到 body document.body.patch(<Counter initialCount={0} />); </script> </head> <body> <!-- 组件将渲染在这里 --> </body> </html>
-
创建 JavaScript 文件 (
counter.js
):javascriptexport class Counter extends Element { // 从 props 获取初始值 constructor(props) { super(); this.count = props.initialCount || 0; } increment() { this.componentUpdate({ count: this.count + 1 }); } decrement() { this.componentUpdate({ count: this.count - 1 }); } render() { return ( <div> <span>当前计数: {this.count}</span> <button onclick={() => this.increment()}>+</button> <button onclick={() => this.decrement()}>-</button> </div> ); } }
-
运行: 使用 Sciter 加载
counter.htm
文件。
现在你应该能看到一个可以增加和减少计数的简单应用了!
最佳实践
-
组件化: 将 UI 拆分成小的、可复用的组件。
-
单一职责: 每个组件最好只做一件事。
-
Props 向下传递: 数据流应该是单向的,从父组件通过 props 传递给子组件。
-
状态最小化: 只在必要时使用组件内部状态。尽可能将状态提升到共同的父组件中管理。
-
使用 Keys: 当渲染列表时,为每个列表项提供一个唯一的
key
属性,帮助 Reactor 识别哪些项发生了变化。javascriptfunction ItemList(props) { const listItems = props.items.map((item) => <li key={item.id}>{item.text}</li> ); return <ul>{listItems}</ul>; }
-
善用生命周期: 在合适的生命周期方法中执行副作用(如 API 调用、定时器设置)。
-
组件命名: 组件名称(函数或类)始终使用大写字母开头。
-
样式封装: 优先使用
styleset
来封装组件样式。
示例代码参考
Sciter SDK 提供了丰富的 Reactor 示例,强烈建议查看:
samples.reactor/basic/
: 包含基础概念的示例,如时钟 (component-clock.htm
)、组件更新 (component-update.htm
)。samples.reactor/form/
: 表单处理示例。samples.reactor/todo/
: 一个经典的 TODO 应用示例。samples.reactor/styling/
: 不同的样式定义方式。samples.reactor/tabs/
: Tabs 组件示例。
结语
本教程介绍了 Sciter.js Reactor 的基础知识。Reactor 提供了一种现代、高效的方式来构建桌面应用程序的用户界面。通过实践和参考 SDK 中的示例,你将能够更快地掌握它,但不要忘记原生的文档哦。可以直接去 docs.sciter.com 网站上看。
下一步的建议:
- 深入阅读官方文档
docs/md/Reactor/
目录下的其他文件。 - 动手尝试修改和运行
samples.reactor/
目录下的示例。 - 开始构建你自己的 Reactor 组件!