作者背景:C++ PC 客户端老兵,想用 Lynx 替代 DuiLib,用 JS 写 PC 客户端 UI。
一、入口在哪里
Lynx Explorer 的 main.cc 只有 25 行,非常干净:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int APIENTRY wWinMain(HINSTANCE instance, ...) { auto& lynx_env = lynx::pub::LynxEnv::GetInstance(); lynx_env.SetDevtoolEnabled(true);
lynx_env.RegisterNativeModule("ExplorerModule", ExplorerModuleCreator, nullptr);
auto* window = new LynxWindow(0, 0, 800, 600); window->SetQuitOnClose(true); window->LoadTemplate("");
::MSG msg; while (::GetMessage(&msg, nullptr, 0, 0)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } }
|
五步走,一步步拆。
① LynxEnv — 全局环境
单例,全局只初始化一次。开了 devtool 方便调试。
类比 DuiLib:类似 CPaintManagerUI::GetInstance()。
② RegisterNativeModule — JS↔C++ 桥梁
JS 里写 require('ExplorerModule') 就能调 C++ 方法。这是 JS 调 C++ 的入口,以后会自己写很多。
③ LynxWindow — 窗口 + 引擎
LynxWindow 构造函数做了两件大事:
- 创建 Win32 窗口(RegisterClass + CreateWindow,跟 DuiLib 一样)
- 创建 LynxView(核心渲染引擎)
关键代码在 Builder 模式:
1 2 3 4 5 6 7
| LynxView::Builder builder; builder.SetScreenSize(w, h, dpi) .SetFrame(0, 0, w, h) .SetParent(window_handle_) .SetGenericResourceFetcher(...) .RegisterNativeView<FakeView>(...); lynx_view_ = builder.Build();
|
④ LoadTemplate — 加载模板
空 URL = 加载默认首页。内部读 resources\homepage\main.lynx.bundle,这是 Lynx 的打包格式(编译后的 JS 字节码 + CSS + 资源索引)。
⑤ 消息循环
标准 Win32 消息循环。Lynx 的渲染和窗口事件跑在同一个线程。
二、ScreenSize vs Frame — 窗口和视口的区别
场景:1920×1080 的显示器上开了个 800×600 的窗口
1 2 3 4 5 6 7 8 9
| ┌─────────────────────────────────────┐ │ 屏幕 1920×1080 │ ← ScreenSize 描述这个 │ │ │ ┌──────────────┐ │ │ │ 窗口 800×600 │ │ ← Frame 描述这个 │ │ │ │ │ │ Lynx在这里画 │ │ │ └──────────────┘ │ └─────────────────────────────────────┘
|
ScreenSize = 屏幕的物理信息
1
| SetScreenSize(1920, 1080, 1.5)
|
告诉 JS 引擎:”你跑在一个 1.5 倍缩放的屏幕上。”
为什么需要? JS 里写 font-size: 14px,这个 14px 到底多大?得看 DPI。1.5 倍屏上 14px = 21 物理像素。
类比 DuiLib:GetSystemMetrics(SM_CXSCREEN) + DPI 感知。
Frame = Lynx 渲染区域
1
| SetFrame(0, 0, 800, 600)
|
告诉渲染引擎:”你在窗口里这个区域画。”
类比 DuiLib:MoveWindow(hwnd, 0, 0, 800, 600)。
实际例子:左边原生列表 + 右边 Lynx 聊天区
1 2 3 4 5
| ┌──────────┬──────────────────────┐ │ 联系人列表 │ 聊天区域 │ │ (Win32) │ (Lynx) │ │ 原生控件 │ Frame(200,0,600,800) │ └──────────┴──────────────────────┘
|
大多数情况下 ScreenSize 和 Frame 宽高一样(Lynx 占满窗口),只有嵌入部分区域时才不同。
|
ScreenSize |
Frame |
| 描述什么 |
屏幕的物理信息 |
Lynx 渲染区域 |
| DuiLib 类比 |
GetSystemMetrics() + DPI |
MoveWindow() |
| 给谁用 |
JS 引擎算像素 |
渲染引擎画图 |
三、FakeView — 自定义原生控件
为什么要 FakeView?
JS+CSS 画不出所有东西:
| JS/CSS 能画的 |
必须用原生控件的 |
| 按钮、文字、图片 |
视频播放器 |
| 列表、滚动 |
WebView |
| 动画、渐变 |
地图 |
| 圆角、阴影 |
摄像头预览 |
从 DuiLib 说起
DuiLib 里在 XML 嵌一个 ActiveX 控件:
1
| <ActiveX name="webview" clsid="{8856F961-340A-11D0-A96B-00C04FD705A2}"/>
|
XML 是占位符,真正干活的是 C++ 创建的 COM 对象。
Lynx 里一模一样
JS 里写:
1
| <x-fake-view style="width: 100%; height: 200px;" />
|
这个 <x-fake-view> 只是占位符,真正创建的是 C++ 的 FakeView 对象。
1 2
| builder.RegisterNativeView<FakeView>("x-fake-view", this);
|
FakeView 每个方法对应什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class FakeView : public LynxNativeView { bool OnCreate() override { return true; }
void OnDestroy() override {}
void OnLayoutChanged(float l, float t, float w, float h, float dpi) override { TriggerEvent("resize", ...); }
void OnMethodInvoked(method, attrs, callback) override { callback(kSuccess, ...); } };
|
| 方法 |
DuiLib 对应 |
说明 |
OnCreate |
Init() |
控件创建时 |
OnDestroy |
析构 |
控件销毁时 |
OnLayoutChanged |
SetPos() |
CSS 布局算完后调用 |
OnMethodInvoked |
自定义消息 |
JS 调 C++ 方法的入口 |
实战:做一个视频播放器控件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class VideoPlayer : public LynxNativeView { HWND video_hwnd_;
bool OnCreate() override { video_hwnd_ = CreateWindow(L"video_player", ...); return true; }
void OnLayoutChanged(float l, float t, float w, float h, float dpi) override { MoveWindow(video_hwnd_, l*dpi, t*dpi, w*dpi, h*dpi, TRUE); }
void OnMethodInvoked(const char* method, ..., callback) override { if (strcmp(method, "play") == 0) { PlayVideo(); callback(kSuccess, ...); } } };
builder.RegisterNativeView<VideoPlayer>("x-video-player", nullptr);
|
1 2 3
| <x-video-player style="width: 100%; height: 300px;" ref="player" /> this.refs.player.callMethod('play')
|
一句话:FakeView = DuiLib 里的自定义控件,RegisterNativeView 就是告诉 Lynx “这个标签归我管”。
四、架构全景图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ┌─────────────────────────────────────────────┐ │ main.cc │ │ LynxEnv → RegisterModule → LynxWindow │ └──────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ LynxView (核心入口) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ │ │ JS 引擎 │ │ 布局引擎 │ │ 渲染引擎 │ │ │ │ QuickJS │→ │ Lynx │→ │ Skia 绘制 │ │ │ │ (Lepus) │ │ Layout │ │ (CPU/GPU) │ │ │ └────┬─────┘ └──────────┘ └──────┬──────┘ │ │ │ │ │ │ ┌────▼─────┐ ┌─────▼─────┐ │ │ │ NAPI 绑定 │ │ Win32 │ │ │ │ JS↔C++桥 │ │ Surface │ │ │ └──────────┘ └───────────┘ │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ .lynx.bundle (模板包) │ │ JS字节码 + CSS + 资源索引 │ └─────────────────────────────────────────────┘
|
完整数据流
1 2 3
| main → 初始化Lynx环境 → 创建Win32窗口 → Builder构建LynxView(设为子窗口) → 设置资源路径 → 加载 .lynx.bundle → JS引擎编译执行 → 生成Element树 → 布局引擎计算位置大小 → 渲染引擎生成绘制指令 → Skia画到窗口
|
五、待解答的疑问
- ❓ Engine 线程具体怎么和 UI 线程通信?Actor 模式是什么?
- ❓
.lynx.bundle 的内部结构是什么?怎么打包的?
- ❓ LynxView 内部 LynxTemplateRenderer 和 LynxUIRenderer 怎么分工?
- ❓ 如何在自己的 Win32 窗口里嵌入 LynxView(不用 Explorer)?