大家好,我是沙漠盡頭的狼。
上文介紹了《C#使用CefSharp內嵌網頁-並給出C#與JS的互動範例》,本文介紹CefSharp的快取實作,先來說說加入快取的好處:
- 提高頁面載入加速:CefSharp快取可以快取已經載入過的頁面和資源,當使用者再次造訪相同的頁面時,可以直接從快取中載入,而不需要重新下載和解析頁面和資源,從而加快頁面載入速度。
- 減少網路流量:使用快取可以減少網路流量,因為已經下載過的資源可以直接從快取中讀取,而不需要重新下載。
- 提升使用者體驗:由於快取可以提高頁面載入速度,因此可以提升使用者的體驗,使用者可以更快地存取頁面和資源,從而更加愉快地使用應用程式。
- 減少伺服器負載:使用快取可以減少伺服器的負載,因為已經下載過的資源可以直接從快取中讀取,而不需要重新生成和發送。
- 離線存取:可以使應用程式支援離線存取,因為它可以快取已經下載過的頁面和資源,當使用者沒有網路連線時,可以直接從快取中載入頁面和資源。
總之,使用快取可以提高應用程式的效能和使用者體驗,減少網路流量和伺服器負載,並支援離線存取,是一個非常有用的特性。
本文範例: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. 自訂快取
這是本文介紹的重點,相對於預設快取,自訂快取有以下好處:
- 更加靈活:可以根據應用程式的需求來靈活地配置快取策略和快取大小,從而更好地滿足應用程式的需求。
- 更好的效能:可以根據應用程式的需求和特定的場景進行配置,以獲得更好的效能。預設的快取可能不適合某些特定的場景或不適合您的應用程式的需求,而自訂快取則可以根據您的需求進行調整,以獲得更好的效能。
- 更好的安全性:可以更好地保護用戶的隱私和安全,因為可以控制快取中儲存的內容和快取的生命週期。
- 更加可控:可以更好地控制快取的行為,例如可以控制快取的清除時間和清除策略,從而更好地管理快取。
- 更好的相容性:可以更好地適應不同的瀏覽器和裝置,預設的快取可能不能提供足夠的相容性,而自訂快取則可以根據您的需求進行調整,以提供更好的相容性。
- 更加高效:可以更好地利用系統資源,例如可以使用更快的儲存裝置來儲存快取,從而提高快取的讀寫速度。
總結:自訂快取可以提供更好的效能、回應性、安全性和相容性,從而提高應用程式的品質和使用者體驗,人話就是更好的操控。
2.1. 程式碼實作
註解前面加的預設快取程式碼。
2.1.1. 註冊資源請求攔截處理常式
首先在使用ChromiumWebBrowser控制項的後台程式碼裡,註冊請求攔截處理常式,CefBrowser是控制項名稱,CefRequestHandlerc是處理常式:
public TestCefCacheView()
{
InitializeComponent();
var handler = new CefRequestHandlerc();
CefBrowser.RequestHandler = handler;
}
2.1.2. 請求攔截處理常式
CefSharp裡的IRequestHandler是一個介面,用於處理瀏覽器發出的請求。它定義了一些方法,可以在請求被發送到伺服器之前或之後對請求進行處理。
IRequestHandler的實作類別可以用於以下幾個方面:
攔截請求:可以透過實作OnBeforeBrowse方法來攔截請求,從而控制瀏覽器的行為。例如,可以在請求被發送到伺服器之前檢查請求的URL,如果不符合要求,則可以取消請求或者重新導向到其他頁面。
修改請求:可以透過實作OnBeforeResourceLoad方法來修改請求,例如可以加入一些自訂的HTTP標頭資訊,或者修改請求的URL。
處理回應:可以透過實作OnResourceResponse方法來處理伺服器返回的回應,例如可以檢查回應的狀態碼和內容,從而決定是否繼續載入頁面。
快取控制:可以透過實作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介面定義了一些方法,例如OnBeforeResourceLoad、OnResourceResponse等方法,這些方法可以用於攔截請求、修改請求、處理回應等方面。例如:
OnBeforeResourceLoad:在瀏覽器請求資源之前被呼叫,可以用於修改請求,例如加入一些自訂的HTTP標頭資訊,或者修改請求的URL。
OnResourceResponse:在瀏覽器接收到伺服器返回的回應之後被呼叫,可以用於處理回應,例如檢查回應的狀態碼和內容,從而決定是否繼續載入頁面。
OnResourceLoadComplete:在資源載入完成後被呼叫,可以用於處理資源載入完成後的操作,例如儲存資源到本地快取。
透過實作IResourceRequestHandler介面,可以對資源請求進行攔截和最佳化,從而提高應用程式的效能和使用者體驗。
這裡我們也不直接實作IResourceRequestHandler介面,我們定義CefResourceRequestHandler類別,繼承該介面的預設實作類別ResourceRequestHandler。
在下面的CefResourceRequestHandler類別中:
GetResourceHandler方法:處理資源是否需要快取,返回null不快取,返回CefResourceHandler表示需要快取,在這個類別中做跨域處理。GetResourceResponseFilter方法:註冊資源快取的操作類別,即資源下載的實作。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介面定義了一些方法,例如ProcessRequest、GetResponseHeaders、ReadResponse等方法,這些方法可以用於處理資源請求、取得回應標頭資訊、讀取回應內容等方面。例如:
ProcessRequest:在瀏覽器請求資源時被呼叫,可以用於處理資源請求,例如從本地快取中讀取資源內容,或者從網路中下載資源內容。
GetResponseHeaders:在瀏覽器請求資源時被呼叫,可以用於取得回應標頭資訊,例如設定回應的MIME類型、快取策略等。
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介面定義了一些方法,例如InitFilter、Filter、GetSize等方法,這些方法可以用於初始化過濾器、過濾回應內容、取得過濾後的回應內容大小等方面。例如:
InitFilter:在瀏覽器接收到回應內容時被呼叫,可以用於初始化過濾器,例如設定過濾器的狀態、取得回應標頭資訊等。
Filter:在瀏覽器接收到回應內容時被呼叫,可以用於過濾回應內容,例如修改回應內容、刪除回應內容等。
GetSize:在瀏覽器接收到回應內容時被呼叫,可以用於取得過濾後的回應內容大小,例如用於計算回應內容的壓縮比例等。
站長使用的CefSharp.Wpf的89.0.170.0版本中的IResponseFilter介面沒有GetSize方法。在該版本中,IResponseFilter介面只定義了兩個方法:InitFilter和Filter。
如果在該版本中您需要取得過濾後的回應內容大小,可以考慮在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. 快取安全問題
如果自訂快取沒有正確地處理快取安全,可能會導致瀏覽器的安全性受到威脅。例如,如果自訂快取快取了敏感資料,但是沒有正確地處理快取的加密和解密,可能會導致敏感資料外洩。
因此,在自訂快取時,需要注意處理快取一致性、快取空間、快取效能和快取安全等問題,以確保瀏覽器的正常執行和安全性。
參考:
微信技術交流群:新增微信(codewf)備註「入群」
QQ技術交流群:771992300。
