本页内容

适用于 WebAssembly 的 Qt

WebAssembly 版 Qt 允许您在网页上运行 Qt 应用程序。

WebAssembly(简称 Wasm)是一种二进制指令格式,旨在通过虚拟机(例如在网页浏览器中)进行执行。

借助 Qt for WebAssembly,您可以将应用程序作为在浏览器沙箱中运行的 Web 应用程序进行分发。这种方法适用于无需完全访问主机设备功能的 Web 分布式应用程序。

注意:Qt for WebAssembly 是一个受支持的平台,但部分模块尚不支持或处于技术预览阶段。请参阅“受支持的 Qt 模块”。

Qt for WebAssembly 入门指南

为 WebAssembly 构建 Qt 应用程序与为其他平台构建 Qt 类似。 您需要安装 SDK(Emscripten),安装 Qt(或从源代码构建 Qt),最后构建应用程序。其中存在一些差异,例如,Qt for WebAssembly 支持的模块和功能比其他 Qt 构建版本更少。

安装 Emscripten

Emscripten是一套用于编译为 WebAssembly 的工具链。它允许您在无需浏览器插件的情况下,以接近原生速度在网页上运行 Qt。

Qt 的每个次要版本都针对特定的 Emscripten 版本,而在补丁版本中该版本保持不变。Qt 的二进制包是根据目标 Emscripten 版本构建的。由于 Emscripten 无法保证不同版本之间的ABI 兼容性,因此应用程序应使用相同的版本。

6.12.0 支持的 Emscripten 版本为 Emscripten 5.0.5。

有关安装 Emscripten SDK 的更多信息,请参阅Emscripten 文档。

请使用 `emsdk ` 安装特定的Emscripten 版本。例如,要安装适用于 6.12.0 的版本,请输入:

  • ./emsdk install 5.0.5
  • ./emsdk activate 5.0.5

安装完成后,您的路径中应包含 Emscripten 编译器。请使用以下命令进行验证:

em++ --version

在 Windows 系统中,安装完成后 Emscripten 会自动加入环境变量路径。在 macOS 或 Linux 系统中,您需要将其添加到环境变量路径中,操作如下:

source /path/to/emsdk/emsdk_env.sh

请使用以下命令进行验证:

em++ --version

如果您在选择 Emscripten 版本时需要更大的灵活性,可以从源代码构建 Qt。在这种情况下,上述版本为最低要求版本。较新版本预计也能正常工作,但可能会引入行为变化,从而需要对 Qt 进行相应修改。

安装 Qt

请从您的 Qt 账户中的“下载”部分下载 Qt。我们提供了适用于 Linux、macOS 和 Windows 这三种开发平台的预编译版本。

这些二进制版本旨在支持尽可能多的浏览器,并提供单线程和多线程版本。二进制版本不支持Wasm SIMD 和Wasm exceptions 等非标准功能。

从源代码构建 Qt

通过源代码编译,您可以设置 Qt 的配置选项,例如线程支持、OpenGL ES 级别或 SIMD 支持。请从您的 Qt 账户的“下载”部分下载 Qt 源代码。

将 Qt 配置为针对wasm-emscripten 平台的交叉编译构建。这将设置-static 、-no-feature-thread 和-no-make examples 配置选项。您可以通过-feature-thread 配置选项启用线程支持。不支持共享库构建。

您需要一个版本相同的 Qt 主机构建。此外,请通过 CMake 变量QT_HOST_PATH或使用 configure 参数-qt-host-path 设置主机构建的路径。

./configure -qt-host-path /path/to/Qt -platform wasm-emscripten -prefix $PWD/qtbase

注意: 如果存在ninja 可执行文件,configure 始终会使用Ninja生成器和构建工具。Ninja 具有跨平台、功能丰富、性能优异等特点,建议在所有平台上使用。使用其他生成器虽然可能有效,但不提供官方支持。

在 Windows 系统上,请确保您的PATH 中包含 Mingw-w64,并使用以下命令执行 configure:

configure -qt-host-path C:\Path\to\Qt -no-warnings-are-errors -platform wasm-emscripten -prefix %CD%\qtbase

然后构建所需的模块:

cmake --build . -t qtbase -t qtdeclarative [-t another_module]

在命令行上构建应用程序

Qt for WebAssembly 支持使用 CMake 配合 ninja 或 make 构建应用程序。

$ /path/to/qt-wasm/qtbase/bin/qt-cmake .
$ cmake --build .

注意:当 使用原生CMake (而非Linux上的qt-cmake 或Windows上的qt-cmake.bat )时, 请务必通过“-DCMAKE_TOOLCHAIN_FILE”指定工具链文件,这与其他跨平台构建操作相同。详情请参阅:CMake入门指南。

构建应用程序会生成多个输出文件,包括一个包含应用程序和 Qt 代码(静态链接)的 .wasm 文件,以及一个可在浏览器中打开以运行应用程序的 .html 文件。

注意: 在“-g”调试级别下,Emscripten 生成的 .wasm 文件体积较大。建议在调试构建时使用“-g2”进行链接。

运行应用程序

