cefsharp自定義緩存實現

cefsharp自定義緩存實現

使用好cefsharp的緩存功能,可以提高應用程式的性能和用戶體驗,減少網絡流量和伺服器負載,並支持離線訪問,是一個非常有用的特性。

最后更新 2023/4/25 上午11:36
沙漠尽头的狼
预计阅读 18 分钟
分类
.NET
标签
.NET C# CefSharp

大家好,我是沙漠盡頭的狼。

上文介绍了《C#使用CefSharp内嵌网页-并给出C#与JS的交互示例》,本文介绍CefSharp的缓存实现,先来说说添加缓存的好处:

  1. 提高頁面加載加速:cefsharp緩存可以緩存已經加載過的頁面和資源,當用戶再次訪問相同的頁面時,可以直接從緩存中加載,而不需要重新下載和解析頁面和資源,從而加快頁面加載速度。
  2. 減少網絡流量:使用緩存可以減少網絡流量,因為已經下載過的資源可以直接從緩存中讀取,而不需要重新下載。
  3. 提高用戶體驗:由於緩存可以提高頁面加載速度,因此可以提高用戶的體驗,用戶可以更快地訪問頁面和資源,從而更加愉快地使用應用程式。
  4. 減少伺服器負載:使用緩存可以減少伺服器的負載,因為已經下載過的資源可以直接從緩存中讀取,而不需要重新生成和發送。
  5. 離線訪問:可以使應用程式支持離線訪問,因為它可以緩存已經下載過的頁面和資源,當用戶沒有網絡連接時,可以直接從緩存中加載頁面和資源。

總之,使用緩存可以提高應用程式的性能和用戶體驗,減少網絡流量和伺服器負載,並支持離線訪問,是一個非常有用的特性。

本文示例:GitHub

断网情况下,演示加载已经缓存的百度百度翻译Dotnet9首页Dotnet9关于4个页面:

接下來講解緩存的實現方式。

1. 默認緩存實現

cefsharp的默認緩存實現方式是基於chromium的緩存機制。chromium使用了兩種類型的緩存:內存緩存和磁碟緩存。

1.1.內存緩存

內存緩存是一個基於lru(最近最少使用)算法的緩存,它緩存了最近訪問的頁面和資源。內存緩存的大小是有限的,當緩存達到最大大小時,最近最少使用的頁面和資源將被刪除。

內存緩存無法通過cefsharp.wpf的api進行設置。具體來說,chromium會在內存中維護一個lru(least recently used)緩存,用於存儲最近訪問的網頁數據。當緩存空間不足時,chromium會根據lru算法自動清除最近最少使用的緩存數據,以騰出空間存儲新的數據。

在cefsharp.wpf中,我們可以通過調用cef.getglobalrequestcontext().clearcacheasync()方法來清除內存緩存中的數據。該方法會清除所有緩存數據,包括內存緩存和磁碟緩存。如果只需要清除內存緩存,可以調用cef.getglobalrequestcontext().clearcache(cefcachetype.memorycache)方法。

需要注意的是,由於內存緩存是由chromium自身維護的,因此我們無法直接控制其大小。如果需要控制緩存大小,可以通過設置磁碟緩存的大小來間接控制內存緩存的大小。

1.2.磁碟緩存

磁碟緩存是一個基於文件系統的緩存,它緩存了已經下載的頁面和資源。磁碟緩存的大小也是有限的,當緩存達到最大大小時,最早的頁面和資源將被刪除。

cefsharp.wpf的磁碟緩存是通過設置cefsettings中的cachepath屬性來實現的。具體來說,我們可以通過以下代碼設置磁碟緩存的路徑:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // CachePath需要为绝对路径
        var settings = new CefSettings
        {
            CachePath = $"{AppDomain.CurrentDomain.BaseDirectory}DefaultCaches"
        };
        Cef.Initialize(settings);
    }
}

