在 WebView2 应用中处理本地内容

除了加载远程内容外,还可以将内容本地加载到 WebView2 中。 有几种方法可用于将本地内容加载到 WebView2 控件中,包括:

  • 导航到文件 URL。
  • 导航到 HTML 字符串。
  • 虚拟主机名映射。
  • WebResourceRequested处理事件。

下面介绍了这些方法。

选择方法

将本地内容加载到 WebView2 控件的各种方法支持以下方案:

应用场景 导航到文件 URL 导航到 HTML 字符串 通过使用虚拟主机名映射 通过使用 WebResourceRequested
基于源的 DOM API ✔️ ✔️ ✔️
需要安全上下文的 DOM API ✔️ ✔️
动态内容 ✔️ ✔️
其他 Web 资源 ✔️ ✔️ ✔️
在 WebView2 进程中解析的其他 Web 资源 ✔️ ✔️

下面将更详细地介绍这些方案。

通过导航到文件 URL 加载本地内容

WebView2 允许导航至文件 URL,以加载基本 HTML 或 PDF。 这是加载本地内容的最简单、最有效的方法。 但是,它不如其他方法灵活。 与在 Web 浏览器中一样,文件 URL 在某些功能上受到限制:

  • 文档的源对其文件路径是唯一的。 这意味着需要源(如 或indexedDBlocalStorage的 Web API 将正常工作,但存储的数据将不适用于从其他文件路径加载的其他本地文档。
  • 某些 Web API 仅限于安全 HTTPS URL,不适用于文件 URL 加载的文档。 这包括用于获取视频或声音、navigator.geolocation.getCurrentPosition()访问设备位置或Notification.requestPermission()请求用户显示通知权限等 navigator.mediaDevices.getUserMedia() API。
  • 对于每个资源,必须指定完整路径。
  • 若要允许从文件 URI 引用其他本地文件,或显示应用了 XSL 转换的 XML 文件,可以设置 --allow-file-access-from-files 浏览器参数。 请参阅 CoreWebView2EnvironmentOptions.AdditionalBrowserArguments 属性

通过导航到文件 URL 加载本地内容的注意事项

文件 URL 的行为与在浏览器中的行为类似。 例如,不能 XMLHttpRequest 在文件 URL 中创建 (XHR) ,因为你不在网页的上下文中工作。

必须为每个资源指定文件的完整路径。 例如:

file:///C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html
跨源资源

指定文件 URL 时,应用将导航到磁盘上的文件,而不是网络上的域。 因此,无法在生成的文档中使用跨源资源。

基于源的 DOM API

通过文件 URL 加载的文档的源对其文件路径是唯一的,就像在浏览器中一样。 需要源(如 或indexedDBlocalStorage的 Web API 将正常工作。 但是,从不同文件 URL 加载的不同文档不被视为来自同一源,并且无法访问相同的存储数据。

需要安全上下文的 DOM API

某些 Web API 仅限于安全 HTTPS URL,不适用于文件 URL 加载的文档。 这包括用于获取视频或声音、navigator.geolocation.getCurrentPosition()访问设备位置或Notification.requestPermission()请求用户显示通知权限等 navigator.mediaDevices.getUserMedia() API。 有关详细信息 ,请参阅保护 MDN 上的上下文。

动态内容

通过文件 URL 加载文档时,文档的内容来自磁盘上的静态文件。 这意味着无法动态修改此本地内容。 这不同于从 Web 服务器加载文档,因为 Web 服务器可以动态生成每个响应。

其他 Web 资源

相对 URL 解析也适用于通过文件 URL 加载的文档。 这意味着加载的文档可以引用其他 Web 资源,例如 CSS、脚本或图像文件,这些文件也通过文件 URL 提供。

在 WebView2 进程中解析的其他 Web 资源

文件 URL 在 WebView2 进程中解析。 此选项比 WebResourceRequested在主机应用进程 UI 线程中解析的速度更快。

用于通过导航到文件 URL 加载本地内容的 API

文件 URL 示例

本部分以独立于平台的方式显示本地内容文件路径的文件 URL 的外观。

WebView2 应用需要使用前缀和正斜杠对本地文件 URL 进行 file:/// 编码。 例如,对于演示待办示例,路径为:

file:///C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html

复制本地文件具有“file”前缀的完整路径:

  1. (可选)克隆 Demos 存储库,以便拥有本地副本。 请参阅安装用于Visual Studio Code的 DevTools 扩展中的步骤 5:克隆演示存储库

  2. 在 Microsoft Edge 中,按 Ctrl+O 打开文件。 打开本地 .html 文件,例如本地克隆的文件 Demos/demo-to-do/index.html

    C:\Users\username\Documents\GitHub\Demos\demo-to-do\index.html

    地址栏最初不显示 file:/// 前缀,但以驱动器号开头:

    C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html
    

    Microsoft Edge 最初隐藏 file:/// 前缀的地址栏

  3. 单击“地址栏”,然后按 “开始 ”键,或按 Ctrl+A 选择整个路径。

    Microsoft Edge 的地址栏现在显示 file:/// 前缀

    整个文件路径(包括) file:/// 将复制到剪贴板缓冲区中,因此可以粘贴包含 file:/// 前缀的完整路径:

    file:///C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html
    

另请参阅:

导航到文件 URL 的示例

webView.CoreWebView2.Navigate(
          "file:///C:/Users/username/Documents/GitHub/Demos/demo-to-do/index.html");

通过导航到 HTML 字符串加载本地内容

加载本地内容的另一种方法是 方法 NavigateToString 。 此方法直接从字符串将内容加载到 WebView2 中。 如果要通过应用代码打包内容,或者想要动态创建内容,则这非常有用。

若要加载无法通过 URL 访问的内容,则导航到字符串可能很有用。 例如,如果有 HTML 文档的内存中表示形式,则可以使用 NavigateToString 方法将内容加载到 WebView2 控件中。 如果要避免在将内容加载到控件之前需要将内容写入文件或服务器,这将非常有用。

通过导航到 HTML 字符串加载本地内容的注意事项

基于源的 DOM API

使用 NavigateToString 方法加载的文档的位置设置为 about:blank ,其原点设置为 null。 这意味着不能使用依赖于所定义的源的 Web API,例如 localStorageindexedDB

需要安全上下文的 DOM API

某些 Web API 仅限于安全的 HTTPS URL,并且对于通过 方法加载的文档不可用, NavigateToString 因为它们的位置设置为 about:blank。 这包括用于获取视频或声音、navigator.geolocation.getCurrentPosition()访问设备位置或Notification.requestPermission()请求用户显示通知权限等 navigator.mediaDevices.getUserMedia() API。 有关详细信息 ,请参阅保护 MDN 上的上下文。

动态内容

通过 NavigateToString 方法加载本地内容时,直接将内容作为参数提供给方法。 这意味着可以在运行时控制内容,并且可以根据需要动态生成内容。

其他 Web 资源

使用 NavigateToString 方法加载本地内容不会使生成的文档引用其他 Web 资源(如 CSS、图像或脚本文件)。 方法仅允许指定 HTML 文档的字符串内容。 若要从 HTML 文档中引用其他 Web 资源,请使用本文中所述的其他方法之一,或在 HTML 文档中内联表示这些附加的 Web 资源。

在 WebView2 进程中解析的其他 Web 资源

NavigateToString 不支持其他 Web 资源,如上所述。

用于通过导航到 HTML 字符串加载本地内容的 API

网页的示例字符串表示形式

下面是 演示要执行的操作 网页的字符串表示形式。 为了提高可读性,下面的列表添加了换行。 实际上,这些行串联成一条长线:

`<html lang="en"><head>\n    
<meta charset="UTF-8">\n    
<meta name="viewport" content="width=device-width, initial-scale=1.0">\n    
<title>TODO app</title>\n    
<link rel="stylesheet" href="styles/light-theme.css" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)">\n    
<link rel="stylesheet" href="styles/dark-theme.css" media="(prefers-color-scheme: dark)">\n    
<link rel="stylesheet" href="styles/base.css">\n    
<link rel="stylesheet" href="styles/to-do-styles.css">\n    
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📋
</text></svg>">\n  
</head>\n\n  
<body>\n    
<h1>📋 My tasks</h1>\n    
<form>\n      
<div class="new-task-form" tabindex="0">\n        
<label for="new-task">➕ Add a task</label>\n        
<input id="new-task" autocomplete="off" type="text" placeholder="Try typing 'Buy milk'" title="Click to start adding a task">\n        
<input type="submit" value="➡️">\n      
</div>\n      
<ul id="tasks"><li class="divider">No tasks defined</li></ul>\n    
</form>\n\n    \x3Cscript src="to-do.js">\x3C/script>\n  \n\n
</body>
</html>`

若要获取上述字符串,请执行以下命令:

  1. 转到 演示操作

  2. 右键单击网页,然后选择“ 检查 ”以打开 DevTools。

  3. 在 DevTools 的 控制台 中,输入: document.body.parentElement.outerHTML控制台输出网页的字符串表示形式:

    演示要执行的操作网页的字符串表示形式

导航到 HTML 字符串的示例

// Define htmlString with the string representation of HTML as above.
webView.CoreWebView2.NavigateToString(htmlString);

使用虚拟主机名映射加载本地内容

在 WebView2 控件中加载本地内容的另一种方法是使用虚拟主机名映射。 这涉及到将本地域名映射到本地文件夹,以便在 WebView2 控件尝试加载该域的资源时,它将改为从指定的本地文件夹位置加载内容。 文档的来源也将是虚拟主机名。

此方法允许使用 CoreWebView2HostResourceAccessKind 枚举指定跨域访问。

由于当前限制,使用虚拟主机名访问的媒体文件加载速度可能会很慢。

使用虚拟主机名映射加载本地内容的注意事项

基于源的 DOM API

通过虚拟主机名映射加载的本地内容会导致文档中具有 HTTP 或 HTTPS URL 以及相应的源。 这意味着需要源(如 或indexedDBlocalStorage的 Web API 将正常工作,属于同一源的其他文档将能够使用存储的数据。 有关详细信息,请参阅 MDN 上的 同源策略

需要安全上下文的 DOM API

某些 Web API 仅限于安全 HTTPS URL。 使用虚拟主机名映射可为本地内容提供 HTTPS URL。 这意味着可以使用 API,例如 navigator.mediaDevices.getUserMedia() 获取视频或声音、 navigator.geolocation.getCurrentPosition() 访问设备位置或 Notification.requestPermission() 请求用户显示通知的权限。 有关详细信息 ,请参阅保护 MDN 上的上下文。

动态内容

通过虚拟主机名映射加载本地内容时,你将将虚拟主机名映射到包含磁盘上静态文件的本地文件夹。 这意味着无法动态修改此本地内容。 这不同于从 Web 服务器加载文档,因为 Web 服务器可以动态生成每个响应。

其他 Web 资源

通过虚拟主机名映射加载的本地内容具有支持相对 URL 解析的 HTTP 或 HTTPS URL。 这意味着加载的文档可以引用其他 Web 资源,例如 CSS、脚本或图像文件,这些资源也通过虚拟主机名映射提供。

在 WebView2 进程中解析的其他 Web 资源

虚拟主机名 URL 在 WebView2 进程中解析。 此选项比 WebResourceRequested在主机应用进程 UI 线程中解析的速度更快。

使用虚拟主机名映射加载本地内容的 API

虚拟主机名映射示例

webView.CoreWebView2.SetVirtualHostNameToFolderMapping("demo", 
         "C:\Github\Demos\demo-to-do", CoreWebView2HostResourceAccessKind.DenyCors);
webView.CoreWebView2.Navigate("https://demo/index.html");

通过处理 WebResourceRequested 事件加载本地内容

在 WebView2 控件中托管本地内容的另一种方法是依赖于 WebResourceRequested 事件。 当控件尝试加载资源时,会触发此事件。 可以使用此事件来截获请求并提供本地内容,如 网络请求的自定义管理中所述。

WebResourceRequested 允许你基于每个请求自定义本地内容的行为。 这意味着你可以决定要拦截哪些请求并提供你自己的内容,以及让 WebView2 控件正常处理的请求。 但是,自定义行为需要更多代码(例如虚拟主机映射),并且需要了解 HTTP 才能构造正确的响应。

从 WebView2 的角度来看,资源将通过网络提供,WebView2 将遵循应用在响应过程中设置的标头。 WebResourceRequested使用事件的速度也比其他方法慢,因为每个请求都需要跨进程通信和处理。

通过处理 WebResourceRequested 事件加载本地内容的注意事项

基于源的 DOM API

通过 WebResourceRequested 加载的本地内容会导致文档中具有 HTTP 或 HTTPS URL 以及相应的源。 这意味着需要源(如 或indexedDBlocalStorage的 Web API 将正常工作,属于同一源的其他文档将能够使用存储的数据。 有关详细信息,请参阅 MDN 上的 同源策略

需要安全上下文的 DOM API

某些 Web API 仅限于安全 HTTPS URL。 使用 WebResourceRequested 可将 HTTPS URL Web 资源请求替换为自己的本地内容。 这意味着可以使用 API,例如 navigator.mediaDevices.getUserMedia() 获取视频或声音、 navigator.geolocation.getCurrentPosition() 访问设备位置或 Notification.requestPermission() 请求用户显示通知的权限。 有关详细信息 ,请参阅保护 MDN 上的上下文。

动态内容

通过 WebResourceRequested加载本地内容时,指定要在事件处理程序中加载的本地内容。 这意味着可以在运行时控制内容,并且可以根据需要动态生成内容。

其他 Web 资源

WebResourceRequested 修改通过 HTTP 或 HTTPS URL 加载的内容,这些 URL 支持相对 URL 解析。 这意味着生成的文档可以引用其他 Web 资源,例如 CSS、脚本或图像文件,这些文件也通过 WebResourceRequested提供。

在 WebView2 进程中解析的其他 Web 资源

通过文件 URL 或虚拟主机名映射加载内容时,解析将在 WebView2 进程中发生。 但是,该 WebResourceRequested 事件在主机应用进程的 WebView2 UI 线程上引发,这可能会导致生成的文档加载速度变慢。

  1. WebView2 首先暂停加载网页,以便等待事件发送到主机应用进程。
  2. 然后,WebView2 等待 UI 线程可用。
  3. 然后,WebView2 等待应用代码处理事件。

这可能需要一些时间。 请确保仅对必须引发WebResourceRequested事件的 Web 资源的调用AddWebResourceRequestedFilter

用于通过处理 WebResourceRequested 事件加载本地内容的 API

处理 WebResourceRequested 事件的示例

// Reading of response content stream happens asynchronously, and WebView2 does not 
// directly dispose the stream once it read.  Therefore, use the following stream
// class, which properly disposes when WebView2 has read all data.  For details, see
// [CoreWebView2 does not close stream content](https://github.com/MicrosoftEdge/WebView2Feedback/issues/2513).
class ManagedStream : Stream {
    public ManagedStream(Stream s)
    {
        s_ = s;
    }

    public override bool CanRead => s_.CanRead;

    public override bool CanSeek => s_.CanSeek;

    public override bool CanWrite => s_.CanWrite;

    public override long Length => s_.Length;

    public override long Position { get => s_.Position; set => s_.Position = value; }

    public override void Flush()
    {
        throw new NotImplementedException();
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        return s_.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        int read = 0;
        try
        {
            read = s_.Read(buffer, offset, count);
            if (read == 0)
            {
                s_.Dispose();
            }
        } 
        catch
        {
            s_.Dispose();
            throw;
        }
        return read;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

   private Stream s_;
}
webView.CoreWebView2.AddWebResourceRequestedFilter("https://demo/*", 
                                                CoreWebView2WebResourceContext.All);
webView.CoreWebView2.WebResourceRequested += delegate (object sender, 
                                     CoreWebView2WebResourceRequestedEventArgs args)
{
    string assetsFilePath = "C:\\Demo\\" + 
                            args.Request.Uri.Substring("https://demo/*".Length - 1);
    try
    {
        FileStream fs = File.OpenRead(assetsFilePath);
        ManagedStream ms = new ManagedStream(fs);
        string headers = "";
        if (assetsFilePath.EndsWith(".html"))
        {
            headers = "Content-Type: text/html";
        }
        else if (assetsFilePath.EndsWith(".jpg"))
        {
            headers = "Content-Type: image/jpeg";
        } else if (assetsFilePath.EndsWith(".png"))
        {
            headers = "Content-Type: image/png";
        }
        else if (assetsFilePath.EndsWith(".css"))
        {
            headers = "Content-Type: text/css";
        }
        else if (assetsFilePath.EndsWith(".js"))
        {
            headers = "Content-Type: application/javascript";
        }

        args.Response = webView.CoreWebView2.Environment.CreateWebResourceResponse(
                                                            ms, 200, "OK", headers);
    }
    catch (Exception)
    {
        args.Response = webView.CoreWebView2.Environment.CreateWebResourceResponse(
                                                        null, 404, "Not found", "");
    }
};

另请参阅