在 CefSharp 中使用本地构建的网页

Working with locally built web page in CefSharp

我在 Winform 中创建了一个 CefSharp 浏览器,我需要在内存中动态构建一个 HTML 页面,然后让 CefSharp 渲染它。

理想情况下,我想向构造函数传递一个包含 HTML 的字符串,但它需要一个 URL。答案可能是否定的,但是是否有一个指令可以在字符串前面加上让 CefSharp 知道它是一个包含网页的字符串?那么 CefSharp 会创建一个临时文件吗?

如果没有,Chromium 临时文件夹设置在哪里?如果我在那里写一个文件然后将其作为完全限定路径传递,它会工作吗?我知道 Chrome 将支持像 file:///Users/dmacdonald/Documents/myFile.htm 这样的东西作为 URL 但不确定如果使用临时结构如何形成 URL。

这是我的新代码,但我的浏览器对象没有 ResourceHandler 属性。我看到它有一个 ResourceHandlerFactory

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CefSharp.WinForms;
using CefSharp;


namespace DanCefWinForm
{
    public partial class Form1 : Form
    {
        public const string TestResourceUrl = "http://maps/resource/load";

        public Form1()
        {
            InitializeComponent();


        }

        private void Form1_Load(object sender, EventArgs e)
        {
            ChromiumWebBrowser browser = new ChromiumWebBrowser("http://maps/resource/load")
            {
                Dock = DockStyle.Fill,
            };

            var handler = browser.ResourceHandler;

           browser.Location = new Point(20, 20);
           browser.Size = new Size(100, 100);
            this.Controls.Add(browser);
        }
    }
}

有关为内存中字符串注册 ResourceHandler 的示例,请参阅 https://github.com/cefsharp/CefSharp/blob/v39.0.0-pre02/CefSharp.Example/CefExample.cs#L44

如您所见,它仍然有一个 URL(网络资源通常会有那个),但它可以是您选择的一个虚拟的。

这里是 GitHub 搜索它在 WinForms(和 WPF)示例应用程序中的调用方式:https://github.com/cefsharp/CefSharp/search?utf8=%E2%9C%93&q=RegisterTestResources

本地文件系统中带有临时文件(任何地方?)的另一个可能不太受欢迎的选项是使用 FileAccessFromFileUrlsAllowed

更新 来自以下评论:

您现在使用的是哪个 CefSharp 版本?请注意,如果您查看 github.com/cefsharp/CefSharp/releases 并搜索 resource,您会看到 API 在版本 49 中发生了更改(查看该版本的重大更改) - 请参阅下面的评论进一步的问题

简单方法(一个 "file",一页)

LoadString()可用于直接从字符串加载:

ChromiumWebBrowser.LoadString(string html, string url);

或者,LoadHtml() 可以从给定编码的字符串加载:

ChromiumWebBrowser.LoadHtml(string html, string url, Encoding encoding);

我都试过了,它们似乎都有效,至少在 CefSharp.Wpf v51.0.0 中是这样。根据WebBrowserExtensions.csLoadHtml()使用RegisterHandler()注册一个ResourceHandler。我不清楚 LoadString() 是如何工作的,但是这两个函数似乎具有相同的效果。

请务必使用有效的 URL 格式来伪造 URL,例如:

https://myfakeurl.com

复杂方法(多个"files",例如文档+图像)

  1. 创建一个从 IResourceHandlerFactory 派生的 class。使用 VS2015,将鼠标悬停在带红色下划线的名称上应该会提供 实现接口 的选项。这个自动完成选项极大地简化了 class 的创建,所以一定要使用它。

  2. 与步骤 1 类似,创建从 IResourceHandler 派生的 class。如果可以,请务必使用 实现界面 自动完成选项。

  3. 在第1步创建的class中(派生自IResourceHandlerFactory),有一个叫做GetResourceHandler()的函数。在此函数中,return 您从 步骤 2 派生的 class 的新实例(基于 IResourceHandler)。此处使用 new 是必不可少的,因为 Web 浏览器可能会同时请求多个文件。每个 IResourceHandler 实例应处理一个来自浏览器的请求(不用担心,这已经为您完成)。

  4. 如OP所述,浏览器控件有一个名为ResourceHandlerFactory的成员。将此成员设置为您在 步骤 1 中创建的 class 的 new 实例(源自 IResourceHandlerFactory)。这就是将 Chromium Web 浏览器控件链接到您的界面 classes 的原因。在第 3 步中,您链接了两个 classes,因此我们有一个完整的链。

  5. 在第 2 步的 class 中,有一个名为 ProcessRequest() 的函数。这是网页发出请求时调用的第一个函数。您的目标是记录请求的 URL 和任何 POST 数据,然后决定是否允许该请求,调用 callback.Continue()callback.Cancel()。 Return true 继续。

  6. 同样在步骤 2 的 class 中,有一个名为 GetResponseHeaders() 的函数。这是调用的第二个函数。您的目标是检查 URL,可能从您存储它的任何地方获取文件数据(但尚未发送),确定响应长度(文件或字符串大小),并在响应中设置适当的状态代码目的。请务必设置所有这些变量,以便请求可以正确进行。

  7. 您的最后一步,也是在第 2 步的 class 中,是在第三个调用的函数中完成请求:ReadResponse()。在此函数中,将您在第 6 步中获取的数据写入 dataOut 流。如果您的数据超过 32kB,您可能需要分多个块发送。 绝对确保将您在给定调用中写入的数量限制dataOut 流的长度。将 bytesRead 设置为您在此特定调用中编写的任何内容。在最后一次调用时,当没有更多数据剩余时,只需将 bytesRead 设置为零和 return false。因为您可能会多次被要求访问一个给定的文件,所以请务必跟踪您当前的读取位置,以便您知道您在哪里以及已发送了多少数据。