运行应用程序需要一个 Web 服务器。构建生成的文件均为静态内容,因此任何 Web 服务器均可满足需求。某些使用场景可能需要特殊的服务器配置,例如提供 HTTPS 证书或设置启用多线程支持所需的 HTTP 头部。

Emrun

Emscripten 提供了emrun实用程序用于测试运行应用程序。Emrun 会启动一个 Web 服务器,打开浏览器,并捕获及转发 stdout/stderr(这些输出通常会发送到 JavaScript 控制台)。

/path/to/emscripten/emrun --browser=firefox appname.html

Python http.server

另一种方法是启动一个开发 Web 服务器,然后单独启动 Web 浏览器。最简单的方法之一是使用 Python 的 http.server:

python -m http.server

请注意,这只是一个简单的 Web 服务器,不支持多线程所需的 SharedArrayBuffer,因为它不会发送下文提到的必需的 COOP(跨源打开策略)和 COEP(跨源嵌入策略)头部。

qtwasmserver

Qt 提供了一个开发版 Web 服务器,该服务器使用mkcert生成 HTTPS 证书。这使得测试需要安全上下文的 Web 功能成为可能。请注意,通过 http://localhost 进行的传输也被视为安全,且无需证书。

该 Web 服务器还会将COOP和COEP头设置为特定值,从而启用对SharedArrayBuffer和多线程的支持。

qtwasmserver 脚本会启动一个服务器,该服务器默认绑定到 localhost。您可以使用-a命令行参数添加其他地址,或使用--all 绑定到所有可用地址。

qtwasmserver 脚本位于源代码目录下的

qtbase/util/wasm/qtwasmserver/qtwasmserver.py

,也可以通过 pip 安装:

pip install qtwasmserver

有关 qtwasmserver 的更多信息,请参阅qtwasmserver

使用 qtwasmserver:

python /path/to/qtbase/util/wasm/qtwasmserver/qtwasmserver.py --all

使用Qt Creator

Qt Creator:为 Web 构建应用程序。

在 Web 上部署应用程序

构建应用程序会生成多个文件(在下表中,请将“app”替换为应用程序名称)。

生成的文件简要说明
app.htmlHTML 容器
qtloader.js用于加载 Qt 应用程序的 JavaScript API
app.js由 Emscripten 生成的 JavaScript 运行时
app.wasm应用程序二进制文件

您可以直接部署app.html,也可以将其替换为自定义的 HTML 文件。此外,还可以进行一些微调,例如将启动画面中的 Qt 徽标替换为应用程序徽标。无论哪种情况,qtloader.js都提供了一个用于加载应用程序的 JavaScript API。

部署前请使用gzip 或brotli 对 Wasm 文件进行压缩,因为这些工具提供的压缩率优于其他工具。更多信息请参阅《缩小二进制文件大小》。

启用某些功能(例如多线程和 SIMD)生成的 .wasm 二进制文件,将与不支持该功能的浏览器不兼容。 可以通过构建多个 .wasm 文件,然后使用 JavaScript 特性检测来选择正确的文件,从而绕过这一限制,但请注意,Qt 并未提供任何用于实现此功能的功能。

使用 qtloader

Qt 提供了一个 JavaScript API,用于下载、编译和实例化 Qt for WebAssembly 应用程序。该加载 API 封装了 Emscripten 提供的加载功能,并为基于 Qt 的应用程序提供了有用的附加功能。该 API 实现于 qtloader.js 文件中。 在构建时,该文件的副本会被写入构建目录。

典型用法如下所示:

const app_container_element = ...;
const instance = await qtLoad({
    qt: {
        containerElements: [ app_container_element ],
        onLoaded: () => { /* handle application load completed */  },
        onExit: () => {  /* handle application exit */ },
    }
});

代码通过一个配置对象调用 qtLoad() 加载函数。该配置对象可以包含任何 emscripten 配置选项,以及一个特殊的“qt”配置对象。qt 配置对象支持以下属性:

属性简要说明
containerElementsHTML 容器元素的数组。应用程序将这些元素视为 QScreens。
onLoaded应用程序加载完成时的回调函数。
onExit应用程序退出时的回调函数。

containerElements 数组是 Qt 与网页之间的主要接口,该数组中的 HTML 元素(通常是 <DIV> 元素)指定了应用程序内容在网页上的位置。

应用程序将每个容器元素视为QScreen 实例,并可照常在屏幕实例上放置应用程序窗口。设置了Qt::WindowFullScreen 状态的窗口将使用整个屏幕区域,而非“全屏”窗口则会显示窗口边框。

qtLoad() 函数返回一个 Promise,当对其进行等待时,该 Promise 会返回一个 Emscripten 实例。该实例提供了对 Embind 导出函数的访问。Qt 导出了若干此类函数,这些函数共同构成了该实例的 API。

使用 Qt 实例 API

Qt 提供了若干实例函数。目前,这些函数支持在运行时添加和移除容器元素。

