前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >NodeGui源码学习

NodeGui源码学习

原创
作者头像
lealc
修改2024-11-25 14:42:22
修改2024-11-25 14:42:22
14300
代码可运行
举报
运行总次数:0
代码可运行

NodeGui是什么

引用一下官方的介绍

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目录结构

NodeGui官方源码的目录结构如下

代码语言:shell
复制
├─config
├─extras
│  ├─assets
│  ├─legal
│  │  ├─logo
│  │  ├─yode
│  │  └─yoga
│  └─logo
├─plugin
├─scripts
│  └─tests
├─src
│  ├─cpp
│  │  └─lib
│  ├─examples
│  └─lib
└─website

其中主要的两个目录是src/cpp 和 src/lib 这两个目录一个是跟C++交互的胶水层,一个是跟js交互的胶水层。

NodeGui的简单用法

NodeGui在正常运行需要NodeJS的环境,实际上是使用的qcode.exe来执行对应的js/ts文件,一个简单示例如下:

代码语言:javascript
代码运行次数:0
复制
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运行机制介绍

NodeGui主要实现了一层针对QT C++的对象映射,然后将对应的每个js对象,内置了一个NativeElement的对象,这个对象利用addon机制与C++里面实例一一绑定,这样js操作每一个qt组件,都相当于同步操作了C++ qt组件,例如jsQMainWindow 定义如下

代码语言:javascript
代码运行次数:0
复制
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++层则通过

代码语言: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对象实现基本一致。

React + NodeGui

NodeGui支持了对于C++ qt对象的创建和管理的能力,那么结合React则可以让js使用更加便利,利用框架的能力和语法来实现界面的快速构建。

那么React又是如何和NodeGui结合起来的呢?

这里主要是利用React的reconciler来实现

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主要是实现Reconciler.HostConfig的定义,Reconciler.HostConfig 是 React Fiber 架构中的一个重要部分,它是渲染器(Renderer)和协调器(Reconciler)之间的接口定义。HostConfig 提供了一组用于操作底层宿主环境(如浏览器 DOM 或原生 UI)的函数,这些函数被 React Fiber 用来执行实际的 DOM 更新、样式设置、事件绑定等操作。

HostConfig 包含以下主要函数:

  • createInstance:创建一个新的宿主节点(如 DOM 元素)。
  • appendChild、removeChild、insertBefore:操作宿主节点的子节点。
  • commitMount:在组件首次挂载到宿主环境时调用,用于执行一些初始化操作,如添加事件监听器。
  • commitUpdate:在组件更新时调用,用于应用 DOM 更新。
  • commitTextUpdate:在文本节点更新时调用,用于更新文本内容。
  • commitDelete:在组件被卸载时调用,用于清理宿主环境中的相关资源。
  • getPublicInstance:获取组件的公共实例(如果有的话)。
  • prepareForCommit:在提交更新之前调用,用于准备宿主环境。
  • resetAfterCommit:在提交更新之后调用,用于重置宿主环境的状态。
  • getRootHostContext、getChildHostContext:获取宿主环境的上下文信息。

React Fiber 架构中的渲染器(如 ReactDOM 或 ReactNative)会提供自己的 HostConfig 实现,以适配不同的宿主环境。这使得 React 可以在多种平台上运行,同时保持核心逻辑的一致性。

主要几个核心函数是createInstance、commitMount、finalizeInitialChildren。

createInstance

createInstance 是 React Fiber 架构中的一个重要函数,它在创建新的宿主节点(如浏览器 DOM 元素)时调用。createInstance 函数在 Reconciler.HostConfig 中定义,并由渲染器(Renderer)实现。不同渲染器(如 ReactDOM 或 ReactNative)可能会提供不同的 createInstance 实现,以适配各自的宿主环境。

createInstance 函数的主要任务是根据组件的类型创建一个新的宿主节点,并返回对该节点的引用。

函数签名如下:

代码语言:javascript
代码运行次数:0
复制
function createInstance(type: string, props: Object, rootContainerInstance: Container): Instance;
  • type:组件类型,可以是 HTML 标签名(如 'div'、'span' 等)或 React 组件类。
  • props:组件的属性(props),包括属性、样式、事件监听器等。
  • rootContainerInstance:根容器实例,用于在需要时访问渲染器的内部状态。

createInstance 函数的主要工作包括:

  • 创建宿主节点:根据组件类型创建一个新的宿主节点(如 DOM 元素)。对于原生 UI 框架(如 React * Native),这可能涉及调用原生 API 创建视图。
  • 设置属性:根据组件的 props 设置宿主节点的属性、样式和类名等。
  • 初始化组件:对于类组件,调用其构造函数进行初始化;对于函数组件,执行函数体以创建组件实例。
  • 返回节点引用:返回对新创建的宿主节点的引用,以便在后续操作中使用。
commitMount

commitMount 是 React Fiber 架构中的一个重要函数,它在组件首次挂载到宿主环境(如浏览器 DOM)时调用。commitMount 主要负责执行一些初始化操作,以确保组件正确地呈现并响应用户交互。

commitMount 函数在 Reconciler.HostConfig 中定义,并由渲染器(Renderer)实现。不同渲染器(如 ReactDOM 或 ReactNative)可能会提供不同的 commitMount 实现,以适配各自的宿主环境。

commitMount 函数的主要任务包括:

  • 设置属性:根据组件的 props 设置宿主节点(如 DOM 元素)的属性、样式和类名等。
  • 添加事件监听器:为宿主节点添加事件监听器,以便响应用户交互(如点击、输入等)。
  • 初始化子组件:递归地初始化组件的子节点,确保整个组件树正确地呈现。
  • 执行副作用:根据组件的生命周期方法(如 componentDidMount)执行副作用操作,如数据获取、订阅等。

