332 lines
15 KiB
Markdown
332 lines
15 KiB
Markdown
# WebView CEF
|
||
|
||
<a href="https://pub.dev/packages/webview_cef"><img src="https://img.shields.io/pub/likes/webview_cef?logo=dart" alt="Pub.dev likes"/></a> <a href="https://pub.dev/packages/webview_cef" alt="Pub.dev popularity"><img src="https://img.shields.io/pub/popularity/webview_cef?logo=dart"/></a> <a href="https://pub.dev/packages/webview_cef"><img src="https://img.shields.io/pub/points/webview_cef?logo=dart" alt="Pub.dev points"/></a> <a href="https://pub.dev/packages/webview_cef"><img src="https://img.shields.io/pub/v/webview_cef.svg" alt="latest version"/></a> <a href="https://pub.dev/packages/webview_cef"><img src="https://img.shields.io/badge/macOS%20%7C%20Windows%20%7C%20Linux-blue?logo=flutter" alt="Platform"/></a>
|
||
|
||
[English](README.md) · **简体中文**
|
||
|
||
基于 [CEF](https://bitbucket.org/chromiumembedded/cef)(Chromium Embedded Framework)的 Flutter **桌面端** WebView。它以离屏方式渲染一个完整的 Chromium 浏览器,并将画面呈现在 Flutter 的 `Texture` 上,因此网页内容能与你的 Flutter UI 在 Windows、macOS、Linux 上原生地合成在一起。
|
||
|
||
> 基于 **CEF 149(Chromium 149)**。
|
||
|
||
---
|
||
|
||
## 特性
|
||
|
||
- 🌐 **完整 Chromium 引擎** —— 现代 Web 标准、WebGL、HTML5 视频等。
|
||
- ⚡ **GPU 零拷贝渲染**(Windows 与 macOS)—— 帧直接从 Chromium 的 GPU 输出经共享纹理交给 Flutter(Windows 用 D3D11 纹理,macOS 用 IOSurface),每帧不再做 CPU 端换色,也不再做 CPU→GPU 上传。Linux 仍走软件路径。
|
||
- 🎞️ **自适应帧率**(Windows 与 macOS)—— 由显示器的垂直消隐(vblank)驱动产帧(Windows 用 `IDXGIOutput::WaitForVBlank`,macOS 用 `CVDisplayLink`),因此 webview 跟随显示器真实刷新率(如 120/144Hz),不再被限制在 60fps;静态内容则保持空闲不产帧。
|
||
- ⌨️ **中日韩输入法实时上屏** —— 原生输入法合成管线,拼音预编辑实时显示、选词正确上屏。
|
||
- 🔌 **JavaScript 桥** —— JS 调用 Dart,Dart 执行 JS。
|
||
- 🍪 **Cookie 管理** —— 读取、设置、删除 Cookie。
|
||
- 🪟 **多实例** —— 同时运行多个独立 webview。
|
||
- 📜 **用户脚本注入** —— 在文档开始/结束时注入 JS/CSS。
|
||
- 🛠️ **DevTools**、鼠标与触控板输入、导航以及加载/标题/URL 事件。
|
||
|
||
## 平台支持
|
||
|
||
| 平台 | 最低版本 | 架构 |
|
||
| --- | --- | --- |
|
||
| Windows | Windows 10 | x64 |
|
||
| macOS | macOS 12.0 | arm64、x86_64 或 Universal(arm64 + x86_64) |
|
||
| Linux | — | x64、arm64 |
|
||
|
||
## 环境要求
|
||
|
||
- Flutter **>= 3.27.0**、Dart **>= 3.6.0**(已在最新稳定版 Flutter 3.44.x 上测试)。
|
||
- 原生侧需要 C++20 工具链(CEF 149 要求)—— 较新的 MSVC / Clang / GCC。
|
||
|
||
---
|
||
|
||
## 从 ≤ 0.2.2 升级
|
||
|
||
0.5.0 是一次大版本升级(Flutter 3.44 + CEF 149),在所有平台上都有破坏性变更。若你从旧版本升级,请按以下步骤操作:
|
||
|
||
- **工具链** —— 升级到 Flutter **≥ 3.27.0** / Dart **≥ 3.6.0**(原为 2.5.0 / 2.17.1)。原生构建现在需要 **C++20**(CEF 149);请确保你的工程没有把插件 target 强制设为更低的 C++ 标准。
|
||
- **移除的 Dart API** —— `WebviewCefPlatform`、`MethodChannelWebviewCef`、`getPlatformVersion()` 已移除(同时移除了 `plugin_platform_interface` 依赖)。它们本就不是对外 API,且无替代(`getPlatformVersion` 仅返回演示值)。请只 import `package:webview_cef/webview_cef.dart`,使用 `WebviewManager` / `WebViewController`。
|
||
- **Windows** —— `initCEFProcesses` 签名变更。请更新 `windows/runner/main.cpp`:它现在接收 `HINSTANCE` 并返回子进程退出码,且必须作为 `wWinMain` 的第一条语句立即返回(见下方 Windows 安装片段)。最低系统现为 **Windows 10**。
|
||
- **macOS** —— 把部署目标提升到 **12.0**:在 `macos/Podfile` 设置 `platform :osx, '12.0'`,并在 Xcode 中把 Runner target 的 macOS Deployment Target 也设为 12.0(CEF 149 的 framework 以 12.0 构建)。要启用多进程渲染,请在 `macos/Podfile` 的 `post_install` 中加入一行钩子(见 macOS 安装一节)。构建默认只针对**本机架构**(arm64 *或* x86_64);在 `pod install` 前设置 `WEBVIEW_CEF_MACOS_ARCH=universal` 即可生成 Universal(arm64 + x86_64)App。
|
||
|
||
---
|
||
|
||
## 安装
|
||
|
||
### Windows
|
||
|
||
1. 添加依赖:
|
||
|
||
```bash
|
||
flutter pub add webview_cef
|
||
```
|
||
|
||
2. 修改 `windows/runner/main.cpp`。由于 Chromium 的多进程架构,以及需要把输入/输入法和 method channel 调用路由到 Flutter 引擎线程,需要加两处钩子:
|
||
|
||
```cpp
|
||
#include "webview_cef/webview_cef_plugin_c_api.h"
|
||
|
||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||
// 启动 CEF 子进程,必须放在 wWinMain 最前面。
|
||
int exit_code = initCEFProcesses(instance);
|
||
if (exit_code >= 0) {
|
||
return exit_code;
|
||
}
|
||
// ……原有 runner 初始化……
|
||
```
|
||
|
||
在消息循环里把消息转发给 CEF(启用键盘输入,并让 CEF 能向 Flutter 引擎线程投递消息):
|
||
|
||
```cpp
|
||
::MSG msg;
|
||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||
::TranslateMessage(&msg);
|
||
::DispatchMessage(&msg);
|
||
handleWndProcForCEF(msg.hwnd, msg.message, msg.wParam, msg.lParam);
|
||
}
|
||
```
|
||
|
||
> 输入法由插件自动接管,runner 无需额外代码。
|
||
|
||
首次构建时会自动从 <https://cef-builds.spotifycdn.com> 下载官方 CEF *Standard Distribution*(约 330MB)到 `third/cef`,并从源码编译 `libcef_dll_wrapper`,因此第一次构建会明显较慢。
|
||
|
||
### macOS
|
||
|
||
> **要求 macOS 12.0 及以上** —— CEF 149 的 framework 以 12.0 为部署目标,所以你 App 的 macOS 部署目标必须 **≥ 12.0**(在 `macos/Podfile` 设 `platform :osx, '12.0'`,并同步 Runner target)。更低的版本无法干净链接。
|
||
|
||
1. 添加依赖:
|
||
|
||
```bash
|
||
flutter pub add webview_cef
|
||
```
|
||
|
||
2. 启用多进程(推荐):在 `macos/Podfile` 已有的 `post_install` 里加上 helper 钩子:
|
||
|
||
```ruby
|
||
post_install do |installer|
|
||
installer.pods_project.targets.each do |target|
|
||
flutter_additional_macos_build_settings(target)
|
||
end
|
||
# webview_cef: 嵌入 CEF helper 子进程 app(多进程)。
|
||
require File.expand_path(
|
||
'Flutter/ephemeral/.symlinks/plugins/webview_cef/macos/embed_cef_helpers.rb', __dir__)
|
||
WebviewCEF.install_helper_phase(installer)
|
||
end
|
||
```
|
||
|
||
然后 `pod install`(`flutter run` 会自动跑)。它会装一个 "Embed CEF Helpers" 构建阶段,把预编译的 helper 克隆成 5 个 CEF 子进程 `.app` 嵌进你的 App —— 无需手动加 Xcode target。**不加这个钩子插件也能用,但会回退单进程**(Chromium 不支持的模式:无崩溃隔离、V8 proxy resolver 被禁等)。
|
||
|
||
macOS 走 CocoaPods,不跑 CMake 的下载流程。改由 podspec 的 `prepare_command` 在 `pod install` 时运行 [`macos/scripts/download_cef.sh`](macos/scripts/download_cef.sh),逻辑与 Windows/Linux 对齐:下载所选架构的官方 CEF *Standard Distribution*(来自 <https://cef-builds.spotifycdn.com>,版本由 [`third/download.cmake`](third/download.cmake) 的 `CEF_VERSION` 固定),从源码编译 `libcef_dll_wrapper`,把 framework 整理成 versioned macOS bundle,并安装进(已 git-ignore 的)`macos/third/cef`。所以首次 `pod install` 会明显变慢;之后只要版本未变即为 no-op。
|
||
|
||
要求:`PATH` 上需有 `cmake`(以及 `ninja`,否则退回 `make`)来编译 wrapper —— `brew install cmake ninja`。
|
||
|
||
> wrapper 默认编 `Debug`,以匹配 `flutter run` / `flutter build macos --debug`。如需 release 构建,在 `pod install` 前设置 `CEF_WRAPPER_BUILD_TYPE=Release`(debug 与 release 需要对应配置编译的 wrapper —— `#if DCHECK_IS_ON()` 会改变其 ABI)。
|
||
|
||
#### 选择架构
|
||
|
||
`WEBVIEW_CEF_MACOS_ARCH` 决定准备哪些架构。下载脚本与 podspec 读取同一个变量,因此 CEF 二进制与 App 的 `EXCLUDED_ARCHS` 始终一致。
|
||
|
||
| 取值 | 结果 |
|
||
| --- | --- |
|
||
| `host`(默认) | 构建机器的架构 |
|
||
| `arm64` | 仅 Apple Silicon |
|
||
| `x86_64` | 仅 Intel |
|
||
| `universal` | 两者,用 `lipo` 合并 |
|
||
|
||
```bash
|
||
WEBVIEW_CEF_MACOS_ARCH=universal pod install
|
||
cd .. && flutter build macos --release
|
||
```
|
||
|
||
Universal 构建会下载两份 CEF 发行包并编译两次 wrapper,耗时约为原来的两倍,并需要约 8 GB 临时空间。framework 二进制、其旁边的 ANGLE/SwiftShader dylib、`libcef_dll_wrapper.a` 以及 helper 可执行文件都会用 `lipo` 合并,随后逐个校验切片是否齐全。Chromium 的 V8 启动快照按架构区分文件名(`v8_context_snapshot.<arch>.bin`),两份都会放进 framework 的 `Resources`,运行时按架构选取;若 helper 缺少 App 正在构建的某个架构,"Embed CEF Helpers" 构建阶段会直接报错。所选取值会写入 `macos/third/cef/version.txt`,因此改动后下次 `pod install` 会重新准备 `macos/third/cef`。
|
||
|
||
### Linux
|
||
|
||
```bash
|
||
flutter pub add webview_cef
|
||
```
|
||
|
||
首次构建时自动下载 CEF(支持 x64 与 arm64)。请确保已安装 Flutter Linux 桌面工具链(`clang`、`cmake`、`ninja-build`、`libgtk-3-dev`、`pkg-config`)。
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
import 'package:webview_cef/webview_cef.dart';
|
||
|
||
class MyWebView extends StatefulWidget {
|
||
const MyWebView({super.key});
|
||
@override
|
||
State<MyWebView> createState() => _MyWebViewState();
|
||
}
|
||
|
||
class _MyWebViewState extends State<MyWebView> {
|
||
late final WebViewController _controller;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller = WebviewManager().createWebView(
|
||
loading: const Center(child: CircularProgressIndicator()),
|
||
);
|
||
_init();
|
||
}
|
||
|
||
Future<void> _init() async {
|
||
await WebviewManager().initialize(); // 整个 App 调用一次
|
||
_controller.setWebviewListener(WebviewEventsListener(
|
||
onUrlChanged: (url) => debugPrint('url => $url'),
|
||
onLoadEnd: (controller, url) => debugPrint('loaded => $url'),
|
||
));
|
||
await _controller.initialize('https://flutter.dev');
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
WebviewManager().quit(); // 仅在整个 App 退出时调用
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ValueListenableBuilder<bool>(
|
||
valueListenable: _controller,
|
||
builder: (_, ready, __) =>
|
||
ready ? _controller.webviewWidget : _controller.loadingWidget,
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
完整示例(地址栏、Cookie、JS 桥、DevTools)见 [`example/`](example/)。
|
||
|
||
---
|
||
|
||
## 用法
|
||
|
||
### 生命周期
|
||
|
||
```dart
|
||
await WebviewManager().initialize(userAgent: 'my-app/1.0'); // 每个 App 一次
|
||
final controller = WebviewManager().createWebView(loading: const Text('…'));
|
||
await controller.initialize('https://example.com');
|
||
// …
|
||
controller.dispose();
|
||
WebviewManager().quit(); // App 关闭时
|
||
```
|
||
|
||
### 导航
|
||
|
||
```dart
|
||
controller.loadUrl('https://example.com');
|
||
controller.reload();
|
||
controller.goBack();
|
||
controller.goForward();
|
||
controller.openDevTools();
|
||
```
|
||
|
||
### 事件
|
||
|
||
```dart
|
||
controller.setWebviewListener(WebviewEventsListener(
|
||
onTitleChanged: (title) {},
|
||
onUrlChanged: (url) {},
|
||
onLoadStart: (controller, url) {},
|
||
onLoadEnd: (controller, url) {},
|
||
onConsoleMessage: (level, message, source, line) {},
|
||
));
|
||
```
|
||
|
||
### JavaScript 桥
|
||
|
||
```dart
|
||
// Dart -> JS
|
||
controller.executeJavaScript("document.title = 'set from Dart'");
|
||
final result = await controller.evaluateJavascript("1 + 1"); // "2"
|
||
|
||
// JS -> Dart
|
||
controller.setJavaScriptChannels({
|
||
JavascriptChannel(
|
||
name: 'Print',
|
||
onMessageReceived: (msg) {
|
||
debugPrint(msg.message);
|
||
controller.sendJavaScriptChannelCallBack(
|
||
false, "{'code':'200'}", msg.callbackId, msg.frameId);
|
||
},
|
||
),
|
||
});
|
||
```
|
||
|
||
### Cookie
|
||
|
||
```dart
|
||
await WebviewManager().setCookie('example.com', 'key', 'value');
|
||
await WebviewManager().deleteCookie('example.com', 'key');
|
||
final all = await WebviewManager().visitAllCookies();
|
||
final some = await WebviewManager().visitUrlCookies('example.com', false);
|
||
```
|
||
|
||
### 用户脚本注入
|
||
|
||
```dart
|
||
final scripts = InjectUserScripts()
|
||
..add(UserScript("console.log('at document start')", ScriptInjectTime.LOAD_START))
|
||
..add(UserScript("console.log('at document end')", ScriptInjectTime.LOAD_END));
|
||
|
||
final controller = WebviewManager().createWebView(injectUserScripts: scripts);
|
||
```
|
||
|
||
---
|
||
|
||
## Windows 构建选项
|
||
|
||
以下 CMake 选项可设置在插件目标上(括号内为默认值):
|
||
|
||
| 选项 | 默认 | 作用 |
|
||
| --- | --- | --- |
|
||
| `WEBVIEW_CEF_GPU_TEXTURE` | `ON` | GPU 零拷贝渲染(CEF `OnAcceleratedPaint` → Flutter GPU 纹理)。设为 `OFF` 回退到软件像素缓冲路径。 |
|
||
| `WEBVIEW_CEF_USE_DEBUG_CEF` | `OFF` | 即使在 Debug 构建中也链接/打包 CEF 的 **Debug** 二进制。默认情况下 Debug 构建使用 Release 的 CEF 二进制,因为 CEF 的 Debug DCHECK 会在输入法过程中使离屏渲染崩溃。仅当你需要单步调试 CEF 自身时才打开。 |
|
||
|
||
## 升级 CEF
|
||
|
||
CEF/Chromium 版本只在一处管理 —— [`third/download.cmake`](third/download.cmake) 里的 `CEF_VERSION`。修改它即可升级三端的 CEF:Windows 与 Linux 自动下载,macOS 也读取同一个 `CEF_VERSION`(通过 podspec `prepare_command` 运行的 [`macos/scripts/download_cef.sh`](macos/scripts/download_cef.sh)),在下次 `pod install` 时重新下载,无需手动放置。
|
||
|
||
---
|
||
|
||
## 演示
|
||
|
||
<kbd></kbd>
|
||
|
||
### 截图
|
||
|
||
| Windows | macOS | Linux |
|
||
| --- | --- | --- |
|
||
| <img src="https://user-images.githubusercontent.com/7610615/190431027-6824fac1-015d-4091-b034-dd58f79adbcb.png" width="400" /> | <img src="https://user-images.githubusercontent.com/7610615/190911381-db88cf33-70a2-4abc-9916-e563e54eb3f9.png" width="400" /> | <img src ="https://github.com/hlwhl/webview_cef/assets/49640121/50a4c2f6-1f24-4d10-9913-ad274d76cf3f" width="400" /> |
|
||
| <img src="https://user-images.githubusercontent.com/7610615/190431037-62ba0ea7-f7d1-4fca-8ce1-596a0a508f93.png" width="400" /> | <img src="https://user-images.githubusercontent.com/7610615/190911410-bd01e912-5482-4f9e-9dae-858874e5aaed.png" width="400" /> | <img src="https://github.com/hlwhl/webview_cef/assets/49640121/10a693d5-4ee0-4389-a1e8-1b0355f7c0a6" width="400" /> |
|
||
|
||
---
|
||
|
||
## 路线图
|
||
|
||
- [x] Windows / macOS / Linux 支持
|
||
- [x] 多实例
|
||
- [x] JavaScript 桥与 Cookie 管理
|
||
- [x] 输入法支持(Windows / macOS / Linux;已用中文输入法测试)
|
||
- [x] 鼠标与触控板输入
|
||
- [x] DevTools
|
||
- [x] GPU 零拷贝渲染与自适应帧率(Windows 与 macOS)
|
||
- [x] 更简单的 macOS 多进程 helper bundle 集成(一行 Podfile 钩子)
|
||
- [x] macOS Universal(arm64 + x86_64)构建
|
||
- [ ] Windows 上无撕裂的 GPU 同步(keyed-mutex)
|
||
|
||
欢迎提交 PR。每个 PR 都会在 Windows、macOS、Linux 上运行构建 + `flutter analyze` CI。
|
||
|
||
## 致谢
|
||
|
||
灵感来自 [**`flutter_webview_windows`**](https://github.com/jnschulze/flutter-webview-windows)。
|
||
|
||
## 许可证
|
||
|
||
[Apache License 2.0](LICENSE)。
|