属性简要说明
qtAddContainerElement添加一个容器元素。添加元素将插入一个新的QScreen 。
qtRemoveContainerElement移除一个容器元素及其对应的屏幕。
qtSetContainerElements设置所有容器元素
qtResizeContainerElement使 Qt 检测到容器元素大小的变化。

移植到 Qt 6.6 的 qtloader

Qt 6.6 包含一个新的 qtloader,其实现已简化且作用范围更小。这涉及 API 变更,可能需要移植应用程序的 JavaScript 代码。Qt 提供了一个兼容性 API 以简化过渡过程。根据具体用例,有以下几种解决方案:

  • 如果您直接使用生成的app.html 文件,则该文件在构建时也会随之更新。无需采取任何操作。
  • 如果您使用的是 qtloader 的基本功能集,则可将 Qt 6.6 中包含的兼容性 API 作为临时措施。该 API 将在未来的版本中移除;您应计划更新以使用新的 qtloader。此时需要执行下文中的移植步骤 1。
  • 如果您正在使用高级功能(例如在运行时添加容器元素),则必须迁移到新的加载器或实例 API。需要执行下面的迁移步骤 1 和 2。

移植步骤

  1. 从加载的 HTML 文件中引入app.js (由 Emscripten 生成的 JavaScript 运行时)。
    <script src="app.js"></script>

    在 Qt 6.6 之前,qtloader 会加载并解析此 JavaScript 文件。现在不再执行此操作,必须使用 <script> 标签来包含该文件。

  2. 移植至使用新的 JavaScript 和实例 API。

请参阅上文的文档部分。

支持的浏览器

桌面版

Qt for WebAssembly 是在以下浏览器上开发和测试的:

  • Chrome
  • Firefox
  • Safari
  • Edge

如果浏览器支持 WebAssembly,Qt 应该可以运行。支持 WebAssembly 的浏览器通常也支持 WebGL,不过有些浏览器会将旧版或不受支持的 GPU 列入黑名单。s/qtloader.js提供了用于检查 WebGL 是否可用的 API。

Qt 不会直接使用操作系统功能,因此,例如 Firefox 是在 Windows 还是 macOS 上运行,对 Qt 来说并无区别。Qt 确实会进行一些操作系统适配,例如在 macOS 上对 Ctrl/Cmd 键的处理。

移动端

用于 WebAssembly 应用程序的 Qt 可在移动浏览器(如移动版 Safari 和 Android 版 Chrome)上运行。

支持的 Qt 模块

WebAssembly 版 Qt 仅支持 Qt 模块和功能子集。已通过测试的模块列于下方,其他模块可能正常工作,也可能无法正常工作。

以下模块处于技术预览阶段。它们的功能可能有限,或在未来的版本中发生重大变化。

在任何情况下,模块的支持都可能不完整,并且可能存在其他限制,这些限制可能是由于浏览器沙箱造成的,也可能是由于 Qt 平台移植尚不完善所致。有关更多信息,请参阅《使用 Qt 开发 WebAssembly》。

使用 Qt for WebAssembly 进行开发

使用 CMake 构建

如果需要在 CMake 中进行 Emscripten 特定的配置,可以使用以下代码:

if(EMSCRIPTEN)
    # WebAssembly specific code
else()
    # other platforms
endif()

该代码既能支持 Emscripten 特有的配置,又能确保与其他平台的兼容性。

OpenGL 和 WebGL

Qt for WebAssembly 支持通过https://developer.mozilla.org/en-US/docs/Web/API/WebGL_APIWebGL 实现硬件加速渲染。

WebGL 严格遵循 OpenGL ES 标准,其版本对应关系如下:

OpenGLWebGL
OpenGL ES 2.0WebGL 1
OpenGL ES 3.0WebGL 2

Qt 会使用可用的最高 WebGL 版本。在当今的浏览器中,这通常是 WebGL 2,但如果受硬件限制,则可能是 WebGL 1。我们建议使用 Qt for WebAssembly 针对支持 WebGL 2 的设备进行开发。

Web 和桌面版 OpenGL 之间的差异已在《WebGL 与 OpenGL 的差异》中记录。WebGL 1.0 与 WebGL 2.0 之间还存在其他差异,这些差异已在《WebGL 2.0 规范》中记录。

默认情况下,系统会使用 ES2(及 ES3)中兼容 WebGL 的子集。如果您需要在不使用绑定缓冲区的情况下使用glDrawArrays 和glDrawElements ,可以通过添加以下内容来启用完整的 ES2 支持:

target_link_options(<your target> PRIVATE -s FULL_ES2=1)

来启用完整的 ES2 支持,以及/或通过添加

target_link_options(<your target> PRIVATE -s FULL_ES3=1)

到项目的CMakeLists.txt 中,即可实现和/或完整的ES3模拟。

有关 Emscripten 对 OpenGL 支持的更多信息,请访问https://emscripten.org/docs/porting/multimedia_and_graphics/OpenGL-support.html

OpenGL 上下文的限制

WebGL 不支持每个表面拥有多个上下文。这会对直接或间接(例如通过QOpenGLWidget 等类)使用 `QOpenGLContext ` 的应用程序产生影响。