緩存目錄結構如下:

其中,cachepath屬性指定了磁碟緩存的路徑(絕對路徑)。如果不設置該屬性,chromium會將緩存數據存儲在默認路徑下(通常是用戶目錄下的appdata\local\cefsharp目錄)。

需要注意的是,磁碟緩存的大小是由chromium自身控制的,我們可以通過設置cachecontroller的setcachelimit方法來控制緩存數據存儲在磁碟上的最大空間。該方法接受一個long類型的參數,表示緩存數據的最大大小(單位為字節)。例如,以下代碼將磁碟緩存的最大大小設置為100mb:

var cacheController = Cef.GetGlobalRequestContext().CacheController;
cacheController.SetCacheLimit(100 * 1024 * 1024); // 100MB

需要注意的是,chromium會根據lru算法自動清除最近最少使用的緩存數據,以騰出空間存儲新的數據。因此,即使設置了緩存大小,也不能保證所有數據都會被緩存。如果需要清除磁碟緩存中的數據,可以調用cef.getglobalrequestcontext().clearcacheasync()方法。

默認的緩存站長研究不多,上面的代碼和描述通過chatgpt搜索得來,我們來看自定義緩存的實現,默認緩存只是個引子。

2. 自定義緩存

这是本文介绍的重点,相对于默认缓存,自定义缓存有以下好处:

  1. 更加靈活:可以根據應用程式的需求來靈活地配置緩存策略和緩存大小,從而更好地滿足應用程式的需求。
  2. 更好的性能:可以根據應用程式的需求和特定的場景進行配置,以獲得更好的性能。默認的緩存可能不適合某些特定的場景或者不適合您的應用程式的需求,而自定義緩存則可以根據您的需求進行調整,以獲得更好的性能。
  3. 更好的安全性:可以更好地保護用戶的隱私和安全,因為可以控制緩存中存儲的內容和緩存的生命周期。
  4. 更加可控:可以更好地控制緩存的行為,例如可以控制緩存的清除時間和清除策略,從而更好地管理緩存。
  5. 更好的兼容性:可以更好地適應不同的瀏覽器和設備,默認的緩存可能不能提供足夠的兼容性,而自定義緩存則可以根據您的需求進行調整,以提供更好的兼容性。
  6. 更加高效:可以更好地利用系統資源,例如可以使用更快的存儲設備來存儲緩存,從而提高緩存的讀寫速度。

总结:自定义缓存可以提供更好的性能、响应性、安全性和兼容性,从而提高应用程序的质量和用户体验,人话就是更好的操控

2.1.代碼實現

注釋前面加的默認緩存代碼。

2.1.1.註冊資源請求攔截處理程式

首先在使用ChromiumWebBrowser控件的后台代码里,注册请求拦截处理程序,CefBrowser是控件名,CefRequestHandlerc是处理程序:

public TestCefCacheView()
{
    InitializeComponent();

    var handler = new CefRequestHandlerc();
    CefBrowser.RequestHandler = handler;
}

2.1.2.請求攔截處理程式

CefSharp里的IRequestHandler是一个接口,用于处理浏览器发出的请求。它定义了一些方法,可以在请求被发送到服务器之前或之后对请求进行处理。

IRequestHandler的实现类可以用于以下几个方面:

  1. 攔截請求:可以通過實現onbeforebrowse方法來攔截請求,從而控制瀏覽器的行為。例如,可以在請求被發送到伺服器之前檢查請求的url,如果不符合要求,則可以取消請求或者重定向到其他頁面。

  2. 修改請求:可以通過實現onbeforeresourceload方法來修改請求,例如可以添加一些自定義的http頭信息,或者修改請求的url。

  3. 處理響應:可以通過實現onresourceresponse方法來處理伺服器返回的響應,例如可以檢查響應的狀態碼和內容,從而決定是否繼續加載頁面。

  4. 緩存控制:可以通過實現onquotarequest方法來控制緩存的大小和清除策略,從而優化緩存的使用。

