用于 WebAssembly 的 Qt
Qt for Webassembly 可让你在网络上运行 Qt 应用程序。
WebAssembly (缩写为 Wasm)是一种二进制指令格式,用于在虚拟机(如网络浏览器)中执行。
有了 Qt for WebAssembly,你就可以将应用程序作为在浏览器沙盒中运行的网络应用程序发布。这种方法适用于不需要完全访问主机设备功能的网络分布式应用程序。
注意: Qt for WebAssembly 是一个受支持的平台,但某些模块尚未受支持或处于技术预览阶段。请参阅支持的 Qt 模块。
开始使用 Qt for WebAssembly
为 WebAssembly 构建 Qt 应用程序与为其他平台构建 Qt 相似。您需要安装 SDK(Emscripten),安装 Qt(或从源代码构建 Qt),最后构建应用程序。但也有一些不同之处,例如,与其他 Qt 版本相比,WebAssembly 版 Qt 支持的模块和功能较少。
安装 Emscripten
Emscripten是编译为 WebAssembly 的工具链。它能让你在网络上以接近原生的速度运行 Qt,而无需浏览器插件。
Qt 的每个次版本都以特定的 Emscripten 版本为目标,该版本在补丁发布时保持不变。Qt 的二进制包使用目标 Emscripten 版本构建。应用程序应使用相同的版本,因为 Emscripten 并不保证不同版本之间的ABI 兼容性。
6.10.0 支持的 Emscripten 版本是 Emscripten 4.0.7。
有关安装 Emscripten SDK 的更多信息,请参阅 Emscripten文档。
使用emsdk
安装特定的Emscripten
版本。例如,要为 6.10.0 安装,请输入
- ./emsdk install 4.0.7
- ./emsdk activate 4.0.7
安装完成后,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 的主机构建。此外,请在QT_HOST_PATHCMake 变量中或使用-qt-host-path
configure 参数设置主机构建的路径。
./configure -qt-host-path /path/to/Qt -platform wasm-emscripten -prefix $PWD/qtbase
注意: 如果有ninja
可执行文件,configure 总是使用Ninja生成器和构建工具。Ninja 跨平台、功能丰富、性能卓越,建议在所有平台上使用。使用其他生成器可能可行,但未获官方支持。
在 Windows 平台上,请确保PATH
中有 Mingw-w64,并使用以下命令进行配置:
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 支持使用 qmake 和 make 或 CMake 中的 ninja 或 make 来构建应用程序。
$ /path/to/qt-wasm/qtbase/bin/qt-cmake . $ cmake --build .
注意: 当使用 vanillaCMake
(而不是 Linux 上的qt-cmake
或 Windows 上的qt-cmake.bat
)时,请记住使用"-DCMAKE_TOOLCHAIN_FILE "指定工具链文件,就像其他跨平台构建一样。详情请参阅此处:开始使用 CMake。
构建应用程序会生成多个输出文件,包括一个包含应用程序和 Qt 代码(静态链接)的 .wasm 文件、一个可在浏览器中打开以运行应用程序的 .html 文件。
注意: 在"-g "调试级别下,Emscripten 生成的 .wasm 文件相对较大。请考虑使用"-g2 "链接进行调试构建。
运行应用程序
运行应用程序需要网络服务器。编译输出文件都是静态内容,因此任何网络服务器都可以。某些用例可能需要特殊的服务器配置,如提供 https 证书或设置支持多线程所需的 http 标头。
Emrun
Emscripten 为测试运行应用程序提供了emrun工具。Emrun 会启动网络服务器、启动浏览器,还会捕获并转发 stdout/stderr(通常会转发到 JavaScript 控制台)。
/path/to/emscripten/emrun --browser=firefox appname.html
Python http.server
另一种方法是启动开发网络服务器,然后单独启动网络浏览器。最简单的选择之一就是 Python 中的 http.server:
python -m http.server
请注意,这只是一个简单的网络服务器,不支持线程所需的 SharedArrayBuffer,因为不会发送下面提到的 COOP 和 COED 头信息。
qtwasmserver
Qt 提供了一个开发人员网络服务器,它使用mkcert生成 https 证书。这允许测试需要安全环境的网络功能。请注意,通过 http://localhost 传输也被认为是安全的,无需证书。
网络服务器还会将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
在网络上部署应用程序
构建应用程序会生成多个文件(在下表中用应用程序名称代替 "app")。
生成的文件 | 简要说明 |
---|---|
app.html | HTML 容器 |
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 为下载、编译和实例化 Qt for WebAssembly 应用程序提供了 JavaScript API。该加载 API 封装了 Emscripten 提供的加载功能,并为基于 Qt 的应用程序提供了更多有用的功能。它在 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 配置对象支持以下属性:
属性 | 简要说明 |
---|---|
容器元素 | HTML 容器元素数组。应用程序将其视为 QScreens。 |
加载时 | 应用程序加载完成后的回调。 |
退出 | 应用程序退出时的回调。 |
containerElements 数组是 Qt 与网页之间的主要接口,数组中的 html 元素(通常是 <div> 元素)指定了应用程序内容在网页上的位置。
应用程序将每个容器元素视为一个QScreen 实例,并可像往常一样在屏幕实例上放置应用程序窗口。设置了Qt::WindowFullScreen 状态的窗口会使用整个屏幕区域,而非 "全屏 "窗口则会获得窗口装饰。
qtLoad() 函数会返回一个承诺,在等待时会生成一个 Emscripten 实例。该实例提供了对 Embind 导出函数的访问。Qt 导出了多个此类函数,这些函数构成了实例 API。
使用 Qt 实例 API
Qt 提供了多个实例函数。目前,这些函数支持在运行时添加和删除容器元素。
属性 | 简要说明 |
---|---|
qtAddContainerElement | 添加容器元素。添加一个元素将添加一个新的QScreen 。 |
qtRemoveContainerElement | 移除一个容器元素及其对应的屏幕。 |
qtSetContainerElements(设置容器元素 | 设置所有容器元素 |
qtResizeContainerElement(调整容器元素大小 | 让 Qt 接收容器元素大小的变化。 |
移植到 Qt 6.6 qtloader
Qt 6.6 包含一个新的 qtloader,其实现经过简化,范围更小。这包括可能需要移植应用程序 JavaScript 代码的 API 变化。Qt 提供了一个兼容性 API 来简化过渡。根据使用情况的不同,有以下几种前进方式:
- 如果直接使用生成的
app.html
文件,那么该文件也将在构建时更新。无需任何操作。 - 如果您使用的是基本的 qtloader 功能集,那么您可以使用 Qt 6.6 中包含的兼容性 API 作为临时措施。该 API 将在未来的版本中移除;您应计划更新以使用新的 qtloader。需要移植下面的步骤 1。
- 如果您正在使用高级功能(如在运行时添加容器元素),则需要移植到新的加载器或实例 API。需要移植以下步骤 1 和 2。
移植步骤
- 从加载的 html 文件中包含
app.js
(由 Emscripten 生成的 JavaScript 运行时)。<script src="app.js"></script>
在 Qt 6.6 之前,qtloader 会加载并评估该 JavaScript 文件。现在不再这样做了,必须使用 <script> 标签包含该文件。
- 移植到使用新的 JavaScript 和实例 API。
请参见上述文档部分。
支持的浏览器
桌面
Qt for WebAssembly 是在以下浏览器上开发和测试的:
- 浏览器
- 火狐浏览器
- Safari
- 边缘浏览器
如果浏览器支持 WebAssembly,Qt 就能运行。Qt 有一个固定的 WebGL 要求,即使应用程序本身不使用硬件加速图形。支持 WebAssembly 的浏览器通常都支持 WebGL,但有些浏览器会将较旧或不支持的 GPU 列入黑名单。s/qtloader.js提供了检查 WebGL 是否可用的 API。
Qt 并不直接使用操作系统的功能,例如,FireFox 在 Windows 或 macOS 上运行并无区别。Qt 确实使用了一些操作系统适配功能,例如 macOS 上的 ctrl/cmd 键处理。
移动版
Qt for WebAssembly 应用程序可在手机浏览器(如手机 Safari 和 Android Chrome)上运行。
支持的 Qt 模块
Qt for WebAssembly 支持一部分 Qt 模块和功能。下面列出的是经过测试的模块,其他模块可能有效,也可能无效。
- Qt Core
- Qt GUI
- Qt Network
- Qt Widgets
- Qt Qml
- Qt Quick
- Qt Quick Controls
- Qt Quick Layouts
- Qt 5 Core Compatibility APIs
- Qt Image Formats
- Qt OpenGL
- Qt SVG
- Qt WebSockets
- Qt Concurrent
- Qt Charts
- Qt Graphs
以下模块为技术预览版。它们的功能可能有限,或在未来版本中可能会有重大变化。
在所有情况下,模块支持可能并不完整,而且由于浏览器沙盒或 Qt 平台移植的不完整性,可能会有额外的限制。更多信息,请参阅使用 Qt for 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 非常接近,其版本映射如下:
OpenGL | WebGL |
---|---|
OpenGL ES 2.0 | WebGL 1 |
OpengL ES 3.0 | WebGL 2 |
Qt 使用最高可用的 WebGL 版本。在当今的浏览器上,这通常是 WebGL 2,但如果受硬件限制,则可能是 WebGL 1。我们建议使用 Qt for WebAssembly 针对支持 WebGL 2 的设备。
Web 和桌面 OpenGL 的差异记录在WebGL 和 OpenGL Differences 中。WebGL 1.0 与 WebGL 2.0 之间还有其他差异,这些差异记录在《WebGL 2.0 规范》(WebGL 2.0 Specification)中。
默认情况下使用的是 ES2(和 ES3)的 WebGL 友好子集。如果需要使用glDrawArrays
和glDrawElements
而不使用绑定缓冲区,可以通过添加
target_link_options(<your target> PRIVATE -s FULL_ES2=1)
和/或添加
target_link_options(<your target> PRIVATE -s FULL_ES3=1)
到您项目的CMakeLists.txt
。
有关 Emscripten 的 OpenGL 支持的更多信息,请访问https://emscripten.org/docs/porting/multimedia_and_graphics/OpenGL-support.html。
OpenGL 上下文限制
WebGL 不支持每个曲面有多个上下文。这对直接使用QOpenGLContext 或通过其他类(如QOpenGLWidget )间接使用 的应用程序有影响。
每个QOpenGLContext 实例只能用于一个曲面。实际上,上下文会在第一次调用 makeCurrent() 时与曲面关联。不支持随后在不同的曲面上调用 makeCurrent(),也不支持从具有相同曲面的不同QOpenGLContext 中调用 makeCurrent()。
不支持 OpenGL 上下文共享。调用QOpenGLContext::setShareContext() 没有任何效果,而QOpenGLContext::shareContext() 则总是返回 nullptr。
销毁曲面(例如QWindow )会导致相关上下文丢失。应用程序应通过重新创建上下文来处理。
QOpenGLWidget 不支持在内部使用上下文共享的"...... "和其他类。
多线程
Qt for WebAssembly 使用 Emscripten 的Pthreads 支持多线程,每个线程都由一个网络工作者支持。从Qt Maintenance Tool 安装 "WebAssembly(多线程)"组件,或从源代码构建 Qt 并在配置时传递"-feature-thread "标志,即可启用多线程。
现有的线程代码一般可以重复使用,但可能需要修改以适应 pthread 实现的特殊性。Emscripten 和 Qt 的某些功能不受支持,其中包括线程代理功能和Qt Quick 线程渲染循环。
请注意,不要阻塞WebAssembly Qt 的主线程,这一点尤为重要,因为主线程可能需要处理次级线程的请求。例如,Qt 中的所有定时器都是在主线程上调度的,如果主线程被阻塞,定时器就不会启动。另一个例子是,(为线程)创建一个新的 Web Worker 只能在主线程中完成。
Emscripten 对此提供了一些缓解措施。通过忙碌等待和在等待锁时处理事件,可以支持短期等待,例如获取互斥锁。应避免在主线程上进行较长时间的等待。特别是,调用QThread::wait() 或 pthread_join() 来等待辅助线程的常见做法是行不通的,除非应用程序能保证该线程(和网络工作者)已经启动,并且在调用 wait() 或 join() 时无需主线程的协助即可完成。
多线程功能需要浏览器支持SharedArrayBufferAPI。(通常,Emscripten 将堆存储在 ArrayBuffer 对象中。对于多线程功能,堆必须与网络工作者共享,因此需要一个 SharedArrayBuffer)所有现代浏览器一般都支持该 API,但如果不符合某些安全要求,则可能会被禁用。启用了线程支持的 WebAssembly 二进制程序将无法运行,如果二进制程序实际上没有启动线程也是如此。
启用 SharedArrayBuffer 需要一个安全的浏览环境(页面通过 https:// 或 http://localhost 提供),并且页面处于跨源隔离模式。后者可以通过在网络服务器上设置所谓的 COOP 和 COEP 标头来实现:
- 跨源生成策略:同源
- 跨源代理策略: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 Network 提供了有限的网络支持。一般来说,网络上已在使用的网络协议也可以通过 Qt 使用,而其他网络协议由于网络沙盒的原因不能直接使用。
支持以下协议:
- QNetworkAccessManager 向网页源服务器或支持 CORS 的服务器发出 http 请求。这包括来自 QML 的XMLHttpRequest 。
- QWebSocket 与任何主机的连接。请注意,通过安全 https 协议提供的网页只允许通过安全 wss 协议进行 webockets 连接。
- 通过 WebSockets 模拟 POSIX TCP Sockets,使用Emscripten 提供的功能。请注意,这需要运行一个转发服务器来处理套接字转换。
不支持所有其他网络协议。
注意: 由于浏览器的限制,不支持QWebSocketServer 。浏览器限制服务器端套接字功能,以确保网络沙盒的安全性。因此,任何依赖QWebSocketServer 接受传入网络连接的功能都无法在网络环境中使用。
注意: QtMqtt 和QtRemoteObjects 模块可与QtWebSockets 一起用作传输。它们不受官方支持,可能正常工作,也可能无法工作,或功能缺失。
跨源资源共享(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 一起使用,将导致错误。
如果使用 -cross-origin-isolation 选项,随附的实用程序脚本 qtwasmserver.py 将设置这些标头。
本地文件访问
网络上的文件系统访问是沙箱式的,这对应用程序如何处理文件有影响。网络平台提供了在用户控制下访问本地文件系统的 API,以及访问持久存储的 API。Emscripten 和 Qt 封装了这些功能,并提供了更易于从基于 C++ 和 Qt 的应用程序中使用的 API。
网络平台提供了访问本地文件和持久存储的功能:
- <input type="file"> 用于显示本地打开文件对话框,用户可以在其中选择文件。
- IndexedDB 提供持久本地存储(浏览器外无法访问)
Emscripten 提供了几种具有类似 POSIX API 的文件系统。这些系统包括
- 在内存中存储文件的 MEMFS 暂存文件系统
- 使用 IndexedDB 存储文件的 IDBFS 持久文件系统
Emscripten 会在程序启动时将临时 MEMFS 文件系统挂载到"/"。这意味着可以使用QFile ,默认情况下会在内存中读写文件。浏览器重新加载时不会保留它。
由于网页无法直接访问本地文件系统,Qt 提供了QFileDialog API,供 Qt for WebAssembly 使用。
- QFileDialog::getOpenFileContent() 打开一个本地文件对话框,用户可以在其中选择一个文件
- QFileDialog::saveFileContent()通过文件下载将文件保存到本地文件系统中
剪贴板访问
Qt 支持使用系统剪贴板复制和粘贴文本、url、已知文件类型和图像,但由于网络沙盒的原因,这些功能存在一些差异。它不支持任意应用程序/八进制流二进制数据。一般来说,剪贴板访问需要用户许可,可通过处理输入事件(如 CTRL+c)或使用剪贴板 API 获得。
最好使用支持剪贴板 API 的浏览器。需要注意的是,使用该 API 的前提条件是网页是通过安全连接(如 https)提供的,而且某些浏览器可能需要更改配置标志。
- Chrome 浏览器 66 版和 Safari 13.1 版支持剪贴板 API
- 如果在 "about:config "中启用以下标志,Firefox 90 版本将支持剪贴板 API:
dom.events.asyncClipboard.read dom.events.asyncClipboard.clipboardItem
字体
Qt WASM 模块包含 3 种嵌入式字体:"Bitstream Vera Sans"(后备字体)、"DejaVu Sans "和 "DejaVu Sans Mono"。
这些字体提供了有限的字符集。Qt 提供了几种添加其他字体的选项:
一种是在 QML 中使用FontLoader ,它可以通过 URL 或使用Qt 资源系统(The Qt Resource System)(与通常桌面应用程序的工作方式相同)获取字体。
另一种方法是通过QFontDatabase::addApplicationFontFromData 添加字体。
可访问性和屏幕阅读器
Qt for WebAssembly 为屏幕阅读器提供了基本支持。按钮和复选框等简单的用户界面元素可以正常工作,而表格或树视图等更复杂的用户界面元素则可能缺少支持。Qt Widgets 和Qt Quick 都支持。
以下屏幕阅读器/浏览器配置已经过测试,已知可以正常使用。其他浏览器和屏幕阅读器也可能正常工作。
- MacOS 上使用 Safari 的 VoiceOver
- MacOS 上使用 Chrome 浏览器的 VoiceOver
辅助功能通过创建 "shadow"(阴影)html 元素来实现,这些元素可为 Qt UI 元素提供辅助信息。该功能默认为禁用。最终用户可以使用屏幕阅读器选择 "激活屏幕阅读器 "按钮来激活它。激活后,该功能将在网页中填充无障碍元素。
应用程序启动和事件循环
Qt for WebAssembly 支持标准的 Qt 启动方法,即应用程序创建一个QApplication 对象并调用执行函数:
int main(int argc, char **argv) { QApplication app(argc, argv); QWindow appWindow; return app.exec(); }
上述 exec() 调用通常会阻塞并处理事件,直到应用程序关闭。遗憾的是,这在网络平台上是不可能的,因为网络平台不允许阻塞主线程。相反,必须在处理完每个事件后将控制权返回给浏览器的事件循环。
Qt 通过让 exec() 将主线程控制权返回浏览器来解决这个问题,同时保留堆栈。从应用程序代码的角度来看,exec() 函数被输入,事件处理照常进行。不过,exec() 调用永远不会返回,也不会在应用程序退出时返回。
这种行为通常是可以接受的,因为浏览器会在应用程序关闭时释放应用程序内存。这确实意味着关机代码无法运行,因为应用程序对象被泄露,其析构函数也不会运行。
您可以通过重写 main() 使其异步来避免这种情况,因为当 main() 返回时,Emscripten 不会退出运行时。这样,应用程序代码就无需调用 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
由于网络平台的限制,同步 C++ 代码无法调用异步 JavaScript API,因此 WebAssembly 的默认 Qt 构建不支持重新进入事件循环,如调用QEventLoop::exec() 或QDialog::exec() 。
需要使用 asyncify/JSPI 的功能包括
- 带返回值的 QDialogs、QMessageBoxes。
- 拖放(特别是拖动)。
- 嵌套/辅助事件循环 exec()。
Emscripten 支持使用Asyncify功能绕过这些限制。该功能有两个版本:
- Asyncify,通过 WebAssembly 代码后处理步骤实现。
- JSPI,使用 WebAssemblyJS Promise Integration功能实现。
有关如何启用每个选项以及所涉及的权衡的详细信息,请参阅下面的章节。简而言之,Asyncify 目前可用,但会带来开销,表现为更长的构建时间、更大的二进制文件大小和运行时性能成本。JSPI 的开销极小,甚至没有开销,但并非所有浏览器都支持它。
同步化
在链接器选项中添加"-sASYNCIFY -Os "标记,启用 asyncify:
CMake:
target_link_options(<your target> PUBLIC -sASYNCIFY -Os)
qmake:
QMAKE_LFLAGS += -sASYNCIFY -Os
启用 asyncify 会增加二进制文件大小和 CPU 占用率。启用优化后再编译可将开销降至最低。
JSPI(JS 承诺集成)
与 Asyncify 不同,使用 JSPI 需要从源代码构建 Qt。向 Qt 配置脚本传递 -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 上的应用程序。
- Qt 调试和日志输出打印在 JavaScript 控制台上,可通过浏览器 "开发工具 "或类似工具访问。
- 使用 -device-option QT_WASM_SOURCE_MAP=1 重新配置 Qt 并构建调试版本,即可创建用于逐步浏览代码的源代码映射。
- 如果使用 -g 标志链接程序,还可通过 DWARF 启用调试符号(在 Chrome 浏览器上进行了测试)
- 这需要以下扩展: https://goo.gle/wasm-debugging-extension
- 另请参见https://developer.chrome.com/blog/wasm-debugging-2020/
- 移动浏览器可使用远程调试
- 要在某一行停止执行并以编程方式弹出浏览器调试器,可以在应用程序源代码中添加函数 emscripten_debugger();。
- 可以使用调试构建和 JavaScript 控制台剖析功能来完成剖析。在调试构建中,Qt 会在链接器参数中添加 -profiling-funcs,从而在剖析中保留函数名称
您还可以使用 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 二进制文件的大小。对于 WebAssembly 平台,Qt 默认禁用某些功能,但你也可以禁用应用程序不使用的功能。更多信息,请参阅禁用的功能。
你可以禁用以下功能来减少二进制文件的大小(通常减少 10-15%):
配置 参数 | 简要说明 |
---|---|
-no-feature-cssparser(无特性-层叠样式表解析器 | 层叠样式表解析器。 |
-no-feature-datetimeedit(无特性-日期时间编辑器 | 编辑日期和时间(取决于 datetimeparser)。 |
-no-feature-datetimeparser(数据时间解析器 | 解析日期时间文本。 |
-no-feature-dockwidget | 将小工具停靠在QMainWindow 中,或将其作为顶层窗口浮动到桌面上。 |
-无特征手势 | 手势框架。 |
-无特性-mimetype | 模拟类型处理。 |
-无特性-qml-网络 | 网络透明度。 |
-no-feature-qml-list-model(无特征 QML 列表模型 | ListModel QML 类型。 |
-QML 类型。 | TableModel QML 类型。 |
-no-feature-quick-canvas(无特性快速画布 | 画布项目。 |
-no-feature-quick-path(无特征快速路径 | 路径元素。 |
-no-feature-quick-pathview(无特性快速路径视图 | PathView 项目。 |
-no-feature-quick-treeview(无特效快速路径视图 | TreeView 项目。 |
-no-feature-style-stylesheet(无特性样式表 | 可通过 CSS 配置的小工具样式。 |
-no-feature-tableview(无特性表格视图 | 表格视图的默认模型/视图实现。 |
-no-feature-texthtml解析器 | HTML 解析器。 |
-无特性-textmarkdown 阅读器 | Markdown (CommonMark 和 GitHub)阅读器。 |
-无特性-textodfwriter | ODF 写入器。 |
Wasm 异常
Qt 在构建时默认不支持异常,抛出异常会中止程序。可以通过从源代码构建并向 Qt configure 传递 -feature-wasm-exceptions 标志来启用WebAssembly 异常。这将在编译和链接时将 -fwasm-exceptions 标志传递给编译器。Qt 不支持启用 Emscripten 对早期基于 JavaScript 的异常实现的支持。
请注意,由于内部实现细节的原因,启用异常时不支持调用QApplication::exec() 。相反,请按照应用程序启动和事件循环中的描述,以提前返回且不调用 exec() 的形式编写 main()。
共享库和动态链接开发人员预览版
Qt for WebAssembly 默认使用静态链接,即应用程序以包含 Qt 库和应用程序代码的单个 WebAssembly 文件的形式部署。动态链接是另一种构建模式,其中每个库和插件都是单独发布的。
例如,一个使用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
动态链接支持目前处于开发人员预览阶段。该实现适合原型开发和评估,但不适合生产使用。当前的限制和约束包括
- 不支持多线程。
- 不支持异步化。
使用动态链接构建的 Qt 应用程序需要与二进制文件同时存在的两个附加文件:qt_plugins.json 和 qt_qml_imports.json。这些文件指定了将在应用程序启动时加载的共享库列表。有一些辅助脚本可用于生成这些文件:preload_qt_plugins.py 和 preload_qml_imports.py。为了演示如何使用这些脚本,我们将提供一个名为 generate_default_preloads_for_<target>.sh 的辅助脚本。
托管应用程序的网络服务器必须有可用的 Qt 共享库。这可以通过将 Qt 安装文件夹的内容复制到网络服务器,或创建文件系统链接来实现。
快速启动
构建和部署程序与静态 wasm 和共享桌面构建略有不同。在进行完整的应用程序构建之前,可考虑先从一个小示例开始。
- 从源代码构建 Qt,向 Qt 配置脚本传递"-shared "选项。使用"-prefix "选项设置安装目录。
- 使用步骤 1 中的 Qt 构建应用程序。
- 通过复制或链接到应用程序目录中名为 "qt "的目录,部署 Qt 安装程序
- ln -s <qtpath> qt
- cp -r <qtpath> qt
- 通过运行部署脚本创建插件预加载列表。
- <qtpath>/qtbase/util/wasm/preload/preload_qt_plugins.py <qt_install_dir> <target_dir>
- <qtpath>/qtbase/util/wasm/preload/preload_qml_imports.py <app_source dir> <qt_host_dir> <qt_install_dir> <target_dir>
共享库深度部署
Qt 的共享库构建分两个阶段部署,第一阶段使 Qt 和应用程序构建可从 Web 服务器下载,第二阶段在应用程序启动时下载所需的 Qt 插件和Qt Quick 导入。
第一步,从网络服务器下载 Qt 安装。根据网络服务器设置的具体情况,可能有不同的方法来实现这一点。常见的是,Qt 加载器希望在加载应用程序的 html 文件相对应的名为 "qt "的目录中找到 Qt 库和插件。
如果作为部署的一部分,您已将应用程序复制到网络服务器,那么复制 Qt 也是一种可能的选择。如果您直接从构建目录为应用程序提供服务(通常是在开发阶段),那么创建一个指向 Qt 的符号链接也是不错的选择。
为第二步做准备,为插件和Qt Quick 导入等 Qt 组件创建预加载列表。预加载可确保所有需要的 Qt 组件在应用程序启动时可用。延迟加载,即按需下载组件,也是可行的,但这里不涉及。
预加载由 Qt JavaScript 加载器实现,它将文件从网络服务器下载到 Emscripten 提供的内存文件系统。使用 json 格式的下载列表可指定要下载的文件。Qt 提供了两个用于生成预加载列表的脚本,请参见上文的快速入门部分。
已知问题
- 不支持嵌套事件循环。应用程序不应调用QDialog::exec() 和QEventLoop::exec() 等 API。可以使用试验性功能 Asyncify。
- 不支持打印。
- QDnsLookup 由于网络沙盒的原因,不支持查找和QSsl 。浏览器可处理 DNS 查询和 SSL 证书。需要 DNS 查询的应用程序可使用 DNS over HTTP。
- 可以使用 QTcpSockets,但并非所有 POSIX 套接字功能都能代理。需要使用Websockify这样的 Websockets 服务器代理。
- 平台不支持所有 Q*Server 类。
- QWebSocket Emscripten 仅在主线程上支持连接。
- WebAssembly 的 QWebSockets 不支持发送 ping 或 pong 框架,因为网页和浏览器可用的 API 并不公开这一功能。
- 要使用 QtWebsockets,可能需要将子协议设置为 "mqtt",以便使用QtMqtt 。打开QWebSocket 时使用QWebSocketHandshakeOptions 。
- 字体:Wasm 沙盒不允许访问系统字体。字体文件必须与应用程序一起发布,例如在 Qt 资源或下载中。Qt for WebAssembly 本身就嵌入了这样一种字体。
- 在某些Qt Quick Controls 2 组件(如复选框)上可能会出现未初始化图形内存的假象。在 HighDPi 显示器上有时会出现这种情况。
- 不支持 Windows 和 macOS 的本地样式,因为 Wasm 作为一个平台不提供该功能。
- 链接时间错误,如 "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 相关。
配置参数 | 简要说明 |
---|---|
-特性-线程 | 多线程 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 平台的某些功能,以减小二进制文件的大小。在为 WebAssembly 配置 Qt 时,可以显式启用某个功能:
配置参数 | 简要说明 |
---|---|
-特性-人民选择域 | 为检查域是否为顶级域提供支持。 |
典型下载大小
预期占用空间(下载大小):编译器生成的 Wasm 模块可能较大,但压缩效果良好:
例如 | gzip | brotli |
---|---|---|
helloglwindow (QtCore +QtGui) | 2.8M | 2.1M |
wiggly widget (QtCore +QtGui + QtWidgets) | 4.3M | 3.2M |
SensorTag (QtCore +QtGui + QtWidgets +QtQuick + QtCharts) | 8.6M | 6.3M |
压缩通常在网络服务器端处理,使用标准压缩功能:服务器自动压缩或拾取文件的预压缩版本。通常不需要对 Wasm 文件进行特殊处理。
更多信息,请参阅最小化二进制文件大小。
示例
在网络浏览器中运行 Qt 应用程序的示例和演示:Qt 演示
外部资源
许可证
Qt for WebAssembly 可在The Qt Company 的商业许可证下使用。此外,它还采用GNU 通用公共许可证第 3 版。更多详情,请参阅Qt Licensing。
另请参阅 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS和https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy。
© 2025 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.