C++使用WebView2控件,通过IPC通信与Javascript交互

引言

在现代桌面应用程序开发中,Web技术与原生应用的融合变得越来越普遍。Microsoft的WebView2控件为C++开发者提供了一个强大的工具,使他们能够在桌面应用中嵌入基于Chromium的Web浏览器引擎。本文将详细介绍如何在C++应用程序中使用WebView2控件,并通过IPC(进程间通信)机制实现C++与JavaScript之间的双向交互。

WebView2简介

WebView2是Microsoft推出的新一代Web视图控件,基于Chromium引擎,替代了旧的MSHTML(IE)引擎。它允许开发者在Windows应用程序中嵌入Web内容,并提供了丰富的API用于控制Web视图和与Web内容交互。

WebView2的主要优势

  • 现代Web标准支持:基于Chromium引擎,支持最新的HTML5、CSS3和JavaScript特性
  • 与系统浏览器独立:不依赖于系统安装的浏览器版本
  • 强大的通信机制:提供多种方式实现本地代码与Web内容的通信
  • 安全性:Web内容在独立的进程中运行,提高了应用的稳定性和安全性

环境准备

在开始开发之前,需要准备以下环境:

  1. Visual Studio:推荐使用Visual Studio 2019或更高版本
  2. WebView2 SDK:可以通过NuGet包管理器安装
  3. C++开发环境:确保已安装C++桌面开发工作负载

安装WebView2 SDK

使用NuGet包管理器安装WebView2 SDK:

powershell 复制代码
Install-Package Microsoft.Web.WebView2

或者在Visual Studio的NuGet包管理器中搜索并安装"Microsoft.Web.WebView2"。

创建基本WebView2应用

首先,我们来创建一个基本的WebView2应用程序。以下是一个简单的示例,展示如何在Win32应用程序中嵌入WebView2控件:

cpp 复制代码
#include <windows.h>
#include <wrl.h>
#include <wil/com.h>
#include <WebView2.h>
#include <WebView2EnvironmentOptions.h>

using namespace Microsoft::WRL;

// WebView2控件
static wil::com_ptr<ICoreWebView2Controller> webViewController;
static wil::com_ptr<ICoreWebView2> webView;

// 初始化WebView2
HRESULT InitializeWebView(HWND hWnd)
{
    // 创建WebView2环境
    return CreateCoreWebView2EnvironmentWithOptions(nullptr, nullptr, nullptr,
        Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
            [hWnd](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
                // 创建WebView2控制器
                env->CreateCoreWebView2Controller(hWnd, 
                    Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
                        [hWnd](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
                            if (controller != nullptr) {
                                webViewController = controller;
                                webViewController->get_CoreWebView2(&webView);

                                // 设置WebView2的大小
                                RECT bounds;
                                GetClientRect(hWnd, &bounds);
                                webViewController->put_Bounds(bounds);

                                // 导航到初始URL
                                webView->Navigate(L"https://www.example.com");
                            }
                            return S_OK;
                        }).Get());
                return S_OK;
            }).Get());
}

// 窗口过程
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
        InitializeWebView(hWnd);
        return 0;
    case WM_SIZE:
        if (webViewController != nullptr) {
            RECT bounds;
            GetClientRect(hWnd, &bounds);
            webViewController->put_Bounds(bounds);
        }
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
}

// 程序入口点
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    // 注册窗口类
    WNDCLASSEX wcex = {};
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WindowProc;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.lpszClassName = L"WebView2Sample";
    RegisterClassEx(&wcex);

    // 创建窗口
    HWND hWnd = CreateWindow(
        L"WebView2Sample", L"WebView2 Sample",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 1200, 900,
        nullptr, nullptr, hInstance, nullptr);

    if (!hWnd) {
        return 1;
    }

    // 显示窗口
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    // 消息循环
    MSG msg = {};
    while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

这段代码创建了一个简单的窗口,并在其中嵌入了WebView2控件,导航到example.com网站。

实现C++与JavaScript的IPC通信

WebView2提供了多种方式实现C++与JavaScript之间的通信。最常用的两种方式是:

  1. PostMessage/AddScriptToExecuteOnDocumentCreated:用于在C++和JavaScript之间传递消息
  2. WebMessage API:提供了更结构化的消息传递机制

从C++调用JavaScript