总之,IRequestHandler的实现类可以用于控制浏览器的行为,优化网络请求和缓存的使用,从而提高应用程序的性能和用户体验。

我们不直接实现接口IRequestHandler,而是继承它的一个默认实现类RequestHandler,这可以简化我们的开发,毕竟实现接口要列出一系列接口方法。

我们重载方法GetResourceRequestHandler, 在这个方法里返回CefResourceRequestHandler实例,页面中资源请求时会调用此方法:

using CefSharp;
using CefSharp.Handler;

namespace WpfWithCefSharpCacheDemo.Caches;

internal class CefRequestHandlerc : RequestHandler
{
    protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame,
        IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
    {
        // 一个请求用一个CefResourceRequestHandler
        return new CefResourceRequestHandler();
    }
}

2.1.3.資源請求攔截程式

在CefSharp中,IResourceRequestHandler接口是用于处理资源请求的,它可以拦截浏览器发出的资源请求,例如图片、CSS、JavaScript等,从而实现对资源请求的控制和优化。

具体来说,IResourceRequestHandler接口定义了一些方法,例如OnBeforeResourceLoadOnResourceResponse等方法,这些方法可以用于拦截请求、修改请求、处理响应等方面。例如:

  1. onbeforeresourceload:在瀏覽器請求資源之前被調用,可以用於修改請求,例如添加一些自定義的http頭信息,或者修改請求的url。

  2. onresourceresponse:在瀏覽器接收到伺服器返回的響應之後被調用,可以用於處理響應,例如檢查響應的狀態碼和內容,從而決定是否繼續加載頁面。

  3. onresourceloadcomplete:在資源加載完成後被調用,可以用於處理資源加載完成後的操作,例如保存資源到本地緩存。

通过实现IResourceRequestHandler接口,可以对资源请求进行拦截和优化,从而提高应用程序的性能和用户体验。

这里我们也不直接实现IResourceRequestHandler接口,我们定义CefResourceRequestHandler类,继承该接口的默认实现类ResourceRequestHandler

在下面的CefResourceRequestHandler类中:

  1. GetResourceHandler方法:处理资源是否需要缓存,返回null不缓存,返回CefResourceHandler表示需要缓存,在这个类中做跨域处理。
  2. GetResourceResponseFilter方法:注册资源缓存的操作类,即资源下载的实现。
  3. OnBeforeResourceLoad方法:在这个方法里,我们可以实现给页面传递header参数。
using System.Collections.Specialized;
using CefSharp;
using CefSharp.Handler;

namespace WpfWithCefSharpCacheDemo.Caches;

internal class CefResourceRequestHandler : ResourceRequestHandler
{
    private string _localCacheFilePath;

    private bool IsLocalCacheFileExist => System.IO.File.Exists(_localCacheFilePath);

    protected override IResourceHandler? GetResourceHandler(IWebBrowser chromiumWebBrowser, IBrowser browser,
        IFrame frame, IRequest request)
    {
        try
        {
            _localCacheFilePath = CacheFileHelper.CalculateResourceFileName(request.Url, request.ResourceType);
            if (string.IsNullOrWhiteSpace(_localCacheFilePath))
            {
                return null;
            }
        }
        catch
        {
            return null;
        }

        if (!IsLocalCacheFileExist)
        {
            return null;
        }

        return new CefResourceHandler(_localCacheFilePath);
    }

    protected override IResponseFilter? GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser,
        IFrame frame,
        IRequest request, IResponse response)
    {
        return IsLocalCacheFileExist ? null : new CefResponseFilter { LocalCacheFilePath = _localCacheFilePath };
    }

    protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser,
        IFrame frame, IRequest request,
        IRequestCallback callback)
    {
        var headers = new NameValueCollection(request.Headers);
        headers["Authorization"] = "Bearer xxxxxx.xxxxx.xxx";
        request.Headers = headers;

        return CefReturnValue.Continue;
    }
}

