The New Old Thing

Qt 与 React 混合开发

问题与探索

最近的项目遇到了一个问题:如何在 Qt 里去渲染 AI 对话并实现打字的效果。要在 Qt 里边实现相同的功能一种方案是首先用 C++Markdown 进行解析,然后将解析的 HTML 交由 Qt WebEngine 进行渲染。另外一种就是将解析、渲染的功能完全交由 Qt WebEngine 处理。

权衡之下,显然第二种方案更有优势,用 Web 处理 Markdown 和实现动画有很多成熟的解决方案。Ant Design X 提供了完整的解决方案,同时配合 markdown-it 也能处理 AI 返回的 Markdown

根据需求,有两种方式让 Qt WebEngine 加载 HTML 文件,一种是将页面发布到服务器,从远程加载,另外一种是使用 Qt 的资源系统将 HTML 文件嵌入到生成的二进制可执行文件中。如果选择远程加载,下文中探讨的一些问题将不复存在,但是需要准备服务器等。这里以第二种方式进行探讨。

Web App 配置

完成了技术选型之后,接下来就要进行项目的开发了。要开始一个 React Web App 的开发,有几种选择:1. 使用诸如 Vite 之类的工具创建项目,2. 使用 Next.js 等框架提供的脚手架创建项目,3. 使用 webpack 从零定制项目。这里选择了第 3 个方案,理由如下:

  1. 需要对编译打包的输出进行定制,例如去掉文件 hash,以防止每次文件生成之后都要更新 CMakeLists.txt 文件。
  2. 项目足够简单,不需要使用第三方框架。

由于不需要 hashwebpack.config.js 的配置可以跟下边类似。

 1module.exports = {
 2  ...
 3  output: {
 4    filename: 'app.js',
 5    publicPath: './',
 6  },
 7  resolve: {
 8    extensions: ['.ts', '.tsx', '.js', '.jsx'],
 9  },
10  plugins: [
11    new MiniCssExtractPlugin({
12      filename: 'style.css',
13    }),
14  ],
15  ...
16};

值得注意的是,需要在 html 文件中插入 Qt 提供的 qwebchannel.js 文件,这个文件是 Qt 自动嵌入到资源系统里的。

1<script src="qrc:/qtwebchannel/qwebchannel.js"></script>

去掉了 文件 hash, Code Splitting 之后,打包输出的文件只有 index.html, app.jsstyle.css 。接下来就可以打包进资源了。

1qt_add_resources(App "html"
2    PREFIX "/"
3    FILES
4        html/index.html
5        html/app.js
6        html/style.css
7)

资源加载与渲染

接下来的流程与文章 WebEngine Markdown Editor Example 类似。鉴于 APP 既需要支持历史消息,又需要支持用户输入和 AI 返回的消息。那么需要向 Qt WebChannel 注册两个对象

1channel->registerObject(QStringLiteral("histories"), &m_histories);
2channel->registerObject(QStringLiteral("message"), &m_message);

对于历史消息、用户输入的消息直接进行渲染,对于 AI 返回的消息,则添加打字效果。

 1useEffect(() => {
 2  new QWebChannel(qt.webChannelTransport, function (channel) {
 3    const histories = channel.objects.histories;
 4    const message = channel.objects.message;
 5
 6    if (histories.text) {
 7      setMessages(JSON.parse(histories.text));
 8    }
 9
10    history.textChanged.connect((histories: string) => {
11      if (histories.length > 0) {
12        setMessages(JSON.parse(histories));
13      }
14    });
15
16    message.textChanged.connect((message: string) => {
17      if (message.length > 0) {
18        setMessages((prev) => [
19          ...prev,
20          {
21            ...JSON.parse(message),
22            typing: true,
23          },
24        ]);
25      }
26    });
27  });
28}, [setMessages]);

image.png

image 2.png

效果如上图所示。

总结

Qt WebChannel 提供了 JS Bridge 类似的功能,使得 JavaScript 代码能够与 Native 代码进行交互。方便编写跨平台的 Hybrid App

#Qt #React #WebEngine