每个QOpenGLContext 实例应仅与单个表面关联。实际上,上下文会在首次调用 makeCurrent() 时与该表面建立关联。此后在其他表面上调用 makeCurrent(),或者从使用同一表面的另一个QOpenGLContext 实例调用 makeCurrent(),均不被支持。

不支持 OpenGL 上下文共享。调用QOpenGLContext::setShareContext() 不会产生任何效果,而QOpenGLContext::shareContext() 始终返回 nullptr。

销毁一个表面(例如QWindow )会导致相关上下文丢失。应用程序应通过重新创建上下文来处理此情况。

QOpenGLWidget 以及其他在内部使用上下文共享的类均不被支持。

多线程

Qt for WebAssembly 通过 Emscripten 的Pthreads 支持实现多线程功能,其中每个线程都由一个Web Worker 提供支持。 要启用多线程,请从Qt Maintenance Tool 安装“WebAssembly(多线程)”组件,或者从源代码构建 Qt 并在 configure 时传入“-feature-thread”标志。

现有的多线程代码通常可以复用,但可能需要进行修改以适应 pthread实现的具体细节。某些 Emscripten 和 Qt 功能不被支持,其中包括线程代理功能以及Qt Quick 中的多线程渲染循环。

请注意,在 Qt for WebAssembly 中,避免阻塞主线程尤为重要,因为主线程可能需要处理来自次要线程的请求。例如,Qt 中的所有定时器都在主线程上调度,如果主线程被阻塞,这些定时器将不会触发。 另一个例子是,创建新的 Web Worker(用于线程)只能在主线程上进行。

Emscripten 针对此问题提供了一些缓解措施。对于获取互斥锁等短期等待,支持通过忙等待并在等待锁期间处理事件来实现。应避免在主线程上进行较长时间的等待。 特别是,调用QThread::wait()或pthread_join()来等待子线程的常见做法将无法奏效,除非应用程序能保证该线程(及Web Worker)已启动,并且在调用wait()或join()时,该线程能够无需主线程协助即可完成执行。

多线程功能需要浏览器支持SharedArrayBufferAPI。(通常,Emscripten 将堆存储在 ArrayBuffer 对象中。 对于多线程,堆必须与 Web Worker 共享,因此需要 SharedArrayBuffer。)该 API 通常在所有现代浏览器中都可用,但如果未满足某些安全要求,可能会被禁用。此时,启用了线程支持的 WebAssembly 二进制文件将无法运行,即使该二进制文件实际上并未启动任何线程也是如此。