2.1.4. CefResourceHandler

在CefSharp中,IResourceHandler接口是用于处理资源的,它可以拦截浏览器发出的资源请求,并返回自定义的资源内容,从而实现对资源的控制和优化。

具体来说,IResourceHandler接口定义了一些方法,例如ProcessRequestGetResponseHeadersReadResponse等方法,这些方法可以用于处理资源请求、获取响应头信息、读取响应内容等方面。例如:

  1. processrequest:在瀏覽器請求資源時被調用,可以用於處理資源請求,例如從本地緩存中讀取資源內容,或者從網絡中下載資源內容。

  2. getresponseheaders:在瀏覽器請求資源時被調用,可以用於獲取響應頭信息,例如設置響應的mime類型、緩存策略等。

  3. readresponse:在瀏覽器請求資源時被調用,可以用於讀取響應內容,例如從本地緩存中讀取資源內容,或者從網絡中下載資源內容。

通過實現iresourcehandler接口,可以對資源進行自定義處理,例如從本地緩存中讀取資源內容,從而提高應用程式的性能和用戶體驗。

这里我们也不直接实现IResourceHandler接口,我们定义CefResourceHandler类,继承该接口的默认实现类ResourceHandler

CefResourceHandler的构造函数里只处理跨域问题,其他需求可通过上面接口的方法查找资料处理即可:

using CefSharp;
using System.IO;

namespace WpfWithCefSharpCacheDemo.Caches;

internal class CefResourceHandler : ResourceHandler
{
    public CefResourceHandler(string filePath, string mimeType = null, bool autoDisposeStream = false,
        string charset = null) : base()
    {
        if (string.IsNullOrWhiteSpace(mimeType))
        {
            var fileExtension = Path.GetExtension(filePath);
            mimeType = Cef.GetMimeType(fileExtension);
            mimeType = mimeType ?? DefaultMimeType;
        }

        var stream = File.OpenRead(filePath);
        StatusCode = 200;
        StatusText = "OK";
        MimeType = mimeType;
        Stream = stream;
        AutoDisposeStream = autoDisposeStream;
        Charset = charset;

        Headers.Add("Access-Control-Allow-Origin", "*");
    }
}

2.1.5. CefResponseFilter

在CefSharp中,IResponseFilter接口是用于过滤响应内容的,它可以拦截浏览器接收到的响应内容,并对其进行修改或者过滤,从而实现对响应内容的控制和优化。

具体来说,IResponseFilter接口定义了一些方法,例如InitFilterFilterGetSize等方法,这些方法可以用于初始化过滤器、过滤响应内容、获取过滤后的响应内容大小等方面。例如:

  1. initfilter:在瀏覽器接收到響應內容時被調用,可以用於初始化過濾器,例如設置過濾器的狀態、獲取響應頭信息等。

  2. filter:在瀏覽器接收到響應內容時被調用,可以用於過濾響應內容,例如修改響應內容、刪除響應內容等。

  3. getsize:在瀏覽器接收到響應內容時被調用,可以用於獲取過濾後的響應內容大小,例如用於計算響應內容的壓縮比例等。

站长使用的CefSharp.Wpf89.0.170.0版本中的IResponseFilter接口没有GetSize方法。在该版本中,IResponseFilter接口只定义了两个方法:InitFilterFilter

如果在该版本中您需要获取过滤后的响应内容大小,可以考虑在Filter方法中自行计算。例如,在Filter方法中,您可以将过滤后的响应内容写入一个缓冲区,并记录缓冲区的大小,最后返回过滤后的响应内容和缓冲区的大小。

public class MyResponseFilter : IResponseFilter
{
    private MemoryStream outputStream = new MemoryStream();

    public void Dispose()
    {
        outputStream.Dispose();
    }

