用于 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,而无需浏览器插件。
有关安装 Emscripten SDK 的更多信息,请参阅 Emscripten文档。
安装完成后,你的路径中应该有 Emscripten 编译器。请使用以下命令检查:
em++ --version
Qt 的每个次版本都以特定的 Emscripten 版本为目标,该版本在补丁发布时保持不变。Qt 的二进制包使用目标 Emscripten 版本构建。应用程序应使用相同的版本,因为 Emscripten 并不保证不同版本之间的ABI 兼容性。
Emscripten 版本如下
- Qt 6.2: 2.0.14
- Qt 6.3: 3.0.0
- Qt 6.4: 3.1.14
- Qt 6.5: 3.1.25
- Qt 6.6: 3.1.37
- Qt 6.7: 3.1.503.1.50
- Qt 6.8: 3.1.56
- Qt 6.9: 3.1.70
使用emsdk
安装特定的Emscripten
版本。例如,要为 Qt 6.8 安装,请输入
- ./emsdk install 3.1.70
- ./emsdk activate 3.1.70
在 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绑定到所有可用地址。
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 布局
- Qt 5 Core Compatibility APIs
- Qt Image Formats
- Qt OpenGL
- Qt SVG
- Qt WebSockets
在所有情况下,模块支持可能并不完整,而且由于浏览器沙盒或 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 | WebGL 1 |
OpengL ES 3 | 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
。
共享 OpenGL 上下文
WebGL 只允许每个本地表面使用一个上下文,不支持 OpenGL 上下文共享。Qt 通过在使用多个 OpenGLContext 时重复使用本地上下文来解决这一问题。
我们建议将 OpenGL 上下文的使用限制为每个本地表面一个上下文,例如使用QOpenGLWindow 而不是QOpenGLWidget 。QOpenGLWidget 也可以工作,前提是应用程序代码在调用QOpenGLWidget::paintGL() 时恢复所有相关的 OpenGL 状态,并且不依赖于QOpenGLWidget::initializeGL() 和QOpenGLWidget::paintGL() 调用之间的状态持久化。
多线程
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 接受传入网络连接的功能都无法在网络环境中使用。
本地文件访问
文件系统访问在网络上是沙箱式的,这对应用程序如何处理文件有影响。网络平台提供了在用户控制下访问本地文件系统的 API,以及访问持久存储的 API。Emscripten 和 Qt 对这些功能进行了封装,并提供了更易于从基于 C++ 和 Qt 的应用程序中使用的 API。
网络平台提供了访问本地文件和持久存储的功能:
- <input type="file"> 用于显示本地打开文件对话框,用户可以在其中选择文件。
- IndexedDB 提供持久本地存储(浏览器外无法访问)
Emscripten 提供了几种具有类似 POSIX API 的文件系统。这些系统包括
- 在内存中存储文件的 MEMFS 暂存文件系统
- 使用 IndexedDB 存储文件的 IDBFS 持久文件系统
Emscripten 会在程序启动时将临时 MEMFS 文件系统挂载到"/"。这意味着可以使用QFile ,默认情况下会在内存中读写文件。Qt 还提供了其他 API:
- QFileDialog::getOpenFileContent() 打开一个本地文件对话框,用户可以在其中选择一个文件
- QFileDialog::saveFileContent()通过文件下载将文件保存到本地文件系统中
剪贴板访问
Qt 支持将文本复制和粘贴到系统剪贴板,但由于网络沙盒的原因会有一些差异。一般来说,剪贴板访问需要用户权限,可通过处理输入事件(如 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 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; }
异步化
由于网络平台的限制,Qt for WebAssembly 的默认构建不支持重新进入事件循环,例如调用QEventLoop::exec() 或QDialog::exec() 。
Emscripten 的asyncify功能允许同步调用(如QEventLoop::exec() 和QDialog::exec() )让位于事件循环,从而解除了这些限制。不支持嵌套调用,因此在顶级QApplication::exec() 调用中不使用 asyncify。
需要 asyncify 的功能有
- 带有返回值的 QDialogs、QMessageBoxes。
- 拖放(特别是拖动)。
- 嵌套/辅助事件循环 exec()。
通过在链接器选项中添加"-sASYNCIFY -Os "标志启用异步化:
CMake:
target_link_options(<your target> PUBLIC -sASYNCIFY -Os)
qmake:
QMAKE_LFLAGS += -sASYNCIFY -Os
启用 asyncify 会增加二进制文件大小和 CPU 占用率。启用优化后再编译可将开销降至最低。
Asyncify JSPI(JavaScript 承诺集成)
JSPI是一种新的 asyncify 后端,使用本地浏览器支持而不是代码转换来实现。与 Emscripten asyncify 相比,它能缩短应用程序的链接时间,而且不会增加代码大小。
JSPI WebAssembly 功能目前处于 "实施 "阶段,尚未标准化。
通过在链接器选项中添加"-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++ 应用程序的标志一样: qmake
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
动态链接支持目前处于开发人员预览阶段。该实现适合原型开发和评估,但不适合生产使用。当前的限制和约束包括
- Emscripten SDK 必须打补丁。使用 emsdk 3.1.70,并应用此补丁:patch#1。
- 不支持多线程。
- 不支持 Asyncify。
使用动态链接构建的 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/deploy_qt_plugins.py <qt_install_dir> <target_dir>
- <qtpath>/qtbase/util/wasm/preload/deploy_qml_imports.py <app_source dir> <qt_host_dir> <qt_install_dir> <target_dir>
共享库深度部署
Qt 的共享库构建分两个阶段部署,第一阶段从 Web 服务器下载 Qt 和应用程序构建,第二阶段在应用程序启动时下载所需的 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 由于网络沙盒的原因,不支持查找、 、 。QTcpSocket QSsl
- 字体: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。
- QWebSocket Emscripten 仅在主线程上支持连接。
- WebAssembly 的 QWebSockets 不支持发送 ping 或 pong 框架,因为网页和浏览器可用的 API 并不公开此功能。
- 运行时错误,如 "RangeError:内存不足 "之类的运行时错误,可以通过将 MAXIMUM_MEMORY 设置为设备支持的值来解决,例如
target_link_options(<your target> PRIVATE -s MAXIMUM_MEMORY=1GB)
- 要使用 QtWebsockets,可能需要将子协议设置为 "mqtt",以便使用QtMqtt 。打开QWebSocket 时使用QWebSocketHandshakeOptions 。
其他主题
Qt 配置选项参考
以下配置选项与从源代码构建 Qt for WebAssembly 相关。
配置参数 | 简要说明 |
---|---|
-特性-线程 | 多线程 Wasm。 |
-feature-wasm-simd128 | 启用 WebAssembly SIMD 支持。 |
-feature-wasm-exceptions | 启用 WebAssembly 异常支持。 |
-feature-opengles3 | 除默认的 opengles2 外,使用 opengles3。 |
-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 for WebAssembly 可在The Qt Company 的商业许可证下使用。此外,它还采用GNU 通用公共许可证第 3 版。更多详情,请参阅Qt Licensing。
© 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.