简介:面向对Web端图形用户界面开发感兴趣的C++开发者,这一示例项目展示了将ImGui(即时模式GUI)完整带入浏览器的可行做法。项目基于WebGL、GLFW与ImGui,借助Emscripten将C++源码编译为WebAssembly(WASM)二进制,并结合FreeType处理字体渲染,在网页中呈现可交互的ImGui界面。资源共14个文件,涵盖C++源文件、头文件、WASM与JS编译产物、HTML宿主页面、字体文件、界面数据文件以及Makefile构建脚本,结构紧凑,压缩包仅431KB,可直接作为轻量级浏览器端GUI开发的起点。目前已有1599人浏览学习,适合想入门WASM工程、研究ImGui浏览器移植或搭建轻量级WebGUI的开发者。借助这个最小实现,读者能够直观看到ImGui、GLFW与WebGL在浏览器中的协作关系,理解从C++到WASM的编译流程,也可以直接打开HTML查看实际渲染效果,为后续扩展自定义控件、事件响应与互动逻辑提供清晰参考。
1. WebGui 想解决的问题:把 IMGUI 桌面工具原样搬进浏览器
最早接触这类 WebGui 方案,是因为要给一个图形工具做在线演示版。当时摆在面前的两条路:用前端框架重写一套 UI,或者把已有的 Dear ImGui 界面编译成 WASM 放进浏览器。前者要改交互、改布局、改状态管理,工作量按周算;后者只改一个主循环,工作量按小时算。这个标题里的组合——IMGUI + GLFW + WebGL + WASM,恰好就是一条把 C++ 桌面工具直接搬到浏览器里的最短路径:IMGUI 负责每帧生成界面,GLFW 在 Web 端模拟窗口与输入,WebGL 做最终渲染,WASM 提供运行时。适合谁?适合手里已经有一套 Dear ImGui 的工具、想低成本出 Web 版,或者想用 C++ 写跨端调试面板的开发者。下面按我实际搭建的顺序,把这套东西从原理到踩坑讲清楚。
2. 技术底座拆解:IMGUI 为什么能在 WASM 里跑得这么自然
2.1 保留模式与即时模式:浏览器里的 DOM 反而是异类
先说结论:IMGUI 这类即时模式 GUI,和浏览器底层的渲染模型其实更接近,和你平时写网页用的 DOM 反而不一样。网页是保留模式——你创建一个按钮节点,它就一直存在,每次改动要走 DOM diff、样式重算、合成。IMGUI 没有常驻的控件对象,每帧在代码里重新执行一遍界面描述:ImGui::Begin("面板")、"ImGui::Button("确定")",这些调用会往内部命令列表里追加绘制指令,这一帧用完后,按钮对象就作废了。下一帧你再次调用同样的代码,按钮才再次出现。
这套模型放到 WebGL 上非常顺。因为 WebGL 本来就是每帧清屏重绘,IMGUI 每帧生成的绘制列表可以直接转换成drawArrays/drawElements调用,中间不需要维护任何 DOM 节点。反过来想,如果你把 IMGUI 的思路搬到 HTML 里,每帧重建 DOM 节点再销毁,那性能没法看。所以标题里说“仅使用 WebGL”,不是单纯为了精简依赖,而是因为 IMGUI 的绘制指令流原本就是面向 GPU 的。
这里有一个很多人忽略的点:IMGUI 不是“轻量 UI 组件库”,它是一个“绘制指令生成器”。它的核心产出是ImDrawList里的顶点数组和索引数组,外加一组回调信息。窗口、按钮、文本框这些只是你调用 API 后的结果。理解这一点,后面调试 Web 版渲染问题时思路会清楚很多。
2.2 GLFW 在 WASM 里不是原来的 GLFW,但接口没变
桌面上,GLFW 负责创建窗口、OpenGL 上下文,以及收集鼠标键盘事件。你要在浏览器里跑同一份代码,直接编译是不行的,因为glfwCreateWindow在浏览器里没有系统窗口可创建。Emscripten 提供了 GLFW 的 JS 实现,它把一次glfwCreateWindow(1280, 720, "WebGui", ...)调用映射成对页面里一个<canvas>的初始化:canvas 的宽高变成窗口尺寸,canvas 上的鼠标、键盘、滚轮事件被转发到 GLFW 的事件队列里。你在 C++ 侧调glfwPollEvents()时,它就从 JS 事件队列里取一批事件喂给 Dear ImGui 的 IO 系统。
这意味着你的桌面代码里,只要没有用到特别冷门的 GLFW 能力(比如多窗口、拖放文件、原生对话框),基本可以做到“编译一次,桌面和 Web 都能跑”。我一般会把平台相关的代码用宏隔开,比如 Emscripten 环境里主循环用emscripten_set_main_loop,桌面环境用while循环,其他部分共用。GLFW 在这里的价值就是帮你把窗口抽象和输入抽象统一起来,省去在浏览器里手写addEventListener桥接的脏活。
2.3 WebGL 2.0 与 OpenGL ES 3.0:Emscripten 眼里的同一件事
标题里的 WebGL 是最终的渲染目标,但在 C++ 侧你写的是 OpenGL ES 3.0 代码。Emscripten 的编译器会把 GLES3 的调用翻译成 WebGL 2.0 的调用,因为两者在功能上是基本对齐的:都支持 GLSL 300 es,都支持 VAO、uniform buffer、多重渲染目标,对 Dear ImGui 这种只画顶点和纹理的用例绰绰有余。编译时加-s USE_WEBGL2=1,然后在glfwWindowHint里请求一个 3.0 版本的上下文,生成的页面打开后拿到的就是一个 WebGL 2.0 画布。
下面是常见的对应关系,方便对照理解:
| 桌面/原生端 | Web 端(Emscripten 编译后) |
|---|---|
| OpenGL ES 3.0 上下文 | WebGL 2.0 上下文 |
| GLSL 300 es shader | 编译后转为 WebGL 2.0 可用的 GLSL 版本 |
glfwCreateWindow创建窗口 | 初始化<canvas>并准备渲染上下文 |
glfwPollEvents事件轮询 | 从浏览器事件队列取出本轮事件 |
本地文件系统fopen | Emscripten 虚拟文件系统(可嵌入或预加载 |
如果因为兼容性原因只能用 WebGL 1.0,也可以跑,只要把上下文版本改成 2.0,并把初始化时传给ImGui_ImplOpenGL3_Init的 GLSL 版本字符串改成#version 100。代价是部分新特性不可用,但 Dear ImGui 基础渲染不受影响。这个降级路径让我在对接一些老版本浏览器内核时不用重写任何 UI 代码,只换编译参数。
3. 用 Emscripten 编译 WebGui:从零跑通的最小命令与代码
3.1 准备工具链:emsdk 环境变量与项目目录
编译 WASM 的主流工具链是 Emscripten SDK。安装方式是从官方 SDK 路径拉取,然后激活当前版本。需要注意的是,每次打开新终端都要重新执行环境脚本,否则emcc命令找不到。我一般会在项目根目录放一个source_env.sh把这一步固定住。
# 先安装并激活 Emscripten SDK(每个机器只需要一次) git clone 你的/emsdk/目录 cd emsdk ./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh # 项目目录建议这样放 # webgui/ # webgui_main.cpp # imgui/ # Dear ImGui 的源码目录 # backends/ # imgui_impl_glfw.cpp # imgui_impl_opengl3.cpp # assets/ # 你的中文字体.ttfDear ImGui 源码不用全部编译,核心只需要imgui.cpp、imgui_draw.cpp、imgui_tables.cpp、imgui_widgets.cpp四个文件,外加两个后端文件imgui_impl_glfw.cpp和imgui_impl_opengl3.cpp。这里有个小坑:Emscripten 的 GLFW 头文件路径和桌面不完全一致,正确做法是在源码里只#include <GLFW/glfw3.h>,由编译器的头文件搜索路径决定用哪一份。不要手动把桌面版 GLFW 的头文件拷过来,否则编译期就会报一堆版本不匹配。
3.2 入口代码:桌面的 while 循环必须换掉
在浏览器里跑 GUI 主循环,最核心的改动就是不能写while(!glfwWindowShouldClose(window))这种死循环。浏览器的主线程是事件驱动的,你用一个无限循环占住线程,渲染永远出不来。正确做法是把一帧的逻辑抽成一个函数,交给emscripten_set_main_loop来调度。
// webgui_main.cpp #include <GLFW/glfw3.h> #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" #include <emscripten/emscripten.h> static GLFWwindow* window = nullptr; static int frame_count = 0; void frame_loop() { glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); ImGui::SetNextWindowPos(ImVec2(20.0f, 20.0f)); ImGui::Begin("WebGui on WASM"); ImGui::Text("frame: %d", frame_count++); if (ImGui::Button("切换标题")) { ImGui::SetWindowTitle("WebGui on WASM", "点过了"); } ImGui::End(); ImGui::ShowDemoWindow(nullptr); ImGui::Render(); int fb_w = 0, fb_h = 0; glfwGetFramebufferSize(window, &fb_w, &fb_h); glViewport(0, 0, fb_w, fb_h); glClearColor(0.06f, 0.06f, 0.08f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); glfwSwapBuffers(window); } int main() { if (!glfwInit()) return 1; // Emscripten 的 GLFW 实现中,(3,0) 会创建 WebGL 2.0 上下文 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); window = glfwCreateWindow(1280, 720, "WebGui", nullptr, nullptr); if (!window) return 2; glfwMakeContextCurrent(window); glfwSwapInterval(1); IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init(nullptr); emscripten_set_main_loop(frame_loop, 0, 1); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); return 0; }这里emscripten_set_main_loop(frame_loop, 0, 1)的三个参数分别代表:要循环执行的帧函数、目标帧率(0 表示让浏览器用requestAnimationFrame的自然节拍)、模拟无限循环模式(传 1 表示每帧执行后不退出)。很多人第一次跑通后发现标签页很容易掉到很低的帧率,通常是忽略了glfwSwapInterval(1),垂直同步在 Web 端是通过请求动画帧实现的,不设置会错乱。
3.3 编译命令:三个必须理解的 linking flag
准备好代码后,编译命令如下。这里有几个 flag 直接决定能不能跑,不建议删:
emcc webgui_main.cpp \ imgui/imgui.cpp imgui/imgui_draw.cpp imgui/imgui_tables.cpp imgui/imgui_widgets.cpp \ imgui/backends/imgui_impl_glfw.cpp imgui/backends/imgui_impl_opengl3.cpp \ -I imgui -I imgui/backends \ -o build/webgui.html \ -s USE_GLFW=3 \ -s USE_WEBGL2=1 \ -s FULL_ES3=1 \ -s ALLOW_MEMORY_GROWTH=1 \ -s INITIAL_MEMORY=67108864 \ --embed-file assets/你的中文字体.ttf@/你的中文字体.ttf参数含义:
| 参数 | 作用 | 不设置的后果 |
|---|---|---|
USE_GLFW=3 | 链接 Emscripten 自带的 GLFW 库 | 链接器报找不到glfwCreateWindow等符号 |
USE_WEBGL2=1 | 让 GLFW 的窗口创建逻辑走 WebGL 2 分支 | 拿到的是 WebGL 1.0,部分 GLSL 写法会崩 |
FULL_ES3=1 | 开启完整的 OpenGL ES 3.0 支持 | shader 里用高版本特性时编译器会报错 |
ALLOW_MEMORY_GROWTH=1 | 允许 WASM 运行时按需增加内存 | 固定内存下 UI 复杂后容易内存不足 |
INITIAL_MEMORY | 初始内存大小,单位字节 | 默认值偏小,中文字体加载时容易卡顿 |
--embed-file | 把资产文件嵌入 WASM 虚拟文件系统 | 运行时打开字体文件失败,中文全是方块 |
这段命令我通常会在构建脚本里固化下来。需要提醒的是,新版 Emscripten 会把USE_WEBGL2等参数默认开启,但在老版本上不写就踩坑。因此保留这些 flag 反而兼容性更好,省得换环境后行为不一致。
3.4 浏览器里加载:两个常见的启动失败
编译成功后,build目录下会出现webgui.html、webgui.js、webgui.wasm三个文件。不能直接在本地双击打开,因为 WASM 的 fetch 请求受文件协议限制,绝大多数浏览器会报“Failed to fetch”的加载错误。要在本地起一个静态服务,最简单的做法是在build目录里跑一条 Python 命令:
cd build python3 -m http.server 8080然后浏览器访问http://localhost:8080/webgui.html。如果服务器没有为.wasm配置正确的 MIME 类型(application/wasm),浏览器也会拒绝执行。常见的http.server内置类型表已经包含 wasm,一般不会出问题;遇到自定义服务器时,记得手动加 MIME 配置。
4. 上线前必调的四个参数:字体、输入、DPI 缩放与 draw call
4.1 中文字体嵌入:GlyphRanges 与嵌入方式的选择
默认的 Dear ImGui 字体图集只有 ASCII 字符,直接显示中文会变成一团方块。要显示中文,得在初始化阶段加载一个真字体文件,并通过GetGlyphRangesChineseFull()指定要生成的字形范围。代码放在ImGui::CreateContext()之后:
ImGuiIO& io = ImGui::GetIO(); io.Fonts->Clear(); ImFontConfig cfg; cfg.OversampleH = 2; cfg.OversampleV = 1; io.Fonts->AddFontFromFileTTF("/你的中文字体.ttf", 18.0f, &cfg, io.Fonts->GetGlyphRangesChineseFull()); io.Fonts->Build();这里有两个细节。第一,字体文件路径要跟编译时--embed-file里@后面的路径完全一致,我一度因为路径写成了相对路径assets/字体.ttf,而@后面写的是/字体.ttf,运行时AddFontFromFileTTF找不到文件直接空跑。第二,GetGlyphRangesChineseFull()覆盖的字形非常多,生成的图集纹理可能接近 1024x1024 甚至更大。如果你只用到部分常用字,可以用缩减后的字面范围,加载速度和内存都会更好。我一般先全量跑通界面,再统计用字去缩减范围,收益明显。
嵌入方式上,--embed-file适合字重中等、不追求首次加载速度的场景,缺点是 WASM 二进制变大、浏览器要等整个文件加载完才能解析。--preload-file则是把资产打包成独立的.data文件,页面加载时可以并行拉取,体验更好,但需要在 JS 侧等文件系统预加载完成再调用主函数。移动端场景我优先用--preload-file,桌面演示图方便就用--embed-file。
4.2 键盘输入焦点:为什么鼠标能点但键盘没反应
GLFW 的 Web 端会把 canvas 上的鼠标事件直接转发给 ImGui,所以鼠标点击通常不需要额外处理。但键盘事件不一样:浏览器规定键盘事件要发给当前获得焦点的元素。如果 canvas 没有tabindex属性,它根本拿不到键盘事件,表现为光标能进来、按钮能点,但文本框敲字没反应。这个坑在页面里有多个元素时尤其隐蔽。
解决办法是在页面脚本里给 canvas 补上tabindex并主动聚焦。Emscripten 的模块初始化完成后,可以这样处理:
Module.onRuntimeInitialized = function() { Module.canvas.setAttribute('tabindex', '0'); Module.canvas.focus(); };代码里还要给 ImGui 的 IO 设置io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard,否则即使事件到了,键盘导航控制的响应也不完整。这在桌面版往往默认好用的功能,在 Web 上属于“少一步就翻车”的典型。
4.3 高分屏适配:canvas 尺寸不等于逻辑尺寸
浏览器里的canvas有两个尺寸概念:一是元素在页面上的 CSS 显示尺寸,二是画布内部的物理像素尺寸。这两者不一致时,鼠标坐标和渲染坐标就会错位,现象是 UI 整体偏移,点击位置随着离左上角越远偏差越大。Retina 类显示屏上 devicePixelRatio 为 2,如果你只设置了 CSS 尺寸,GLFW 拿到的 framebuffer 尺寸和窗口尺寸不一致,ImGui 的DisplayFramebufferScale就会算成 1.0,字也会发虚。
我在页面脚本里通常会做这样一次尺寸同步:
function syncCanvasSize() { var dpr = window.devicePixelRatio || 1; var width = window.innerWidth; var height = window.innerHeight; Module.canvas.width = Math.floor(width * dpr); Module.canvas.height = Math.floor(height * dpr); Module.canvas.style.width = width + 'px'; Module.canvas.style.height = height + 'px'; } window.addEventListener('resize', syncCanvasSize);这个函数在模块初始化后调用一次,之后每次浏览器窗口变化都同步。C++ 侧不需要改任何代码,glfwGetFramebufferSize每帧读到的是最新物理尺寸,ImGui::GetIO().DisplayFramebufferScale会按比例自动算对。
4.4 draw call 数量与帧率:WASM 下 IMGUI 的性能边界
IMGUI 每帧的绘制指令数等于ImDrawData里CmdList中命令的数量。在浏览器里,每一个 draw command 都是一次从 JS 到 WebGL 的调用,开销比桌面原生 OpenGL 要高。帧率掉的元凶往往不是顶点数量,而是 draw call 太多。最简单的观察办法是通过页面控制台打印一下当前帧的 command 数量:
if (frame_count % 120 == 0) { ImDrawData* draw_data = ImGui::GetDrawData(); int cmd_total = 0; for (int i = 0; i < draw_data->CmdListsCount; i++) { cmd_total += draw_data->CmdLists[i]->CmdBuffer.Size; } printf("draw commands this frame: %d\n", cmd_total); }正常的界面几十到一两百个 command 都没问题,超过三百个就得注意了。常见的优化方式有三条:减少同时可见的窗口数量与复杂边框,因为每个裁剪区域都可能导致 command 拆分;尽量避免在界面上放大量不同颜色的细碎区域,它们会打断同一纹理批次;以及把高频更新的数据和静态数据显示在不同窗口,避免每帧强制全部重排。在 Web 端,帧率目标我一般定为 60 帧的桌面窗口跑 30 帧以上就算能交付,毕竟浏览器里对 GUI 刷新率的要求本来就不高。
5. WebGui 移植避坑清单:五个我实际踩过的浏览器端问题
5.1 编译报“undefined symbol: glfwCreateWindow”
现象:链接阶段报错,提示找不到glfwCreateWindow、glfwInit等符号。原因:编译命令里少了-s USE_GLFW=3,或者这个 flag 写在了单独一条命令里而链接时没有带上。Emscripten 的 GLFW 不是从系统头文件里自动链接的,必须显式声明。解决:把USE_GLFW=3写进同一条emcc命令的链接参数区。另外注意-s参数不要拼写错误,写成了USE_GLFW3这类漏等号的形式也会静默丢失。
5.2 中文全部显示成方块或问号
现象:按钮文字、窗口标题全是豆腐块,但英文数字正常。原因:默认字体图集里没有 CJK 字形,且字体文件没有成功加载到虚拟文件系统。解决:先确认编译命令里--embed-file的源路径存在,再确认AddFontFromFileTTF里的路径和@后的路径完全一致。如果路径没问题但依然方块,检查io.Fonts->Build()是否在ImGui_ImplOpenGL3_NewFrame()之前调用,字体图集重建后必须更新 GPU 纹理,顺序错了字形还是旧的。
5.3 鼠标悬停和点击整体偏移
现象:按钮 hover 高亮位置和光标差了半个屏幕,越往右下角偏移越大。原因:canvas 物理像素尺寸和 CSS 尺寸不一致,通常是 devicePixelRatio 不为 1 时没有同步。解决:用前面 4.3 的同步函数,确保canvas.width/height等于CSS 尺寸 * devicePixelRatio。做完后鼠标映射就准确了,不需要在 C++ 侧做任何坐标换算。有时候只显示了页面的一部分,检查是不是 CSS 里给 canvas 设了固定宽高或 transform 缩放,那又会引入新的偏移。
5.4 页面白屏,浏览器标签页像卡死一样
现象:打开页面一片空白,控制台没有报错,但整个页面响应很慢。原因:在main里写了while (!glfwWindowShouldClose(window)) { ... },这个死循环占用了浏览器主线程,事件循环永远不被触发,渲染和输入全部被阻塞。解决:删除 while 循环,把帧体逻辑拆成frame_loop(),用emscripten_set_main_loop(frame_loop, 0, 1)注册。这里传 0 表示帧率交给浏览器控制,不要自己写 sleep,否则在移动端低电量模式下会异常延迟。
5.5 界面状态刷新后全部重置(imgui.ini 丢失)
现象:每次刷新页面,窗口位置、控件配置全部回到默认。原因:Emscripten 的虚拟文件系统是内存态,程序退出后所有写入都清空。ImGui 默认会把布局写入imgui.ini,但那个路径在内存文件系统里,浏览器刷新后立刻消失。解决:在初始化时把io.IniFilename设为nullptr禁用自动写文件,然后在每次状态变化时用 EM_JS 把文件的文本内容同步到浏览器 localStorage 里。示例代码:
extern "C" { EM_JS(void, save_ini_to_storage, (const char* data, int len), { const str = UTF8ToString(data, len); localStorage.setItem("webgui_layout", str); }); EM_JS(void*, load_ini_from_storage, (), { const str = localStorage.getItem("webgui_layout") || ""; const len = str.length + 1; const ptr = _malloc(len); stringToUTF8(str, ptr, len); return ptr; }); }启动时检测到 localStorage 里有内容,就把地址转成std::string塞回io.IniData;关闭前把ImGui::SaveIniSettingsToMemory()的结果交给save_ini_to_storage。这样刷新前后配置能完整保留。
6. 进阶交互:EM_JS 桥接、多 canvas 实例与前端嵌入
6.1 用 EM_JS 把配置导出成文件
WebGui 最常见的实用需求是“导出当前配置”。在桌面版你直接写文件即可,浏览器里得借助 JS 的 Blob 下载能力。用EM_JS声明一个函数,接收 C++ 侧的内存指针和长度:
EM_JS(void, download_file, (const char* name, const char* data, int len), { const filename = UTF8ToString(name); const content = new Uint8Array(Module.HEAPU8.buffer, data, len); const blob = new Blob([content], {type: 'application/octet-stream'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); });调用时把 ImGui 的 ini 内容或用户数据作为const char*传入,浏览器就会自动触发下载。注意new Uint8Array的第三个参数是字节长度,传错会导致文件损坏。这个模式我几乎在每个 WebGui 工具里都用,比把数据回传到服务器再下载简单得多。
6.2 一个页面跑多个 WebGui 实例
多实例用 WASM 的多 Module 方案实现。每个 Emscripten 模块实例化时可以指定自己的 canvas:
const guiA = { canvas: document.getElementById('canvasA'), locateFile: f => 'buildA/' + f }; const guiB = { canvas: document.getElementById('canvasB'), locateFile: f => 'buildB/' + f };两套 WebGui 各自在编译时用不同的build/目录,资源互不干扰。注意要保证两份 WASM 的全局函数名不冲突,或者用模块闭包隔离。这个方案的适用场景是同时对比多个参数面板,比在单个实例里开多个窗口更可控,内存隔离也更干净。
6.3 嵌入现有前端框架
如果 WebGui 只是整个产品的一半,另一半是业务网站,最省心的嵌入方式是<iframe>指向编译出的webgui.html。同源场景下可以用postMessage做双向通信:前端把参数通过postMessage发给 iframe 内的 JS,再由 JS 调用 C++ 侧暴露的EM_ASM接口写入内存;C++ 侧的状态变化同理反向推送。这个方案隔离了中国样式冲突、JS 全局污染和键盘事件抢焦点的问题,维护成本最低。如果追求极致体积,可以把编译输出做成纯 JS 模块再异步加载,但调试继承复杂度会高一截。
最后说一点个人习惯。我每次搭建新的 WebGui 项目,都会先跑通一个只包含ShowDemoWindow的最小页面,确认主循环、输入、字体和 DPI 四条链路都正常,再往里面加业务代码。早年我图省事直接抄桌面版的主循环结构,结果白屏排查花了一整个下午。从那之后,emscripten_set_main_loop就成了我检查的第一行代码。希望这些经验能帮你少走一遍同样的弯路。
本文还有配套的精品资源,点击获取