启用 SharedArrayBuffer 需要安全的浏览上下文(即页面通过 https:// 或 http://localhost 提供),并且页面必须处于跨源隔离模式。后者可通过在 Web 服务器上设置所谓的 COOP 和 COEP 标头来实现:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

SIMD

Emscripten 支持WebAssembly SIMD,该功能为 WebAssembly 提供了 128 位 SIMD 类型和运算。

请从源代码构建 Qt,并使用 -feature-wasm-simd128 标志进行配置以启用该功能;这将在编译和链接时传递 -msimd128 标志。 请注意,目前 Qt 本身并不包含针对 wasm-simd 优化的代码路径,但启用 wasm-simd 将开启编译器的自动向量化功能,使编译器能够在适用的情况下使用 SIMD 指令。

您可以直接使用 GCC/Clang SIMD 向量扩展或 WASM SIMD128 内置函数来针对 WebAssembly SIMD 进行开发。有关更多信息,请参阅 Emscripten SIMD 文档。

此外,Emscripten 支持将 x86 SSE 指令模拟/转换为 Wasm SIMD 指令。Qt 不使用此模拟机制,因为使用没有原生 Wasm SIMD 对应指令的 SSE SIMD 指令可能会导致性能下降。

请注意,启用了 SIMD 的二进制文件与不支持 WebAssembly SIMD 的浏览器不兼容,即使在运行时未调用 SIMD 代码路径也是如此。可能需要在浏览器的高级配置中(例如“about:config”或“chrome:flags”)启用 SIMD 支持。

网络

Qt 对网络功能的支持有限。通常,Web 上已使用的网络协议也可在 Qt 中使用,而其他协议则因 Web 沙箱的限制而无法直接使用。

支持以下协议:

  • QNetworkAccessManager 向网页源服务器或支持 CORS 的服务器发送 HTTP 请求。这包括来自 QML 的 XMLHttpRequest。
  • QWebSocket 连接到任何主机。请注意,通过安全 https 协议提供的网页仅允许通过安全的 wss 协议建立 WebSocket 连接。
  • 通过 WebSockets 模拟 POSIX TCP 套接字,利用Emscripten 提供的功能。请注意,这需要运行一个负责处理套接字转换的转发服务器。

不支持所有其他网络协议。

注意: 由于浏览器的限制, QWebSocketServer 不受支持。浏览器会限制服务器端的套接字功能,以确保 Web 沙箱内的安全性。因此,任何依赖QWebSocketServer 来接受传入网络连接的功能,都无法在 Web 环境中使用。

注意: Qt MQTTQtRemoteObjects 模块可与QtWebSockets 作为传输协议配合使用。这些模块未获得官方支持,可能正常工作也可能无法工作,或者存在功能缺失的情况。请参阅QMqttClient::connectToHostWebSocket 以及QtRemoteObjects的WebSockets应用程序示例。

跨源资源共享(CORS)与策略(CORP)

使用网络功能的 WebAssembly 应用程序可能需要服务器设置跨源资源共享 (CORS) 和跨源资源策略 (CORP) 响应头。这些机制会限制源自不同域的 HTTP 请求,因为此类请求会带来安全风险。

使用QHttpServer ,以下是设置这些标头的示例:

auto headers = response.headers();
headers.append("Access-Control-Allow-Origin", "*");
headers.append("Access-Control-Allow-Origin", "localhost");
headers.append("Access-Control-Allow-Methods", "POST, GET, OPTIONS");

headers.append("Cross-Origin-Opener-Policy", "same-origin");
headers.append("Cross-Origin-Embedder-Policy", "require-corp");
headers.append("Cross-Origin-Resource-Policy", "cross-origin");

其他知名服务器也有各自配置发送这些标头的具体方法。

如果在 Access-Control-Allow-Origin 中使用通配符“*”,且同时使用了 Access-Control-Allow-Credentials,将会导致错误。

随附的实用脚本 qtwasmserver.py 会在启用--cross-origin-isolation 选项时设置这些标头。

本地文件访问

在 Web 环境中,文件系统访问处于沙箱环境中,这会对应用程序处理文件的方式产生影响。 Web 平台提供了在用户控制下访问本地文件系统的 API,以及访问持久化存储的 API。Emscripten 和 Qt 封装了这些功能,并提供了便于从 C++ 和基于 Qt 的应用程序使用的 API。

Web 平台提供了访问本地文件和持久化存储的功能:

  • <input type="file"> 用于显示原生文件打开对话框,供用户选择文件。
  • IndexedDB 提供持久性本地存储(无法在浏览器外部访问)

Emscripten 提供了多个具有 POSIX 样式 API 的文件系统。其中包括:

  • MEMFS 临时文件系统,该系统将文件存储在内存中
  • IDBFS 持久性文件系统,它使用 IndexedDB 存储文件

Emscripten 会在应用程序启动时将一个临时 MEMFS 文件系统挂载到“/”目录下。这意味着可以使用 `QFile `,且默认情况下该文件系统会将文件的读写操作直接在内存中进行。该文件系统在浏览器刷新后不会被保留。

由于网页无法直接访问本地文件系统,Qt 为 Qt for WebAssembly 提供了QFileDialog API。

剪贴板访问

Qt 支持通过系统剪贴板复制和粘贴文本、URL、已知文件类型以及图像,但受 Web 沙箱限制,存在一些差异。它不支持任意的 application/octet-stream 二进制数据。 通常,访问剪贴板需要用户许可,可通过处理输入事件(例如 CTRL+c)或使用剪贴板 API 来获取该许可。

字体

Qt WASM 模块包含 3 种嵌入式字体:“Bitstream Vera Sans”(备用字体)、“DejaVu Sans”和“DejaVu Sans Mono”。

这些字体提供的字符集有限。Qt 提供了多种选项来添加其他字体:

其中一种方法是在 Qt Qml 中使用 `FontLoader `,该方法既可以通过 URL 获取字体,也可以使用Qt 资源系统(与常规桌面应用程序的工作方式相同)。

另一种使用字体的方法是通过QFontDatabase::addApplicationFontFromData 添加字体。

无障碍功能与屏幕阅读器

Qt for WebAssembly 为屏幕阅读器提供了基本支持。按钮和复选框等简单 UI 元素可以正常工作,而表格或树形视图等较复杂的 UI 元素可能尚不完全支持。Qt Widgets 和Qt Quick 均受支持。

以下屏幕阅读器/浏览器组合已经过测试,确认可正常工作。其他浏览器和屏幕阅读器也可能正常工作。

  • macOS 系统上的 Safari 配合 VoiceOver
  • macOS 系统上 Chrome 配合 VoiceOver 使用

该无障碍功能通过创建“shadow”HTML元素来为Qt UI元素提供无障碍信息。此功能默认处于禁用状态。最终用户可通过屏幕阅读器点击“激活屏幕阅读器”按钮来启用该功能。启用后,网页中将填充相应的无障碍元素。

拖放

  • 支持拖放操作(支持浏览器支持的文件和 MIME 类型)。
  • 支持应用程序内部的拖放操作。
  • 不支持将文件拖出应用程序。

应用程序启动与事件循环

Qt for WebAssembly 支持标准的 Qt 启动方式,即应用程序创建一个 `QApplication ` 对象并调用 `exec` 函数:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    QWindow appWindow;

    return app.exec();
}

上述对 exec() 的调用通常会阻塞并处理事件,直至应用程序关闭。遗憾的是,在 Web 平台上,由于不允许阻塞主线程,这种做法行不通。因此,在处理完每个事件后,必须将控制权交还给浏览器的事件循环。