对于那些不熟悉这件事的人,您可以将数据文件直接编译到您的 EXE 中,方法是将它们添加到您的项目并将它们的 "Build Action" 设置为 "Embedded Resource",然后使用编程方式加载它们的数据System.Reflection.Assembly.GetManifestResourceStream()。使用上述方法,不需要从磁盘创建或读取任何文件

下面是一个从文件系统加载资源的自定义工厂示例:

public class FileResourceHandlerFactory : ISchemeHandlerFactory {
    private string scheme, host, folder, default_filename;

    public string Scheme => scheme;

    public FileResourceHandlerFactory(string scheme, string host, string folder, string default_filename = "index.html") {
        this.scheme = scheme;
        this.host = host;
        this.folder = folder;
        this.default_filename = default_filename;
    }

    private string get_content(Uri uri, out string extension) {
        var path = uri.LocalPath.Substring(1);
        path = string.IsNullOrWhiteSpace(path) ? this.default_filename : path;
        extension = Path.GetExtension(path);
        return File.ReadAllText(Path.Combine(this.folder, path));
    }

    IResourceHandler ISchemeHandlerFactory.Create(IBrowser browser, IFrame frame, string schemeName, IRequest request) {
        var uri = new Uri(request.Url);
        return ResourceHandler.FromString(get_content(uri, out var extension), extension);
    }
}

下面是您将如何应用它:

var settings = new CefSettings();
settings.RegisterScheme(new CefCustomScheme {
    SchemeName = "app",
    SchemeHandlerFactory = fileResourceHandlerFactory,
    IsSecure = true //treated with the same security rules as those applied to "https" URLs
});
var chromeBrowser = new ChromiumWebBrowser();
chromeBrowser.Load("app://local");

您可能需要使用自定义方案处理程序,以便为本地文件提供服务,并且 "bypass" 关于文件协议的 Chromium 安全性。

我就此事写了blog post

您要添加的是您的方案处理程序及其工厂:

using System;
using System.IO;
using CefSharp;

namespace MyProject.CustomProtocol
{
    public class CustomProtocolSchemeHandler : ResourceHandler
    {
        // Specifies where you bundled app resides.
        // Basically path to your index.html
        private string frontendFolderPath;

        public CustomProtocolSchemeHandler()
        {
            frontendFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "./bundle/");
        }

        // Process request and craft response.
        public override bool ProcessRequestAsync(IRequest request, ICallback callback)
        {
            var uri = new Uri(request.Url);
            var fileName = uri.AbsolutePath;

            var requestedFilePath = frontendFolderPath + fileName;

            if (File.Exists(requestedFilePath))
            {
                byte[] bytes = File.ReadAllBytes(requestedFilePath);
                Stream = new MemoryStream(bytes);

                var fileExtension = Path.GetExtension(fileName);
                MimeType = GetMimeType(fileExtension);

                callback.Continue();
                return true;
            }

            callback.Dispose();
            return false;
        }
    }

    public class CustomProtocolSchemeHandlerFactory : ISchemeHandlerFactory
    {
        public const string SchemeName = "customFileProtocol";

        public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
        {
            return new CustomProtocolSchemeHandler();
        }
    }
}

然后注册再调用Cef.Initialize:

var settings = new CefSettings
{
  BrowserSubprocessPath = GetCefExecutablePath()
};

settings.RegisterScheme(new CefCustomScheme
{
  SchemeName = CustomProtocolSchemeHandlerFactory.SchemeName,
  SchemeHandlerFactory = new CustomProtocolSchemeHandlerFactory()
});