    public bool InitFilter()
    {
        return true;
    }

    public FilterStatus Filter(Stream dataIn, out long dataInRead, Stream dataOut, out long dataOutWritten)
    {
        dataInRead = 0;
        dataOutWritten = 0;

        byte[] buffer = new byte[4096];
        int bytesRead = 0;

        do
        {
            bytesRead = dataIn.Read(buffer, 0, buffer.Length);
            if (bytesRead > 0)
            {
                outputStream.Write(buffer, 0, bytesRead);
            }
        } while (bytesRead > 0);

        byte[] outputBytes = outputStream.ToArray();
        dataOut.Write(outputBytes, 0, outputBytes.Length);

        dataInRead = outputBytes.Length;
        dataOutWritten = outputBytes.Length;

        return FilterStatus.Done;
    }

    public int GetResponseFilterBufferSize()
    {
        return 0;
    }

    public int GetResponseFilterDelay()
    {
        return 0;
    }
}

在上述示例代码中,我们在Filter方法中将过滤后的响应内容写入了一个MemoryStream对象中,并记录了缓冲区的大小。最后,我们在Filter方法的返回值中返回了过滤后的响应内容和缓冲区的大小。

总结,通过实现IResponseFilter接口,可以对响应内容进行自定义处理,例如对响应内容进行压缩、加密等操作,从而提高应用程序的性能和安全性。

本文示例这里定义类CefResponseFilter直接实现接口处理文件缓存实际操作类,即资源下载实现:

using CefSharp;
using System.IO;

namespace WpfWithCefSharpCacheDemo.Caches;

internal class CefResponseFilter : IResponseFilter
{
    public string LocalCacheFilePath { get; set; }
    private const int BUFFER_LENGTH = 1024;
    private bool isFailCacheFile;


    public FilterStatus Filter(Stream? dataIn, out long dataInRead, Stream? dataOut, out long dataOutWritten)
    {
        dataInRead = 0;
        dataOutWritten = 0;

        if (dataIn == null)
        {
            return FilterStatus.NeedMoreData;
        }

        var length = dataIn.Length;
        var data = new byte[BUFFER_LENGTH];
        var count = dataIn.Read(data, 0, BUFFER_LENGTH);

        dataInRead = count;
        dataOutWritten = count;

        dataOut?.Write(data, 0, count);

        try
        {
            CacheFile(data, count);
        }
        catch
        {
            // ignored
        }

        return length == dataIn.Position ? FilterStatus.Done : FilterStatus.NeedMoreData;
    }

    public bool InitFilter()
    {
        try
        {
            var dirPath = Path.GetDirectoryName(LocalCacheFilePath);
            if (!string.IsNullOrWhiteSpace(dirPath) && !Directory.Exists(dirPath))
            {
                Directory.CreateDirectory(dirPath);
            }
        }
        catch
        {
            // ignored
        }

        return true;
    }

    public void Dispose()
    {
    }

    private void CacheFile(byte[] data, int count)
    {
        if (isFailCacheFile)
        {
            return;
        }

        try
        {
            if (!File.Exists(LocalCacheFilePath))
            {
                using var fs = File.Create(LocalCacheFilePath);
                fs.Write(data, 0, count);
            }
            else
            {
                using var fs = File.Open(LocalCacheFilePath, FileMode.Append);
                fs.Write(data,0,count);
            }
        }
        catch
        {
            isFailCacheFile = true;
            File.Delete(LocalCacheFilePath);
        }
    }
}

2.1.6. CacheFileHelper

緩存文件幫助類,用於管理頁面的ajax接口緩存白名單、緩存文件路徑規範等:

using CefSharp;
using System;
using System.Collections.Generic;
using System.IO;

namespace WpfWithCefSharpCacheDemo.Caches;

internal static class CacheFileHelper
{
    private const string DEV_TOOLS_SCHEME = "devtools";
    private const string DEFAULT_INDEX_FILE = "index.html";