Qt 通过让 exec() 将主线程控制权交还给浏览器(同时保留栈)来解决这个问题。从应用程序代码的角度来看,exec() 函数会被调用,事件处理也照常进行。但是,exec() 调用永远不会返回,即使在应用程序退出时也是如此。

这种行为通常是可以接受的,因为浏览器会在应用程序关闭时释放其内存。但这确实意味着关闭代码不会执行,因为应用程序对象会发生内存泄漏,其析构函数也不会被调用。

您可以通过将 main() 重写为异步来避免此问题——由于 Emscripten 在 main() 返回时不会退出运行时,因此这是可行的。随后,应用程序代码省略对 exec() 的调用,并通过删除顶级窗口和应用程序对象来干净地关闭 Qt。

QApplication *g_app = nullptr;
AppWindow *g_appWindow = nullptr;

int main(int argc, char **argv)
{
    g_app = new QApplication(argc, argv);
    g_appWindow = new AppWindow();
    return 0;
}

注意:此方法 仅适用于未使用 Asyncify 或 JSPI 的情况。使用 Asyncify 或 JSPI 时,必须在 main() 中调用 exec()。

Asyncify 和 JSPI

由于 Web 平台的限制,禁止同步 C++ 代码调用异步 JavaScript API,因此 WebAssembly 的默认 Qt 构建版本不支持通过调用QEventLoop::exec() 或QDialog::exec() 等方式重新进入事件循环。

需要使用 asyncify/JSPI 的功能包括:

  • QDialogs、带有返回值的 QMessageBox。
  • 拖放(特别是拖动操作)。
  • 嵌套/二级事件循环 exec()。

Emscripten 通过Asyncify功能提供了绕过这些限制的支持。该功能有两种版本:

  • Asyncify,通过 WebAssembly 代码后处理步骤实现。
  • JSPI,通过 WebAssembly 的JS Promise 集成功能实现。

有关如何启用每种选项以及相关权衡的详细信息,请参阅下文各章节。简而言之,Asyncify 目前已可用,但会带来构建时间延长、二进制文件体积增大以及运行时性能开销等额外负担。JSPI 的开销极小或几乎为零,但尚未得到所有浏览器的支持。

注意: 在早期版本的 Qt for WebAssembly 中,在 main()函数中调用 application.exec() 是可选的,但现在使用 Asyncify 或 JSPI 时,此调用已成为必需。

Asyncify

通过在链接器选项中添加“-sASYNCIFY -Os”标志来启用 asyncify:

CMake:

target_link_options(<your target> PUBLIC -sASYNCIFY -Os)

qmake:

QMAKE_LFLAGS += -sASYNCIFY -Os

启用 asyncify 会增加二进制文件大小和 CPU 占用率,从而产生额外开销。请在启用优化选项的情况下进行构建,以尽量减少这些开销。

JSPI(JS Promise 集成)

与 Asyncify 不同,使用 JSPI 需要从源代码构建 Qt。向 Qt 的 configure 脚本传递 -feature-wasm-jspi 和 -feature-wasm-exceptions 标志以启用该功能。(JSPI 与 Emscripten 默认模拟的异常不兼容。)

然后,通过在链接器选项中添加 -sJSPI 标志,为您的应用程序启用 JSPI:

CMake:

target_link_options(<your target> PUBLIC -sJSPI)

qmake:

QMAKE_LFLAGS += -sJSPI

调试与性能分析

Wasm 的调试是在浏览器的 JavaScript 控制台中进行的。无法直接在Qt Creator 中对 Wasm 应用程序进行调试。

您可以通过 Emscripten 链接器参数增加输出详细程度,以辅助调试:

  • -s LIBRARY_DEBUG=1(打印库调用)
  • -s SYSCALL_DEBUG=1(打印系统调用)
  • -s FS_LOG=1(打印文件系统操作)
  • -s SOCKET_DEBUG(打印套接字和网络数据传输信息)

CMake:

target_link_options(<your target> PRIVATE -s LIBRARY_DEBUG=1)

qmake:

QMAKE_LFLAGS_DEBUG += -s LIBRARY_DEBUG=1

优化

Qt for WebAssembly 使用 Emscripten 工具链生成二进制文件,其中有许多参数可能会影响性能和二进制文件的大小。更多信息请参阅《Emscripten:代码优化》。

您可以像对待普通 C++ 应用程序一样传递链接器和编译器参数:

target_compile_options(<your target> PRIVATE -oz -flto)
target_link_options(<your target> PRIVATE -flto)
QMAKE_CXXFLAGS += -oz -flto
QMAKE_LFLAGS += -flto

缩小二进制文件的大小

为了提供无缝的用户体验,缩短 WebAssembly 应用程序的下载和加载时间至关重要。更小的应用程序二进制文件是实现更快下载的关键因素之一。请使用以下方法来减小二进制文件大小:

  • 请确保分发的是发布版构建。调试版构建包含调试符号,文件体积会大得多。
  • 在服务器端启用压缩功能。gzip 和Brotli等最常见的压缩算法对Wasm二进制文件效果显著,可大幅缩减其大小。
  • 尝试使用可能生成更小二进制文件的编译器和链接器选项(例如 '-os'、'-oz')。具体效果因应用程序而异。
  • 从源代码编译 Qt for WebAssembly 时,请禁用未使用的功能(见下文)。
