CefSharp自定義快取實現

Dotnet9個人部落格發表於2023-04-26

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

上文介紹了《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. 快取安全問題

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

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

參考:

相關文章