引用一下官方的介绍
NodeGui 使您能够使用 JavaScript 创建桌面应用程序。你可以看到它 作为 Node.js 运行时的略微修改变体,专注于桌面应用程序 而不是 Web 服务器。 NodeGui 还是与跨平台图形用户界面的有效 JavaScript 绑定 (GUI) 库。Qt 是用于构建桌面应用程序的最成熟、最高效的库之一。 与其他流行的 Javascript 桌面 GUI 解决方案相比,这使得 NodeGui 具有极高的内存和 CPU 效率。使用 NodeGui 构建的 hello world 应用程序在不到 20MB 的内存上运行
NodeGui主要是借助node利用addon来调用C++的能力,然后结合QT这个UI框架,来实现用JS驱动C++渲染原生的界面,便于Web从业者能够更好的实现跨平台的客户端应用。
NodeGui官方源码的目录结构如下
├─config
├─extras
│ ├─assets
│ ├─legal
│ │ ├─logo
│ │ ├─yode
│ │ └─yoga
│ └─logo
├─plugin
├─scripts
│ └─tests
├─src
│ ├─cpp
│ │ └─lib
│ ├─examples
│ └─lib
└─website
其中主要的两个目录是src/cpp 和 src/lib 这两个目录一个是跟C++交互的胶水层,一个是跟js交互的胶水层。
NodeGui在正常运行需要NodeJS的环境,实际上是使用的qcode.exe来执行对应的js/ts文件,一个简单示例如下:
import { QMainWindow } from './lib/QtWidgets/QMainWindow';
import { QLabel } from './lib/QtWidgets/QLabel';
import { FlexLayout } from './lib/core/FlexLayout';
import { QWidget } from './lib/QtWidgets/QWidget';
import { QBoxLayout } from './lib/QtWidgets/QBoxLayout';
import { Direction } from './lib/QtEnums';
import { QStackedLayout } from './lib/QtWidgets/QStackedLayout';
import { QComboBox } from './lib/QtWidgets/QComboBox';
// Create main window
const win = new QMainWindow();
win.setWindowTitle('QStackedLayout');
// Create central widget and layout
const centralWidget = new QWidget();
centralWidget.setObjectName('myroot');
const rootLayout = new QBoxLayout(Direction.TopToBottom);
centralWidget.setLayout(rootLayout);
// Create stacked layout
const stackedLayout = new QStackedLayout();
// Create pages with labels
const createPage = (text: string) => {
const page = new QWidget();
const layout = new FlexLayout();
page.setLayout(layout);
const label = new QLabel();
label.setText(text);
layout.addWidget(label);
return page;
};
stackedLayout.addWidget(createPage('This is page 1'));
stackedLayout.addWidget(createPage('This is page 2'));
stackedLayout.addWidget(createPage('This is page 3'));
// Create combo box to switch pages
const combobox = new QComboBox();
combobox.addItems(['Page 1', 'Page 2', 'Page 3']);
combobox.addEventListener('currentIndexChanged', (index) => stackedLayout.setCurrentIndex(index));
// Add combo box and stacked layout to root layout
rootLayout.addWidget(combobox);
rootLayout.addLayout(stackedLayout);
// Create and update label for current index
const currentIndexLabel = new QLabel();
currentIndexLabel.setText(`Current Index: ${stackedLayout.currentIndex()}`);
stackedLayout.addEventListener('currentChanged', (index) => {
currentIndexLabel.setText(`Current Index: ${index}`);
});
rootLayout.addWidget(currentIndexLabel);
// Set up and show main window
win.setCentralWidget(centralWidget);
win.setMinimumSize(300, 100);
win.show();
(global as any).win = win;
基本上实现一个窗口和使用C++来实现并无二致
NodeGui主要实现了一层针对QT C++的对象映射,然后将对应的每个js对象,内置了一个NativeElement的对象,这个对象利用addon机制与C++里面实例一一绑定,这样js操作每一个qt组件,都相当于同步操作了C++ qt组件,例如jsQMainWindow 定义如下
export class QMainWindow extends QWidget<QMainWindowSignals> {
constructor(arg?: QWidget<QWidgetSignals> | NativeElement) {
let native: NativeElement;
if (checkIfNativeElement(arg)) {
native = arg as NativeElement;
} else if (arg != null) {
const parent = arg as QWidget;
native = new addon.QMainWindow(parent.native);
} else {
native = new addon.QMainWindow();
}
super(native);
}
...
}
可以看到,构造函数会创建一个native对象,用于管理组件,而C++层则通过
Napi::Object QMainWindowWrap::init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
char CLASSNAME[] = "QMainWindow";
Napi::Function func = DefineClass(
env, CLASSNAME,
{InstanceMethod("setCentralWidget", &QMainWindowWrap::setCentralWidget),
InstanceMethod("centralWidget", &QMainWindowWrap::centralWidget),
InstanceMethod("takeCentralWidget", &QMainWindowWrap::takeCentralWidget),
InstanceMethod("setMenuBar", &QMainWindowWrap::setMenuBar),
InstanceMethod("menuBar", &QMainWindowWrap::menuBar),
InstanceMethod("setMenuWidget", &QMainWindowWrap::setMenuWidget),
InstanceMethod("setStatusBar", &QMainWindowWrap::setStatusBar),
InstanceMethod("statusBar", &QMainWindowWrap::statusBar),
QWIDGET_WRAPPED_METHODS_EXPORT_DEFINE(QMainWindowWrap)});
constructor = Napi::Persistent(func);
exports.Set(CLASSNAME, func);
QOBJECT_REGISTER_WRAPPER(QMainWindow, QMainWindowWrap);
return exports;
}
实现了对象以及属性的注册,其他qt对象实现基本一致。
NodeGui支持了对于C++ qt对象的创建和管理的能力,那么结合React则可以让js使用更加便利,利用框架的能力和语法来实现界面的快速构建。
那么React又是如何和NodeGui结合起来的呢?
这里主要是利用React的reconciler来实现
React的Reconciler(协调器)是React内部的一个核心组件,负责处理组件的更新和渲染过程。当组件的状态(state)或属性(props)发生变化时,React需要重新渲染组件以反映这些变化。Reconciler的主要任务是协调新旧虚拟DOM(Virtual DOM)之间的差异,并确定需要对实际DOM(Real DOM)执行的最小更改。
Reconciler的工作原理如下:
1、当组件的状态或属性发生变化时,React会创建一个新的虚拟DOM树。
2、Reconciler会将新的虚拟DOM树与旧的虚拟DOM树进行比较(这个过程称为对比或协调)。
3、在对比过程中,Reconciler会递归地遍历新旧虚拟DOM树的节点,并找出它们之间的差异。
4、根据找到的差异,Reconciler会生成一个更新操作列表,这些操作描述了需要对实际DOM执行的更改。
5、React会将更新操作列表发送给Renderer(渲染器),Renderer会负责将这些操作应用到实际DOM上,从而实现组件的更新。
React的Reconciler是一个高度优化的过程,它使用了一些启发式算法来减少不必要的DOM操作,从而提高应用程序的性能。在React 16及更高版本中,Reconciler使用了Fiber架构,这是一种新的协调算法,可以实现更细粒度的控制和更高的性能。
我们通过自定义Renderer来实现对于React各个元素的生命周期的拦截,需要实现自定义Reconciler和Renderer。
实现Reconciler主要是实现Reconciler.HostConfig的定义,Reconciler.HostConfig 是 React Fiber 架构中的一个重要部分,它是渲染器(Renderer)和协调器(Reconciler)之间的接口定义。HostConfig 提供了一组用于操作底层宿主环境(如浏览器 DOM 或原生 UI)的函数,这些函数被 React Fiber 用来执行实际的 DOM 更新、样式设置、事件绑定等操作。
HostConfig 包含以下主要函数:
React Fiber 架构中的渲染器(如 ReactDOM 或 ReactNative)会提供自己的 HostConfig 实现,以适配不同的宿主环境。这使得 React 可以在多种平台上运行,同时保持核心逻辑的一致性。
主要几个核心函数是createInstance、commitMount、finalizeInitialChildren。
createInstance 是 React Fiber 架构中的一个重要函数,它在创建新的宿主节点(如浏览器 DOM 元素)时调用。createInstance 函数在 Reconciler.HostConfig 中定义,并由渲染器(Renderer)实现。不同渲染器(如 ReactDOM 或 ReactNative)可能会提供不同的 createInstance 实现,以适配各自的宿主环境。
createInstance 函数的主要任务是根据组件的类型创建一个新的宿主节点,并返回对该节点的引用。
函数签名如下:
function createInstance(type: string, props: Object, rootContainerInstance: Container): Instance;
createInstance 函数的主要工作包括:
commitMount 是 React Fiber 架构中的一个重要函数,它在组件首次挂载到宿主环境(如浏览器 DOM)时调用。commitMount 主要负责执行一些初始化操作,以确保组件正确地呈现并响应用户交互。
commitMount 函数在 Reconciler.HostConfig 中定义,并由渲染器(Renderer)实现。不同渲染器(如 ReactDOM 或 ReactNative)可能会提供不同的 commitMount 实现,以适配各自的宿主环境。
commitMount 函数的主要任务包括:
需要注意的是,commitMount 是 React 内部的 API,通常不需要直接使用。然而,了解这个概念有助于更好地理解 React 的工作原理和架构。
在 React 18 中,commitMount 函数已经被移除,取而代之的是 commitUpdateQueue 函数。commitUpdateQueue 函数负责执行组件的更新操作,包括首次挂载和后续更新。这使得 React 18 的协调过程更加简洁和高效。
finalizeInitialChildren 函数用于在组件首次挂载时执行一些最终的初始化操作。这个函数通常在 appendInitialChild 之后调用,用于设置宿主节点的属性、样式或事件监听器等。
函数签名如下:
function finalizeInitialChildren(parent: Instance, type: string, props: Object, rootContainerInstance: Container): boolean;
finalizeInitialChildren 函数返回一个布尔值,表示是否需要重新渲染组件。在大多数情况下,这个函数返回 false,表示不需要重新渲染。
这个主要是自定义封装,以替代原本React的renderer函数,主要实现如下:
export class Renderer {
static container?: ReactReconciler.FiberRoot;
update(element: ReactElement, callback: () => void | null | undefined) {
const root = Renderer.container;
if (root) {
return reconciler.updateContainer(element, Renderer.container, null, callback);
}
console.error('ReactTinyDOM update, no root');
}
static forceUpdate() {
if (Renderer.container) {
//@ts-ignore
Renderer.container._reactInternalInstance = Renderer.container.current;
deepForceUpdate(Renderer.container);
}
}
static render(element: React.ReactNode, options?: RendererOptions) {
const isConcurrent = true;
const hydrate = false;
const rendererOptions = Object.assign({}, defaultOptions, options);
Renderer.container = reconciler.createContainer(
appContainer,
isConcurrent,
hydrate
); // Creates root fiber node.
// eslint-disable-next-line @typescript-eslint/ban-types
(rendererOptions.onInit as Function)(reconciler);
const parentComponent = null; // Since there is no parent (since this is the root fiber). We set parentComponent to null.
reconciler.updateContainer(
element,
Renderer.container,
parentComponent,
() => {
// eslint-disable-next-line @typescript-eslint/ban-types
(rendererOptions.onRender as Function)();
}
); // Start reconcilation and render the result
}
static clear() {
reconciler.updateContainer(null, Renderer.container, null, () => {});
}
}
创建完自定义的Reconciler和Renderer之后,就可以利用React框架来创建基本的app,有如下简单的app实现
// App.ts
import React from "react";
export default function App() {
const size = {height: 600, width: 800};
const pos = {x: 300, y: 300};
const [count, setCount] = React.useState(10);
const buttonHandler = {
clicked: () => {
console.log("the button was clicked");
setCount(count + 1);
}
};
return (
<Window size={size} pos={pos}>
<View>
<Button text="Click me" on={buttonHandler}></Button>
<Text>count is {count}</Text>
</View>
</Window>
);
};
创建js文件,自定义创建app挂载到#app上
// main.js
import React from 'React'
import { Renderer } from '@/renderer'
import App from './main/App/App'
console.log('create main page')
// 找到挂载点
const rootElement = document.getElementById('app')
// 渲染
const app = React.createElement(App, rootElement)
// 使用自定义解析
Renderer.render(app) // 这里其实是使用我们自定义的Renderer的render函数了
以上可以实现将React组件解析后转入我们自行处理的逻辑,以便于后面调用NodeGui来创建自定义UI。
react-nodegui这个库主要是为了封装一些组件,来实现更好的对NodeGui使用,开箱即用的组件,包含了常见组件,下面是官方的一个示例
import React from "react";
import { Text, Renderer, Window } from ".";
import { Button } from "./components/Button";
import { View } from "./components/View";
const App = () => {
return (
<Window styleSheet={styleSheet}>
<View id="container">
<View id="textContainer">
<Text>Hello</Text>
</View>
<View>
<Button text="Click me"></Button>
</View>
</View>
</Window>
);
};
const styleSheet = `
#container {
flex: 1;
min-height: '100%';
justify-content: 'center';
}
#textContainer {
flex-direction: 'row';
justify-content: 'space-around';
align-items: 'center';
}
`;
Renderer.render(<App />);
注意这里app的根节点必须是Window组件。
Vue与NodeGui的结合主要是依赖vue框架中提供的createRenderer函数来实现,植入自定义的rendererOptions来实现组件生命周期的拦截。
Vue.js 框架中的 createRenderer 函数是一个用于创建自定义渲染器的工具。它允许你根据不同的宿主环境(如浏览器 DOM、原生移动应用、服务器端渲染等)来创建相应的渲染器实例。createRenderer 函数接受一个 rendererOptions 对象作为参数,该对象包含了一些配置选项,用于定制渲染器的行为。
createRenderer 函数的定义如下:
function createRenderer(rendererOptions?: RendererOptions): Renderer;
rendererOptions 对象包含以下属性:
以下是一个使用 createRenderer 函数创建自定义渲染器的示例:
import { createRenderer } from 'vue';
const rendererOptions = {
// 提供自定义的 patchProp 方法
patchProp(el, key, prevValue, nextValue) {
// 自定义属性更新逻辑
},
// 提供其他自定义选项...
};
const renderer = createRenderer(rendererOptions);
// 使用 renderer 挂载 Vue 组件
const app = Vue.createApp({...});
const vm = renderer.createApp(app).mount('#app');
NodeGui的事件机制
export interface QAbstractButtonSignals extends QWidgetSignals {
clicked: (checked: boolean) => void;
pressed: () => void;
released: () => void;
toggled: (checked: boolean) => void;
}
这里button就只会接受clicked、pressed、released、toggled四个事件
// EventWidget.ts
export abstract class EventWidget<Signals extends unknown> extends Component {
private emitter: EventEmitter;
private _isEventProcessed = false;
constructor(native: NativeElement) {
super(native);
if (native.initNodeEventEmitter == null) {
throw new Error('initNodeEventEmitter not implemented on native side');
}
const preexistingEmitterFunc = native.getNodeEventEmitter();
if (preexistingEmitterFunc != null) {
this.emitter = preexistingEmitterFunc.emitter;
return;
}
this.emitter = new EventEmitter();
this.emitter.emit = wrapWithActivateUvLoop(this.emitter.emit.bind(this.emitter));
const logExceptions = (eventName: string, ...args: any[]): boolean => {
// Preserve the value of `_isQObjectEventProcessed` as we dispatch this event
// to JS land, and restore it afterwards. This lets us support recursive event
// dispatches on the same object.
const previousEventProcessed = this._isEventProcessed;
this._isEventProcessed = false;
// Events start with a capital letter, signals are lower case by convention.
const firstChar = eventName.charAt(0);
const isQEvent = firstChar.toUpperCase() === firstChar;
if (isQEvent) {
try {
const event = wrapNative(args[0]);
const afterBaseWidget = args[1];
const baseWidgetResult = args[2];
if (!afterBaseWidget) {
this.emitter.emit(eventName, event);
} else {
this._isEventProcessed = baseWidgetResult;
this.emitter.emit(`${eventName}_after`, event);
}
} catch (e) {
console.log(
`An exception was thrown while dispatching an event of type '${eventName.toString()}':`,
);
console.log(e);
}
} else {
try {
const wrappedArgs = args.map(wrapNative);
this.emitter.emit(eventName, ...wrappedArgs);
} catch (e) {
console.log(
`An exception was thrown while dispatching a signal of type '${eventName.toString()}':`,
);
console.log(e);
}
}
const returnCode = this._isEventProcessed;
this._isEventProcessed = previousEventProcessed;
return returnCode;
};
logExceptions.emitter = this.emitter;
native.initNodeEventEmitter(logExceptions);
addDefaultErrorHandler(native, this.emitter);
}
...
}
1、NodeGui
4、官方示例
感谢大家支持,辛苦帮忙点点赞🙏。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。