如何通过网络部署 Chromium 二进制文件

在某些情况下,安装程序或部署包需要尽可能小,例如,发布应用程序的服务可能会受到限制。

另一方面,DotNetBrowser 使用自己的 Chromium 引擎,二进制文件必须部署在目标环境中。 最直接的方法是将二进制文件作为安装程序的一部分提供,但是,这将会导致安装程序本身的大小增加。

可能的解决方案之一是在 DotNetBrowser 尝试定位它们时通过网络提供 Chromium 二进制文件。 DotNetBrowser 使用默认的 .NET 程序集加载逻辑来定位和加载二进制 DLL,因此可以使用 AppDomain.AssemblyResolve 事件来自定义整个过程并以非标准方式提供 DLL。

以下是对这一想法的一般描述:

  1. 注册 AppDomain.AssemblyResolve 事件的自定义处理程序。
  2. 在此处理程序中,筛选出与 DotNetBrowser 二进制文件相关的尝试。
  3. 当接收到相应的 AssemblyResolve 事件时,使用完全限定的程序集名称准备网络请求。
  4. 执行请求并获取字节数组形式的 DLL。
  5. 从字节加载 DLL 并从处理程序返回它。

执行所述操作以加载二进制文件 DLL 的示例应用程序的代码在 GitHub 存储库中作为 Visual Studio 项目提供: C#, VB.NET

该示例本身是一个 WPF 应用程序,但该方法并非特定于 WPF ,也可以在 WinForms 和控制台应用程序中使用。

下文将详细介绍该示例的实现过程,并解释其中最重要的部分。

实现

BinariesResolverBase 类

BinariesResolverBase 抽象类提供二进制解析器的一般实现。 该类的构造函数初始化 RequestUri 属性,该属性稍后可用于准备请求,创建 HttpClient 实例以发出这些请求并订阅应用程序域的 AssemblyResolve 事件。

protected BinariesResolverBase(string requestUri, AppDomain domain = null)
{
    if (domain == null)
    {
        domain = AppDomain.CurrentDomain;
    }

    RequestUri = requestUri;
    client = new HttpClient();
    domain.AssemblyResolve += Resolve;
}

Resolve(object sender, ResolveEventArgs args) 方法处理 AssemblyResolve 事件,并筛选出名称以”DotNetBrowser.Chromium”开头的程序集的请求。 对于这些程序集,它会调用私有的 Resolve 方法重载以进行进一步处理。

public Assembly Resolve(object sender, ResolveEventArgs args)
    => args.Name.StartsWith("DotNetBrowser.Chromium") ? Resolve(args.Name).Result : null;

Resolve(string binariesAssemblyName)方法重载创建一个 AssemblyName 实例并将其传递给抽象的 PrepareRequest(AssemblyName assemblyName) 方法以准备 URL 请求字符串。 之后,URL 请求字符串用于通过 HttpClient 执行实际请求。 响应流应包含 Chromium 二进制程序集的字节。 然后,这些字节被传递给抽象的 ProcessResponse(Stream responseBody, AssemblyName assemblyName) 方法,该方法返回程序集本身。

private async Task<Assembly> Resolve(string binariesAssemblyName)
{
    //注意:程序集通常在 UI 应用程序的后台线程中解析。
    try
    {
        //使用完全限定程序集名称构造请求。
        AssemblyName assemblyName = new AssemblyName(binariesAssemblyName);
        string request = PrepareRequest(assemblyName);

        //执行请求并下载响应。
        OnStatusUpdated("Downloading Chromium binaries...");
        Debug.WriteLine($"Downloading {request}");
        HttpResponseMessage response = await client.GetAsync(request);

        response.EnsureSuccessStatusCode();
        OnStatusUpdated("Chromium binaries package downloaded");
        Stream responseBody = await response.Content.ReadAsStreamAsync();

        //处理响应字节并加载程序集。
        return ProcessResponse(responseBody, assemblyName);
    }
    catch (Exception e)
    {
        Debug.WriteLine("Exception caught: {0} ", e);
    }

    return null;
}