需要注意的是,commitMount 是 React 内部的 API,通常不需要直接使用。然而,了解这个概念有助于更好地理解 React 的工作原理和架构。

在 React 18 中,commitMount 函数已经被移除,取而代之的是 commitUpdateQueue 函数。commitUpdateQueue 函数负责执行组件的更新操作,包括首次挂载和后续更新。这使得 React 18 的协调过程更加简洁和高效。

finalizeInitialChildren

finalizeInitialChildren 函数用于在组件首次挂载时执行一些最终的初始化操作。这个函数通常在 appendInitialChild 之后调用,用于设置宿主节点的属性、样式或事件监听器等。

函数签名如下:

代码语言:javascript
代码运行次数:0
复制
function finalizeInitialChildren(parent: Instance, type: string, props: Object, rootContainerInstance: Container): boolean;
  • parent:宿主节点(如 DOM 元素)。
  • type:组件类型(如 HTML 标签名或 React 组件类)。
  • props:组件的属性(props)。
  • rootContainerInstance:根容器实例。

finalizeInitialChildren 函数返回一个布尔值,表示是否需要重新渲染组件。在大多数情况下,这个函数返回 false,表示不需要重新渲染。

Renderer

这个主要是自定义封装,以替代原本React的renderer函数,主要实现如下:

代码语言:javascript
代码运行次数:0
复制
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, () => {});
  }
}

创建App

创建完自定义的Reconciler和Renderer之后,就可以利用React框架来创建基本的app,有如下简单的app实现

代码语言:typescript
复制
// 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上

代码语言:javascript
代码运行次数:0
复制
// 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

react-nodegui这个库主要是为了封装一些组件,来实现更好的对NodeGui使用,开箱即用的组件,包含了常见组件,下面是官方的一个示例

代码语言:typescript
复制
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与NodeGui的结合主要是依赖vue框架中提供的createRenderer函数来实现,植入自定义的rendererOptions来实现组件生命周期的拦截。

Vue.js 框架中的 createRenderer 函数是一个用于创建自定义渲染器的工具。它允许你根据不同的宿主环境(如浏览器 DOM、原生移动应用、服务器端渲染等)来创建相应的渲染器实例。createRenderer 函数接受一个 rendererOptions 对象作为参数,该对象包含了一些配置选项,用于定制渲染器的行为。

createRenderer 函数

createRenderer 函数的定义如下:

代码语言:javascript
代码运行次数:0
复制
function createRenderer(rendererOptions?: RendererOptions): Renderer;
  • rendererOptions:一个可选的配置对象,用于定制渲染器的行为。
  • 返回值:一个 Renderer 实例,该实例具有用于挂载、更新和卸载 Vue 组件的方法。

rendererOptions 对象

rendererOptions 对象包含以下属性:

  • patchProp:用于更新宿主节点属性的方法。默认情况下,Vue 使用了一个通用的 patchProp 方法,但你可以通过提供自定义的实现来覆盖它。
  • insert:用于将子节点插入到宿主节点中的方法。默认情况下,Vue 使用了宿主环境的原生 appendChild 方法,但你可以通过提供自定义的实现来覆盖它。
  • remove:用于从宿主节点中移除子节点的方法。默认情况下,Vue 使用了宿主环境的原生 removeChild 方法,但你可以通过提供自定义的实现来覆盖它。
  • createElement:用于创建新的宿主节点的方法。默认情况下,Vue 使用了宿主环境的原生 createElement 方法,但你可以通过提供自定义的实现来覆盖它。
  • setElementText:用于设置宿主节点文本内容的方法。默认情况下,Vue 使用了宿主环境的原生 textContent 属性,但你可以通过提供自定义的实现来覆盖它。
  • triggerEvent:用于触发宿主节点上的事件的方法。默认情况下,Vue 使用了宿主环境的原生 dispatchEvent 方法,但你可以通过提供自定义的实现来覆盖它。
  • nextTick:用于在下一个 DOM 更新周期后执行回调的方法。默认情况下,Vue 使用了平台特定的实现(如 Promise.resolve().then() 或 setTimeout),但你可以通过提供自定义的实现来覆盖它。

示例

以下是一个使用 createRenderer 函数创建自定义渲染器的示例:

代码语言:javascript
代码运行次数:0
复制
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的事件机制

  • React/Vue层:都是各自封装到各个组件实现上面的对应的Signals来进行注册,例如Button会继承QAbstractButton,然后注册QAbstractButtonSignals来实现对对应事件的注册
代码语言:typescript
复制
export interface QAbstractButtonSignals extends QWidgetSignals {
    clicked: (checked: boolean) => void;
    pressed: () => void;
    released: () => void;
    toggled: (checked: boolean) => void;
}

这里button就只会接受clicked、pressed、released、toggled四个事件

  • NodeGui层:对于事件的封装主要是通过继承,每一个js中的组件都会继承自EventWidget,然后在其构造函数时通过组件对应的C++对象提供的initNodeEventEmitter方法来绑定事件触发函数,最后进行事件的分发。
代码语言:typescript
复制
// 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

2、React-NodeGui

3、Vue-NodeGui

4、官方示例

感谢大家支持,辛苦帮忙点点赞🙏。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • NodeGui是什么
  • NodeGui目录结构
  • NodeGui的简单用法
  • NodeGui运行机制介绍
  • React + NodeGui
    • Reconciler是什么
    • 如何结合
      • Reconciler
      • Renderer
    • 创建App
    • react-nodegui
  • Vue + NodeGui
    • createRenderer 函数
    • rendererOptions 对象
    • 示例
  • 事件机制
  • 资源整理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档