禁用功能

WebAssembly 应用程序默认会静态链接到 Qt 库,从而使编译器能够消除死代码。然而,由于 Qt 的动态特性,编译器并不总能进行此类优化。

如果您从源代码构建 Qt for WebAssembly,可以禁用某些功能以减小 Qt 二进制文件的大小,从而相应地减小 .wasm 二进制文件的大小。 Qt 默认会为 WebAssembly 平台禁用某些功能,但您也可以禁用应用程序中未使用到的功能。有关详细信息,请参阅“已禁用功能”。

您可以禁用以下功能来减小二进制文件大小(通常可减少 10-15%):

配置参数简要说明
-no-feature-cssparser层叠样式表(CSS)解析器。
-no-feature-datetimeedit编辑日期和时间(依赖于 datetimeparser)。
-no-feature-datetimeparser解析日期和时间文本。
-no-feature-dockwidget将小部件停靠在QMainWindow 内,或将其作为顶级窗口浮动在桌面上。
-no-feature-gestures手势框架。
-no-feature-mimetypeMIME 类型处理。
-no-feature-qml-network网络透明性。
-no-feature-qml-list-modelListModel QML 类型。
-no-feature-qml-table-modelTableModel QML 类型。
-no-feature-quick-canvasCanvas 项目。
-no-feature-quick-path路径元素。
-no-feature-quick-pathviewPathView 项目。
-no-feature-quick-treeviewTreeView 项目。
-no-feature-style-stylesheet可通过 CSS 配置的小部件样式。
-no-feature-tableview表格视图的默认模型/视图实现。
-no-feature-texthtmlparserHTML 解析器。
-no-feature-textmarkdownreaderMarkdown(CommonMark 和 GitHub)解析器。
-no-feature-textodfwriterODF 写入器。

Wasm 异常

Qt 默认在构建时不支持异常,此时抛出异常将导致程序终止。可以通过从源代码构建并向 Qt configure 传递 -feature-wasm-exceptions 标志来启用WebAssembly 异常。这将在编译和链接时向编译器传递 -fwasm-exceptions 标志。 Qt 不支持启用 Emscripten 对早期基于 JavaScript 的异常实现的支持。

请注意,由于内部实现细节的原因,在启用异常时不支持调用 `QApplication::exec()`。相反,应按照《应用程序启动与事件循环》中的描述,以早期返回且不调用 `exec()` 的形式编写 `main()` 函数。

共享库与动态链接(技术预览)

Qt for WebAssembly 默认使用静态链接,此时应用程序将作为单个 WebAssembly 文件部署,该文件包含 Qt 库和应用程序代码。动态链接是一种替代的构建模式,其中每个库和插件都会单独分发。

例如,一个使用Qt Quick 的应用程序可能会用到以下库和插件:

  • <qtpath>/lib/libQt6Core.so
  • <qtpath>/lib/libQt6Gui.so
  • <qtpath>/lib/libQt6Qml.so
  • <qtpath>/lib/libQt6Quick.so
  • <qtpath>/plugins/imageformats/libqjpeg.so
  • <qtpath>/plugins/imageformats/libqjgif.so
  • <qtpath>/qml/QtQuick/Window/libquickwindowplugin.so

6.12 版本中的动态链接支持处于技术预览阶段。该实现适用于原型设计和评估,但不适用于生产环境。当前的限制和约束包括:

  • 不支持多线程。
  • 不支持异步化。

使用动态链接构建的 Qt 应用程序需要在二进制文件旁额外放置两个文件:qt_plugins.json 和 qt_qml_imports.json。这些文件指定了将在应用程序启动时加载的共享库列表。 有一个辅助工具可用于生成这些文件:wasmdeployqt。要演示如何使用该工具,您可以使用--help 标志运行它,以了解运行该工具所需的必要标志以及使用示例。

托管该应用程序的 Web 服务器必须具备 Qt 共享库。可通过将 Qt 安装文件夹的内容复制到 Web 服务器,或创建文件系统链接来实现。

快速入门

构建和部署流程与静态 WASM 以及共享桌面构建略有不同。建议先从一个小型示例入手,然后再进行完整应用程序的构建。

  1. 从源代码构建 Qt,并在 Qt 的 configure 脚本中传入-shared 选项。使用 '-prefix' 选项设置安装目录。
  2. 使用步骤 1 中构建的 Qt 来构建您的应用程序。
  3. 在应用程序构建目录中运行部署工具,以创建插件预加载列表。
    • <qtpath>/qtbase/bin/wasmdeployqt --qt-wasm-dir=<Path to the Qt for WebAssembly directory> --qml-root-path=<Root directory for QML files> --qt-host-dir=<Path to the Qt host directory>

共享库部署详解

Qt的共享库构建分为两个阶段进行部署:第一阶段将Qt和应用程序的构建结果发布到Web服务器供下载;第二阶段则在应用程序启动时下载所需的Qt插件和Qt Quick 导入项。

