Qt:靠谱点了!基于WebAssembly开发网页端

Qt:靠谱点了!基于WebAssembly开发网页端

序言

Qt for WebAssembly,是Qt在2018年发布的技术,于5.12加入到Qt,官方对此技术介绍如下:

https://www.qt.io/blog/2018/05/22/qt-for-webassembly

简单的说,这是一个让Qt程序可以直接跑在web中的一个方法,具体流程如下:

使用Emscripten作为platfrom静态编译Qt工程,把整个工程和Qt环境打包编译成wasm可执行文件,配合html套壳一起加载到浏览器中,然后浏览器会提供一个虚拟化环境运行wasm,程序运行起来后所有的图形结果通过一个canvas输出。

相比之前WebGL技术这样的远程运行技术,这一次WebAssembly是真的把Qt程序跑在了浏览器本地上,实现了性能,效果的保证。总体靠谱得多。

关于WebAssembly详细描述和资料,这里不再累述,请直接参考Qt官方文档:

如果你想真的通过WebAssembly开发程序,我也建议务必看完以下链接里所有资料,这会帮你节约很多时间

https://wiki.qt.io/Qt_for_WebAssembly

https://www.qt.io/cn/blog/2019/01/18/getting-started-qt-webassembly

https://www.youtube.com/watch?v=W3WC-VpKdGQ&t=1319s

作为一个面世2年的技术,这项技术已经被多位前辈介绍,但是大多都是草草带过没有涉及很多细节,因此本文会相对整体介绍,看完本文后可以构建一个比较满意的工程。


环境准备

系统:Ubuntu 18.04 64bit

注:我是在虚拟机里面安装的是原版系统,并且进行了常规更新(update/upgrade),我建议为了避免奇怪的环境问题,尽量使用虚拟机安装环境,虽然Qt支持Windows和macOS下使用WebAssembly,但是我不建议在这两个系统下操作。

注2:请不要过多担心系统问题,因为编译后的产物例如wasm、html,都是跨平台的,无所谓是通过哪个系统编译出来的。

Qt源码:qt-everywhere-src-5.14.2.tar.xz

源码下载地址: http://download.qt.io/archive/qt/5.14/5.14.2/single/

环境配置参数

sudo apt-get install vim git
sudo apt-get install gcc g++ make libgl1-mesa-dev libglu1-mesa-dev libssl1.0-dev
sudo apt-get install libssl-dev
sudo apt-get install libfontconfig1-dev libfreetype6-dev libx11-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libx11-xcb-dev libxcb-glx0-dev libxkbcommon-x11-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev libxcb-sync0-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-render-util0-dev
sudo apt-get install python
sudo apt-get install openjdk-8-jdk

Emscripten配置

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install sdk-fastcomp-1.38.30-64bit
./emsdk activate sdk-fastcomp-1.38.30-64bit
source ./emsdk_env.sh
em++ --version

注:Emscripten和Qt是有版本对应关系,版本不匹配可能导致编译失败以及运行时奇怪问题,具体对应关系请参考本文开头发的参考链接。我使用的 Qt5.14.2 配套 Emscripten1.38.27 Emscripten1.38.30

注2:source ./emsdk_env.sh是每次打开终端后必须执行的,他会初始化Emscripten环境

Qt编译参数

xz -d ./qt-everywhere-src-5.14.2.tar.xz
tar -xvf ./qt-everywhere-src-5.14.2.tar
cd qt-everywhere-src-5.14.2
./configure -prefix ~/Develop/qt5.14.2_web -confirm-license -opensource -xplatform wasm-emscripten          -nomake tools -nomake examples -nomake tests -skip qt3d -skip qtcharts -skip qtdatavis3d -skip qtdoc -skip qtgamepad -skip qtlocation -skip qtlottie -skip qtmacextras -skip qtmultimedia -skip qtnetworkauth -skip qtpurchasing -skip qtquick3d -skip qtremoteobjects -skip qtscxml -skip qtsensors -skip qtserialbus -skip qtserialport -skip qtspeech -skip qtsvg -skip qttools -skip qtvirtualkeyboard -skip qtwayland -skip qtwebchannel -skip qtwebengine -skip qtwebglplugin -skip qtwebview -skip qtwinextras          -no-feature-accessibility -no-feature-appstore-compliant -no-feature-big_codecs -no-feature-calendarwidget -no-feature-colordialog -no-feature-d3d12 -no-feature-filesystemwatcher -no-feature-fontcombobox -no-feature-fontdialog -no-feature-ftp -no-feature-imageformat_xbm -no-feature-lcdnumber -no-feature-library -no-feature-multiprocess -no-feature-pdf -no-feature-printdialog -no-feature-printpreviewdialog -no-feature-printpreviewwidget -no-feature-qml-debug -no-feature-qml-devtools -no-feature-qml-worker-script -no-feature-qml-xml-http-request -no-feature-quick-designer -no-feature-sha3-fast -no-feature-sharedmemory -no-feature-socks5 -no-feature-udpsocket -no-feature-whatsthis -no-feature-wizard -no-feature-xml-schema -no-feature-xmlstreamwriter
make -j6
make install