    private static HashSet<string> needInterceptedAjaxInterfaces = new();

    private static string CachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "caches");

    public static void AddInterceptedAjaxInterfaces(string url)
    {
        if (needInterceptedAjaxInterfaces.Contains(url))
        {
            return;
        }

        needInterceptedAjaxInterfaces.Add(url);
    }

    private static bool IsNeedInterceptedAjaxInterface(string url, ResourceType resourceType)
    {
        var uri = new Uri(url);
        if (DEV_TOOLS_SCHEME == url)
        {
            return false;
        }

        if (ResourceType.Xhr == resourceType && !needInterceptedAjaxInterfaces.Contains(url))
        {
            return false;
        }

        return true;
    }

    public static string? CalculateResourceFileName(string url, ResourceType resourceType)
    {
        if (!IsNeedInterceptedAjaxInterface(url, resourceType))
        {
            return default;
        }

        var uri = new Uri(url);
        var urlPath = uri.LocalPath;

        if (urlPath.StartsWith("/"))
        {
            urlPath = urlPath.Substring(1);
        }

        var subFilePath = urlPath;
        if (ResourceType.MainFrame == resourceType || string.IsNullOrWhiteSpace(urlPath))
        {
            subFilePath = Path.Combine(urlPath, DEFAULT_INDEX_FILE);
        }

        var hostCachePath = Path.Combine(CachePath, uri.Host);
        var fullFilePath = Path.Combine(hostCachePath, subFilePath);
        return fullFilePath;
    }
}

自定義緩存的子目錄以資源的域名(host)為目錄名稱創建:

打开缓存的dotnet9.com目录,通过查看目录结构和程序发布目录基本一致,这更适合人看了,是不?

2.2.可能存在的問題

第一点,站长目前遇到的问题,后面4点由Token AI提供解释。

2.2.1.對緩存的資源url帶querystring的方式支持不好

建議用route(路由的方式:https://dotnet9.com/albums/wpf)代替querystring(查詢參數的試工:https://dotnet9.com/albums? slug=wpf)的方式,站長有空再研究下querystring的緩存方式。

如果確實資源帶querystring,那對於這種資源就放開緩存,直接通過網絡請求吧。

2.2.2.緩存一致性問題

如果自定義緩存不正確地處理了緩存一致性,可能會導致瀏覽器顯示過期的內容或者不一致的內容。例如,如果緩存了一個網頁,但是該網頁在伺服器上已經被更新了,如果自定義緩存沒有正確地處理緩存一致性,可能會導致瀏覽器顯示過期的網頁內容。

2.2.3.緩存空間問題

如果自定義緩存沒有正確地管理緩存空間,可能會導致瀏覽器占用過多的內存或者磁碟空間。例如,如果自定義緩存緩存了大量的數據,但是沒有及時清理過期的數據或者限制緩存的大小,可能會導致瀏覽器占用過多的內存或者磁碟空間。

2.2.4.緩存性能問題

如果自定義緩存沒有正確地處理緩存性能,可能會導致瀏覽器的性能下降。例如,如果自定義緩存沒有正確地處理緩存的讀取和寫入,可能會導致瀏覽器的響應速度變慢。

2.2.5.緩存安全問題

如果自定義緩存沒有正確地處理緩存安全,可能會導致瀏覽器的安全性受到威脅。例如,如果自定義緩存緩存了敏感數據,但是沒有正確地處理緩存的加密和解密,可能會導致敏感數據泄露。

因此,在自定義緩存時,需要注意處理緩存一致性、緩存空間、緩存性能和緩存安全等問題,以確保瀏覽器的正常運行和安全性。

參考:

  • CefSharp

  • 微信技術交流群:添加微信(codewf)備註“入群”

  • qq技術交流群:771992300。

Keep Exploring

延伸阅读

更多文章