可以使用ExecuteScript方法从C++调用JavaScript代码:

cpp 复制代码
// 从C++调用JavaScript函数
void CallJavaScriptFunction(const std::wstring& functionName, const std::wstring& parameter)
{
    if (webView) {
        std::wstring script = functionName + L"('" + parameter + L"');";
        webView->ExecuteScript(script.c_str(), nullptr);
    }
}

// 示例:调用JavaScript函数
CallJavaScriptFunction(L"updateStatus", L"Connected from C++");

从JavaScript调用C++

要从JavaScript调用C++代码,我们可以使用AddHostObjectToScriptWebMessageReceived事件。以下是使用WebMessageReceived的示例:

cpp 复制代码
// 设置WebMessage处理程序
void SetupWebMessageHandler()
{
    if (webView) {
        // 注册消息处理程序
        webView->add_WebMessageReceived(
            Callback<ICoreWebView2WebMessageReceivedEventHandler>(
                [](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
                    LPWSTR message;
                    args->TryGetWebMessageAsString(&message);
                    
                    // 处理来自JavaScript的消息
                    ProcessMessageFromJs(message);
                    
                    CoTaskMemFree(message);
                    return S_OK;
                }).Get(), nullptr);
                
        // 注入JavaScript代码,使Web页面能够向C++发送消息
        webView->AddScriptToExecuteOnDocumentCreated(
            L"window.chrome.webview.postMessage = function(message) {"
            L"    window.chrome.webview.postMessage(message);"
            L"};"
            L"window.callNative = function(functionName, parameter) {"
            L"    window.chrome.webview.postMessage(JSON.stringify({function: functionName, param: parameter}));"
            L"};",
            nullptr);
    }
}

// 处理来自JavaScript的消息
void ProcessMessageFromJs(const std::wstring& message)
{
    // 这里可以解析JSON消息,执行相应的本地功能
    // 例如,可以使用JSON库解析message,然后根据function字段调用不同的C++函数
    
    // 简单示例:打印消息
    OutputDebugString((L"Message from JS: " + message + L"\n").c_str());
    
    // 响应消息(可选)
    if (webView) {
        webView->PostWebMessageAsString(L"Message received by C++");
    }
}

在JavaScript端,可以这样调用C++:

javascript 复制代码
// 调用C++函数
function callCppFunction(functionName, parameter) {
    window.chrome.webview.postMessage(JSON.stringify({
        function: functionName,
        param: parameter
    }));
}

// 示例:调用C++函数
callCppFunction('saveData', 'This is data from JavaScript');

// 接收C++的响应
window.chrome.webview.addEventListener('message', function(event) {
    console.log('Response from C++:', event.data);
});

完整的IPC通信示例

下面是一个更完整的示例,展示了C++和JavaScript之间的双向通信:

C++部分

cpp 复制代码
#include <windows.h>
#include <wrl.h>
#include <wil/com.h>
#include <WebView2.h>
#include <WebView2EnvironmentOptions.h>
#include <string>
#include <sstream>
#include <nlohmann/json.hpp>

using namespace Microsoft::WRL;
using json = nlohmann::json;

// WebView2控件
static wil::com_ptr<ICoreWebView2Controller> webViewController;
static wil::com_ptr<ICoreWebView2> webView;

// 处理来自JavaScript的消息
void ProcessMessageFromJs(const std::wstring& messageW)
{
    // 将wstring转换为string以便使用JSON库
    std::string message(messageW.begin(), messageW.end());
    
    try {
        auto jsonData = json::parse(message);
        std::string function = jsonData["function"];
        std::string param = jsonData["param"];
        
        // 根据function字段调用不同的C++函数
        if (function == "saveData") {
            // 保存数据的示例实现
            OutputDebugStringA(("Saving data: " + param + "\n").c_str());
            
            // 响应JavaScript
            if (webView) {
                webView->PostWebMessageAsString(L"Data saved successfully");
            }
        }
        else if (function == "getData") {
            // 获取数据的示例实现
            std::string data = "Sample data from C++ - " + param;
            OutputDebugStringA(("Getting data for: " + param + "\n").c_str());
            
            // 调用JavaScript函数返回数据
            if (webView) {
                std::wstring script = L"receiveDataFromCpp('" + std::wstring(data.begin(), data.end()) + L"');";
                webView->ExecuteScript(script.c_str(), nullptr);
            }
        }
    }
    catch (const std::exception& e) {
        OutputDebugStringA(("Error parsing JSON: " + std::string(e.what()) + "\n").c_str());
    }
}