第一步,将 Qt 安装包发布到 Web 服务器供用户下载。根据 Web 服务器配置的具体情况,实现方式可能有所不同。通常,Qt 加载器会期望在相对于加载应用程序的 HTML 文件的“qt”目录下找到 Qt 库和插件。

如果您在部署过程中已将应用程序复制到 Web 服务器,那么一并复制 Qt 也是可行的方案。如果您直接从构建目录提供应用程序(开发阶段通常如此),那么创建指向 Qt 的符号链接会是一个不错的选择。

为第二步做准备,请为 Qt 组件(如插件和Qt Quick 导入)创建预加载列表。预加载可确保应用程序启动时所有必需的 Qt 组件均已就绪。延迟加载(即按需下载组件)也是可行的,但本文不作讨论。

预加载由 Qt JavaScript 加载器实现,该加载器会将文件从 Web 服务器下载到 Emscripten 提供的内存文件系统中。需要下载的文件通过 JSON 格式的下载列表指定。Qt 提供了一个用于生成预加载列表的工具,请参阅上文的“快速入门”部分。

已知问题

  • 仅在使用实验性的 Asyncify 或 JSPI 功能时才支持嵌套事件循环。
  • 不支持打印功能。
  • QDnsLookup 由于 Web 沙箱的限制,域名查询和QSsl 无法正常工作且不受支持。浏览器负责处理 DNS 查询和 SSL 证书。需要进行 DNS 查询的应用程序可以使用“通过 HTTP 传输 DNS”(DNS over HTTP)。
  • 可以使用 QTcpSockets,但并非所有 POSIX 套接字函数都经过代理。需要使用Websockify之类的 WebSocket 服务器代理。
  • 该平台不支持所有 Q*Server 类。
  • QWebSocket Emscripten 仅在主线程上支持连接。
  • WebAssembly 版的 QWebSockets 不支持发送 ping 或 pong 帧,因为网页和浏览器可用的 API 未公开此功能。
  • 若要使用 QtWebSockets,可能需要将子协议设置为“mqtt”以使用QtMqtt 。打开QWebSocket 时,请使用QWebSocketHandshakeOptions 。
  • 字体:Wasm 沙箱不允许访问系统字体。字体文件必须随应用程序一起分发,例如通过 Qt 资源或下载方式。Qt for WebAssembly 本身已内嵌了一款此类字体。
  • 某些Qt Quick Controls 2 组件(如复选框)上可能会出现未初始化的图形内存残留。这种情况有时会在高DPI显示屏上显现。
  • 由于 Wasm 作为平台不提供此功能,因此不支持 Windows 和 macOS 的原生样式
  • 链接时出现的错误(例如“wasm-ld: error: initial memory too small”),需要调整初始内存大小。使用 QT_WASM_INITIAL_MEMORY 以 KB 为单位设置初始大小,该值必须是 64KB(65536)的倍数。 默认值为 50 MB。在 CMakeLists.txt 中:set_target_properties(<target> PROPERTIES QT_WASM_INITIAL_MEMORY "150MB")
  • CMakeLists.txt 中的 add_executable 不会生成 <target>.html 文件,也不会复制 qtloader.js。请改用 qt_add_executable。

其他主题

Qt 配置选项参考

在从源代码构建 Qt for WebAssembly 时,以下 configure 选项相关。

Configure 参数简要说明
-feature-thread多线程 Wasm。
-feature-wasm-simd128启用 WebAssembly SIMD 支持。
-feature-wasm-exceptions启用 WebAssembly 异常支持。
-device-option QT_EMSCRIPTEN_ASYNCIFY=1启用 asyncify 支持。
-device-option QT_EMSCRIPTEN_ASYNCIFY=2启用 asyncify(JSPI)支持。

Qt 默认会禁用 WebAssembly 平台上的某些功能,以减小二进制文件的大小。在配置 Qt for WebAssembly 时,您可以显式启用某项功能:

配置参数简要说明
-feature-topleveldomain提供对检查域名是否为顶级域名的支持。

典型下载大小

预计占用空间(下载大小):编译器生成的 Wasm 模块虽然体积可能较大,但压缩效果良好:

示例gzipbrotli
helloglwindow(QtCore +QtGui )2.8M2.1M
wiggly widget (QtCore +QtGui +QtWidgets)4.3M3.2M
SensorTag(QtCore +QtGui +QtWidgets +QtQuick + QtCharts)8.6M6.3M

压缩通常由 Web 服务器端处理,使用标准的压缩功能:服务器会自动压缩文件,或直接使用文件的预压缩版本。通常无需对 Wasm 文件进行特殊处理。

有关更多信息,请参阅《缩小二进制文件的大小》。

示例

在网页浏览器中运行的 Qt 应用程序示例和演示:Qt 演示

外部资源

许可协议

Qt for WebAssembly 由The Qt Company 提供商业许可。此外,它还遵循GNU 通用公共许可证第 3 版。更多详情请参阅Qt 许可条款。

另请参阅 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS和https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy。

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.