OnStatusUpdated 方法会引发带有特定信息的 StatusUpdated 事件,然后可利用该信息了解应用程序其他部分的进度。

BinariesResolverBase 抽象实现随后由BinariesResolver 类扩展,该类提供 PrepareRequestProcessResponse 方法的实现。

BinariesResolver 类

BinariesResolver 通过下载 DotNetBrowser 分发存档并从中即时提取所需的程序集来解析 Chromium 二进制程序集。 此类派生自 BinariesResolverBase 并提供其抽象成员的实现。

下面是 PrepareRequest 的实现:

protected override string PrepareRequest(AssemblyName assemblyName)
{
    //如果构建组件为 0,则只使用主要和次要版本组件。
    int fieldCount = assemblyName.Version.Build == 0 ? 2 : 3;
    return string.Format(RequestUri, assemblyName.Version.ToString(fieldCount));
}

此实现重用 RequestUri 属性,根据 DotNetBrowser 版本准备对分发存档的直接引用。 本例中的 RequestUri 设置为 UriTemplate ,如下所示:

private const string UriTemplate =
    "https://storage.googleapis.com/cloud.teamdev.com/downloads/"
    +"dotnetbrowser/{0}/dotnetbrowser-net45-{0}.zip";

以流的形式接收分发归档后,会调用 ProcessResponse 方法。 该方法的实现方式是在运行过程中提取 DLL,并将其直接加载到当前应用程序域中。 然后将加载的程序集用作返回值。

protected override Assembly ProcessResponse(Stream responseBody, AssemblyName assemblyName)
{
    // 下载的字节表示一个 ZIP 存档。 在此存档中找到我们需要的 DLL。
    ZipArchive archive = new ZipArchive(responseBody);
    ZipArchiveEntry binariesDllEntry 
        = archive.Entries
                .FirstOrDefault(entry => entry.FullName.EndsWith(".dll")
                                        && entry.FullName.Contains(assemblyName.Name));
    if (binariesDllEntry == null)
    {
        return null;
    }

    // 解压找到的条目,并加载 DLL。
    OnStatusUpdated("Unzipping Chromium binaries");
    Stream unzippedEntryStream;
    using (unzippedEntryStream = binariesDllEntry.Open())
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            unzippedEntryStream.CopyTo(memoryStream);
            OnStatusUpdated("Loading Chromium binaries assembly");
            Assembly assembly = Assembly.Load(memoryStream.ToArray());
            OnStatusUpdated("Chromium binaries assembly loaded.", true);
            return assembly;
        }
    }
}

将 DLL 加载到应用程序域后,DotNetBrowser 将二进制文件从此 DLL 解压到通过 EngineOptions.Builder.ChromiumDirectory 配置的目录,然后照常启动 Chromium 进程。

此处的 DotNetBrowser 分发存档仅用作示例。 如果您计划在应用程序中实现类似的方法,您应该考虑实现自己的服务,提供所需的 DLL,以避免过度的内存使用并减少初始化时间。

优点

建议方法的主要优点是最大限度地减小了分布式应用程序包的大小-不需要将 DotNetBrowser.Chromium DLL 作为安装程序或分发包的一部分提供。

缺点

  1. 初始化时间增加。 当 Chromium 二进制文件通过网络提供时,初始化过程包括下载二进制文件 DLL,即使连接良好,这通常也需要一些时间。 如果连接不佳,可能需要重试几次,并且初始化时间会显着增加。
  2. 需要连接互联网。 如果没有连接互联网,则无法下载二进制文件和初始化 DotNetBrowser 引擎。
  3. 内存使用量增加。 如果 DotNetBrowser.Chromium DLL 从字节数组中加载,DotNetBrowser 会在内存中解压缩二进制文件,初始化过程中内存使用量会增加。 如果内存使用率太高,甚至会导致 32 位环境中内存不足的问题。

总结

所描述的方法似乎对特定类型的解决方案很有用,但是,在做出最终决定之前,有必要全面考虑其优缺点。

Go Top