// 初始化WebView2
HRESULT InitializeWebView(HWND hWnd)
{
    // 创建WebView2环境
    return CreateCoreWebView2EnvironmentWithOptions(nullptr, nullptr, nullptr,
        Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
            [hWnd](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
                // 创建WebView2控制器
                env->CreateCoreWebView2Controller(hWnd, 
                    Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
                        [hWnd](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
                            if (controller != nullptr) {
                                webViewController = controller;
                                webViewController->get_CoreWebView2(&webView);

                                // 设置WebView2的大小
                                RECT bounds;
                                GetClientRect(hWnd, &bounds);
                                webViewController->put_Bounds(bounds);
                                
                                // 设置WebMessage处理程序
                                webView->add_WebMessageReceived(
                                    Callback<ICoreWebView2WebMessageReceivedEventHandler>(
                                        [](ICoreWebView2* sender, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
                                            LPWSTR message;
                                            args->TryGetWebMessageAsString(&message);
                                            
                                            // 处理来自JavaScript的消息
                                            ProcessMessageFromJs(message);
                                            
                                            CoTaskMemFree(message);
                                            return S_OK;
                                        }).Get(), nullptr);
                                
                                // 注入JavaScript代码
                                webView->AddScriptToExecuteOnDocumentCreated(
                                    L"window.callNative = function(functionName, parameter) {"
                                    L"    window.chrome.webview.postMessage(JSON.stringify({function: functionName, param: parameter}));"
                                    L"};"
                                    L"window.receiveDataFromCpp = function(data) {"
                                    L"    document.getElementById('result').innerText = data;"
                                    L"};",
                                    nullptr);
                                
                                // 导航到本地HTML文件
                                webView->Navigate(L"file:///C:/path/to/your/index.html");
                            }
                            return S_OK;
                        }).Get());
                return S_OK;
            }).Get());
}

// 窗口过程
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_CREATE:
        InitializeWebView(hWnd);
        return 0;
    case WM_SIZE:
        if (webViewController != nullptr) {
            RECT bounds;
            GetClientRect(hWnd, &bounds);
            webViewController->put_Bounds(bounds);
        }
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
}