注:我的配置参数特别长,因为我对Qt进行了裁剪,这个裁剪能缩减大约1mb的wasm文件体积,如果你觉得没必要可以去掉 -no-feature 开头的那批参数

注2:相比编译桌面版Qt,WebAssembly版的Qt程序HTTPS请求依托浏览器环境,因此这里不需要配置ssl相关参数。也就是说编译时不需要配置ssl环境就可以用HTTPS

使用

若一切成功,依次执行 qmake -v em++ -v 应该可以看到以下信息


编译工程

和编译普通Qt工程一样,切换到源码目录,直接执行这2个命令就行了

qmake
make -j3

但是因为WebAssembly和静态编译原因,编译会特别慢,一个简单的工程会需要编译几分钟,这里耐心等待就行了

编译好后,我们一共需要从工程目录中提取5个文件,如下:

注:我的工程名为 test

qtloader.js
qtlogo.svg
test.html
test.js
test.wasm

其中 test.js test.wasm 是对应Qt工程的主要产物,每次Qt工程重新编译后都要更新这2个文件,这两个文件也不建议进行二次修改,直接用就行了。

如果要对页面进行美化,直接修改 test.html 就行了

因为不支持直接从文件打开,所以如果要对页面进行访问,最简单的方法就是用python开启一个web服务器

python3 -m http.server

然后在浏览器(例如Ubuntu自带的火狐)中直接打开 http://127.0.0.1:8000/test.html 就能看到程序了

我这里有一个已经挂在网上的WebAssembly程序,可以直接打开看下效果

https://web.jasonserver.com:10035/test/test.html


优化

截止上一步,一个简单的web程序就构建完成了,但是我们还有几步完善过程要走

中文字体裁剪

目前Qt for WebAssembly这边有一个缺陷就是无法使用浏览器或者说客户宿主机的字体,简单的数字和英文Qt有处理可以显示但是中文全部是小方块。根据静态编译的思路我们也不太会把整个中文字体都打包到资源文件里,毕竟中文字体基本都10mb+,就算打包进去加载速度也会被大幅度拖慢。因此我们需要对字体进行裁剪,我使用的是一个在线裁剪工具,配合常用2000字汉字表。

https://font-subset.disidu.com
https://wenku.baidu.com/view/3d2bd02b453610661ed9f40a.html

注:如果百度不让你复制,一个最简单的破解方法就是打开页面后禁用页面JS,然后再复制,就行了

我裁剪了 SourceHanSansCN-Medium 字体,裁剪前10mb+,裁剪后才300多kb,打包进资源文件完全可以接受

然后如果要在Qt里支持这个字体,在main函数里增加一行代码就行了

QFontDatabase::addApplicationFont( ":/SourceHanSansCN-Medium.subset.otf" );

压缩

为什么要压缩,因为wasm文件真的有点大。我这里一个简单工程编译出来就有18.2mb,这个对于一个web而言有点大了,在实际网络环境中可能需要10秒左右才能加载完成。

至于如何压缩,这个东西就有点坑了,最初我不熟悉web的压缩体系看了半天没看懂,只看官方说可以压缩却没说怎么压缩,最后才知道这个是WebServer自带功能,不需要开发者手动压缩

比如说我们可以使用nginx部署web,在nginx配置文件中配置开启glib压缩,我的配置如下:

gzip on;
gzip_min_length 32k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 3;
gzip_types text/plain text/css text/xml application/xml application/json text/javascript application/javascript application/octet-stream;

然后这个压缩就全自动的完成了。

大致流程就是浏览器在请求资源文件的时候会说明自己支持的压缩方式,然后服务器如果可以匹配到这个压缩方式就会压缩好数据再返回给浏览器。

可以看到开启glib压缩后,18.2mb下降到了7.3mb,还是比较可观的,缩减了几倍的网络传输消耗

如果追求更高压缩率可以使用brotli,本文不再介绍。注意这个压缩方法不是每个浏览器都支持,比如说Safari就不支持

ico

ico这个东西很多地方都用得到,web这里需要一个favicon.ico建议还是准备下

ico建议弄多分辨率版本,推荐工具gfie

我在html中配置ico方式为:

<link rel="shortcut icon" type="image/ico" href="favicon.ico">
<link rel="apple-touch-icon" href="favicon.ico">
<link rel="icon" type="image/ico" href="favicon.ico">

PWA

现在Web已经可以做的和应用程序一样,在桌面有一个小图标,进入后只有本身的页面没有浏览器框架,这依托于PWA标准

具体参考如下:

https://developer.mozilla.org/zh-CN/docs/Web/Manifest

简单的说这里只要一个 manifest.json 配置文件,我的配置如下:

{
    "name": "Test",
    "short_name": "Test",
    "start_url": "test.html",
    "display": "standalone",
    "background_color": "#000e27",
    "theme_color": "#000e27",
    "description": "Qt for WebAssembly Test",
    "icons": [{
      "src": "favicon.ico",
      "sizes": "256x256 64x64 48x48 32x32 24x24 16x16",
      "type": "image/x-icon"