// 程序入口点
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    // 注册窗口类
    WNDCLASSEX wcex = {};
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WindowProc;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.lpszClassName = L"WebView2IPCSample";
    RegisterClassEx(&wcex);

    // 创建窗口
    HWND hWnd = CreateWindow(
        L"WebView2IPCSample", L"WebView2 IPC Sample",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 1200, 900,
        nullptr, nullptr, hInstance, nullptr);

    if (!hWnd) {
        return 1;
    }

    // 显示窗口
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    // 消息循环
    MSG msg = {};
    while (GetMessage(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

HTML/JavaScript部分 (index.html)

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>WebView2 IPC Demo</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        button {
            margin: 10px 0;
            padding: 8px 16px;
        }
        #result {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ccc;
            min-height: 100px;
        }
    </style>
</head>
<body>
    <h1>WebView2 IPC通信演示</h1>
    
    <div>
        <input type="text" id="inputData" placeholder="输入数据">
        <button onclick="saveData()">保存数据到C++</button>
        <button onclick="getData()">从C++获取数据</button>
    </div>
    
    <div>
        <h3>结果:</h3>
        <div id="result"></div>
    </div>
    
    <script>
        // 保存数据到C++
        function saveData() {
            const data = document.getElementById('inputData').value;
            window.callNative('saveData', data);
        }
        
        // 从C++获取数据
        function getData() {
            const query = document.getElementById('inputData').value || 'default';
            window.callNative('getData', query);
        }
        
        // 接收C++的响应
        window.chrome.webview.addEventListener('message', function(event) {
            document.getElementById('result').innerText = event.data;
        });
    </script>
</body>
</html>

高级IPC通信技术

除了基本的消息传递外,WebView2还提供了一些高级的IPC通信技术:

1. 使用AddHostObjectToScript

AddHostObjectToScript允许将C++对象直接暴露给JavaScript,使JavaScript可以直接调用C++对象的方法:

cpp 复制代码
// 定义要暴露给JavaScript的COM对象
class HostObject : public Microsoft::WRL::RuntimeClass<
    Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
    IDispatch> {
public:
    // IDispatch实现...
    
    // 示例方法
    HRESULT Add(int a, int b, int* result) {
        *result = a + b;
        return S_OK;
    }
};

// 将对象暴露给JavaScript
void ExposeHostObjectToJs()
{
    if (webView) {
        auto hostObject = Make<HostObject>();
        webView->AddHostObjectToScript(L"cppObject", hostObject.Get());
    }
}

在JavaScript中:

javascript 复制代码
// 直接调用C++对象的方法
const result = window.chrome.webview.hostObjects.cppObject.Add(5, 3);
console.log('Result:', result);

2. 使用DevToolsProtocol

WebView2还支持Chrome DevTools Protocol (CDP),可以用于高级调试和控制:

cpp 复制代码
// 使用CDP发送命令
void SendDevToolsCommand()
{
    if (webView) {
        webView->CallDevToolsProtocolMethod(L"Network.enable", L"{}", nullptr);
    }
}

3. 使用自定义事件

可以使用自定义事件在C++和JavaScript之间建立更结构化的通信:

cpp 复制代码
// C++中发送事件
void SendEventToJs(const std::wstring& eventName, const std::wstring& eventData)
{
    if (webView) {
        std::wstring script = L"document.dispatchEvent(new CustomEvent('" + 
            eventName + L"', { detail: " + eventData + L" }));";
        webView->ExecuteScript(script.c_str(), nullptr);
    }
}

在JavaScript中:

javascript 复制代码
// 监听来自C++的事件
document.addEventListener('myCustomEvent', function(event) {
    console.log('Event received:', event.detail);
});

性能优化与最佳实践

在使用WebView2进行IPC通信时,有一些性能优化和最佳实践需要注意:

1. 批量处理消息

对于频繁的通信,应该考虑批量处理消息,而不是每次都单独发送:

cpp 复制代码
// C++批量发送数据
void SendBatchData(const std::vector<std::wstring>& dataItems)
{
    if (webView && !dataItems.empty()) {
        std::wstringstream ss;
        ss << L"[";
        for (size_t i = 0; i < dataItems.size(); ++i) {
            ss << L"'" << dataItems[i] << L"'";
            if (i < dataItems.size() - 1) {
                ss << L",";
            }
        }
        ss << L"]";
        
        std::wstring script = L"processBatchData(" + ss.str() + L");";
        webView->ExecuteScript(script.c_str(), nullptr);
    }
}

2. 使用二进制数据传输

对于大量数据,考虑使用二进制格式而不是文本格式:

cpp 复制代码
// 在JavaScript中使用ArrayBuffer
function sendBinaryData() {
    const buffer = new ArrayBuffer(1024);
    const view = new Uint8Array(buffer);
    // 填充数据...
    
    // 将ArrayBuffer转换为Base64
    const base64 = arrayBufferToBase64(buffer);
    window.callNative('processBinaryData', base64);
}

function arrayBufferToBase64(buffer) {
    let binary = '';
    const bytes = new Uint8Array(buffer);
    for (let i = 0; i < bytes.byteLength; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
}

在C++中:

cpp 复制代码
// 处理二进制数据
void ProcessBinaryData(const std::string& base64Data)
{
    // 解码Base64数据
    // 处理二进制数据...
}

3. 避免频繁的DOM操作

在JavaScript中,避免频繁的DOM操作,可以使用虚拟DOM或批量更新:

javascript 复制代码
// 批量更新DOM
function updateElements(dataArray) {
    const fragment = document.createDocumentFragment();
    
    dataArray.forEach(item => {
        const div = document.createElement('div');
        div.textContent = item;
        fragment.appendChild(div);
    });
    
    document.getElementById('container').appendChild(fragment);
}

4. 使用异步通信

对于不需要立即响应的操作,使用异步通信可以提高性能:

cpp 复制代码
// C++中异步处理消息
void ProcessMessageAsync(const std::wstring& message)
{
    // 在另一个线程中处理消息
    std::thread([message]() {
        // 处理消息...
        
        // 处理完成后通知JavaScript
        // 注意:需要在UI线程中调用ExecuteScript
    }).detach();
}

安全性考虑

在使用WebView2进行IPC通信时,需要注意以下安全问题:

1. 输入验证

始终验证从JavaScript接收的数据,防止注入攻击:

cpp 复制代码
// 验证输入数据
bool ValidateInput(const std::wstring& input)
{
    // 实现适当的验证逻辑
    return true; // 示例
}

2. 限制JavaScript访问权限

只暴露必要的功能给JavaScript:

cpp 复制代码
// 限制JavaScript访问权限
void LimitJsAccess()
{
    if (webView) {
        // 设置WebView2的权限
        ICoreWebView2Settings* settings;
        webView->get_Settings(&settings);
        
        // 禁用JavaScript对文件系统的访问
        settings->put_IsWebMessageEnabled(TRUE);
        settings->put_AreDefaultScriptDialogsEnabled(FALSE);
        settings->put_IsScriptEnabled(TRUE);
        settings->put_AreDevToolsEnabled(FALSE);
    }
}

3. 使用内容安全策略

在HTML中使用内容安全策略限制JavaScript的行为:

html 复制代码
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

调试技巧

调试WebView2应用程序中的IPC通信可能具有挑战性。以下是一些有用的调试技巧:

1. 启用开发者工具

在开发过程中启用WebView2的开发者工具:

cpp 复制代码
// 启用开发者工具
void EnableDevTools()
{
    if (webView) {
        ICoreWebView2Settings* settings;
        webView->get_Settings(&settings);
        settings->put_AreDevToolsEnabled(TRUE);
        
        // 打开开发者工具
        webView->OpenDevToolsWindow();
    }
}

2. 日志记录

在C++和JavaScript中添加详细的日志记录:

cpp 复制代码
// C++日志记录
void LogMessage(const std::wstring& message)
{
    OutputDebugString((L"[C++] " + message + L"\n").c_str());
}
javascript 复制代码
// JavaScript日志记录
function logMessage(message) {
    console.log(`[JS] ${message}`);
}

3. 使用事件监听器调试通信

在JavaScript中添加事件监听器来监视通信:

javascript 复制代码
// 监视所有WebMessage通信
window.chrome.webview.addEventListener('message', function(event) {
    console.log('Message from C++:', event.data);
});

实际应用场景

WebView2的IPC通信机制可以应用于多种场景:

1. 混合桌面应用

创建具有原生性能和Web界面优势的混合应用:

  • 使用C++处理复杂计算、文件操作和系统集成
  • 使用Web技术创建现代化、响应式的用户界面

2. 离线Web应用

创建可以在没有互联网连接的情况下工作的Web应用:

  • 使用C++处理本地数据存储和同步
  • 使用Web技术提供用户界面

3. 扩展现有应用

为现有的C++应用程序添加Web功能:

  • 逐步将传统应用程序的部分UI迁移到Web技术
  • 保留现有的C++业务逻辑

结论

WebView2为C++开发者提供了一个强大的工具,使他们能够在桌面应用中嵌入现代Web技术,并通过IPC机制实现C++与JavaScript之间的无缝通信。通过本文介绍的技术,开发者可以创建兼具原生性能和Web灵活性的应用程序。

参考资料

  1. Microsoft WebView2 官方文档
  2. WebView2 Samples
  3. WebView2 API Reference
  4. C++/WinRT Documentation
  5. Windows App SDK
相关推荐
李匠202411 分钟前
C++学习之工厂模式-套接字通信
c++·学习
freyazzr24 分钟前
Leedcode刷题 | Day30_贪心算法04
数据结构·c++·算法·leetcode·贪心算法
magic 2451 小时前
ES6变量声明:let、var、const全面解析
前端·javascript·ecmascript·es6
好_快1 小时前
Lodash源码阅读-dropWhile
前端·javascript·源码阅读
好_快1 小时前
Lodash源码阅读-dropRightWhile
前端·javascript·源码阅读
请叫我欧皇i2 小时前
vue2使用ezuikit-js播放萤石视频
开发语言·javascript·ecmascript
李匠20242 小时前
C++学习之金融类安全传输平台项目git
c++·学习
IT专家-大狗3 小时前
Google Chrome Canary版官方下载及安装教程【适用于开发者与进阶用户】
开发语言·javascript·chrome·ecmascript
Hello eveybody4 小时前
C++二进制
c++
喜欢便码7 小时前
JS小练习0.1——弹出姓名
java·前端·javascript