Nga tema e revistës në praktikën e projektit
Faqe shërbimi dhe teknike të përshtatshme për artikullin
Kur në një Business-Software ekzistuese dikush papritmas dëshiron të integrjë shpejt përmbajtje moderne web, zakonisht tek Windows përfundohet te WebView2. Në Delphi WebView2 FMX problemi themelor rrallë është thjesht shfaqja e një URL-je, por embeddimi i pastër në një ndërfaqe FireMonkey (FMX), inicializimi i besueshëm (asinkron dhe i bazuar në COM), si dhe kurthet e Edge rreth dosjeve të User-Data, shkarkimeve, debugging-ut dhe komunikimit të qëndrueshëm JS↔Delphi.
Ky copëz source tregon një model që preferoj për aplikacione të mirëmbajtshme: një objekt i kapsulluar „Host“ që kontrollon lifecycle-in e WebView2, dhe një bridge të përcaktuar mbi WebMessage (JSON), në vend të „ExecuteScript überall“ të paqëllimshëm. Qëllimi nuk është kod demo, por një bllok ndërtimi që mbijeton në klientë të rritur.
Pse WebView2 në FMX është ndryshe nga „Browser-Component drop“
WebView2 është një API e afërt me COM/WinRT me inicializim asinkron. FireMonkey abstrahon Windows-Handles, megjithatë për WebView2 në fund ju duhët një Parent-Window të vërtetë (HWND) dhe një përcjellje e kontrolluar e resize-/focus. Njëkohësisht, ngjarjet nuk gjithmonë përsëriten aty ku priten në FMX. Nëse nisni „quick and dirty“ këtu, tipikisht do të merrni:
- AV të sporadike gjatë mbylljes së formës (callback-et mbërrijnë pas Destroy)
- ngjarje navigimi që vijnë nga një kontekst i gabuar i thread-it
- probleme të paqëndrueshme me persistencën/cache për shkak të strategjisë së paqartë për UserDataFolder
- asnjë shkarkim ose dialogë shkarkimi të „bllokuar“
- debugging vetëm me fat në vend të një konfigurimi të synuar për Remote-Debug
Kundërmasa është një lifecycle i qartë: Create → InitializeAsync → Attach → Navigate → Detach/Dispose – dhe një kufi i përcaktuar midis UI dhe Browser-Engine.
Source-Schnipsel: WebView2Host für Delphi WebView2 FMX
Kodi vijues skicon një klasë Host të kapsulluar, që (1) krijon një konfigurim të WebView2-Environment, (2) lidh objektin Controller me një HWND, (3) lidh ngjarjet e Navigation dhe Download, dhe (4) ofron një JS-Bridge bazuar në JSON përmes WebMessageReceived. Kodi është qëllimisht „i përshtatshëm për arkitekturë“: kapsullon referencat COM, parandalon callback-et që mbërrijnë pas Destroy, dhe lejon skenare operimi si „pro User“ ose „pro Maschine“ me UserDataFolder të ndara.
unit WebView2Host;
interface
uses
System.SysUtils, System.Classes, System.IOUtils, System.JSON,
Winapi.Windows, Winapi.ActiveX,
FMX.Types,
WebView2, WebView2_TLB; // në varësi të konfigurimit: WebView2.pas ose Import-TLB
type
TWebView2JsonMessage = record
Name: string;
CorrelationId: string;
Payload: TJSONObject;
class function TryParse(const Json: string; out Msg: TWebView2JsonMessage): Boolean; static;
end;
TOnWebMessage = reference to procedure(const Msg: TWebView2JsonMessage);
TOnDownload = reference to procedure(const FileName, MimeType: string; TotalBytes: Int64);
TWebView2Host = class
private
FParentHwnd: HWND;
FUserDataFolder: string;
FEnvironment: ICoreWebView2Environment;
FController: ICoreWebView2Controller;
FWebView: ICoreWebView2;
FDestroyed: Boolean;
FOnWebMessage: TOnWebMessage;
FOnDownload: TOnDownload;
procedure EnsureNotDestroyed;
function MakeUserDataFolder: string;
// Event handler
procedure HookEvents;
procedure UnhookEvents;
procedure OnWebMessageReceived(
const sender: ICoreWebView2;
const args: ICoreWebView2WebMessageReceivedEventArgs);
procedure OnDownloadStarting(
const sender: ICoreWebView2;
const args: ICoreWebView2DownloadStartingEventArgs);
public
constructor Create(AParentHwnd: HWND; const AUserDataFolder: string = '');
destructor Destroy; override;
procedure InitializeAsync;
procedure Navigate(const Url: string);
procedure Resize(const Bounds: TRect);
procedure PostJsonToWeb(const Obj: TJSONObject);
procedure SetDevToolsEnabled(const Enabled: Boolean);
property WebView: ICoreWebView2 read FWebView;
property OnWebMessage: TOnWebMessage read FOnWebMessage write FOnWebMessage;
property OnDownload: TOnDownload read FOnDownload write FOnDownload;
end;
implementation
{ TWebView2JsonMessage }
class function TWebView2JsonMessage.TryParse(const Json: string; out Msg: TWebView2JsonMessage): Boolean;
var
V: TJSONValue;
O: TJSONObject;
begin
Result := False;
Msg.Name := '';
Msg.CorrelationId := '';
Msg.Payload := nil;
V := TJSONObject.ParseJSONValue(Json);
try
if not (V is TJSONObject) then Exit;
O := TJSONObject(V);
Msg.Name := O.GetValue('name', '');
Msg.CorrelationId := O.GetValue('cid', '');
// Payload mund të mungojë ose të jetë null
if O.TryGetValue('payload', Msg.Payload) then
Msg.Payload := TJSONObject(Msg.Payload.Clone)
else
Msg.Payload := TJSONObject.Create;
Result := Msg.Name <> '';
finally
V.Free;
end;
end;
{ TWebView2Host }
constructor TWebView2Host.Create(AParentHwnd: HWND; const AUserDataFolder: string);
begin
inherited Create;
FParentHwnd := AParentHwnd;
FUserDataFolder := AUserDataFolder;
FDestroyed := False;
end;
destructor TWebView2Host.Destroy;
begin
FDestroyed := True;
// Heqni lidhjet e eventeve para lëshimit të objekteve COM
UnhookEvents;
FWebView := nil;
FController := nil;
FEnvironment := nil;
inherited;
end;
procedure TWebView2Host.EnsureNotDestroyed;
begin
if FDestroyed then
raise EInvalidOperation.Create('WebView2Host është tashmë i shkatërruar.');
end;
function TWebView2Host.MakeUserDataFolder: string;
begin
if FUserDataFolder <> '' then
Exit(FUserDataFolder);
// Praktikë: për çdo app + për çdo përdorues Windows, jo në dosjen e programit
Result := TPath.Combine(TPath.GetHomePath, 'AppDataLocalMyCompanyMyAppWebView2');
ForceDirectories(Result);
end;
procedure TWebView2Host.InitializeAsync;
var
UserData: string;
Opt: ICoreWebView2EnvironmentOptions;
begin
EnsureNotDestroyed;
UserData := MakeUserDataFolder;
// Opsione: këtu mund të shtoni argumente shtesë për shfletuesin, p.sh. Remote-Debug
Opt := TCoreWebView2EnvironmentOptions.Create;
// Async CreateEnvironment
OleCheck(CreateCoreWebView2EnvironmentWithOptions(
nil, PWideChar(UserData), Opt,
TCoreWebView2CreateCoreWebView2EnvironmentCompletedHandler.Create(
procedure (errorCode: HRESULT; const createdEnvironment: ICoreWebView2Environment)
begin
if FDestroyed then Exit;
OleCheck(errorCode);
FEnvironment := createdEnvironment;
// Lidh controller me HWND të prindit
OleCheck(FEnvironment.CreateCoreWebView2Controller(
FParentHwnd,
TCoreWebView2CreateCoreWebView2ControllerCompletedHandler.Create(
procedure (errorCode2: HRESULT; const createdController: ICoreWebView2Controller)
begin
if FDestroyed then Exit;
OleCheck(errorCode2);
FController := createdController;
OleCheck(FController.get_CoreWebView2(FWebView));
HookEvents;
// Initial sichtbar machen
FController.put_IsVisible(1);
end)));
end)));
end;
procedure TWebView2Host.HookEvents;
var
TokenMsg, TokenDl: EventRegistrationToken;
begin
if (FWebView = nil) then Exit;
// WebMessageReceived (JS->Delphi)
TokenMsg.value := 0;
OleCheck(FWebView.add_WebMessageReceived(
TCoreWebView2WebMessageReceivedEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2WebMessageReceivedEventArgs)
begin
if FDestroyed then Exit;
OnWebMessageReceived(sender, args);
end), TokenMsg));
// DownloadStarting
TokenDl.value := 0;
OleCheck(FWebView.add_DownloadStarting(
TCoreWebView2DownloadStartingEventHandler.Create(
procedure(const sender: ICoreWebView2; const args: ICoreWebView2DownloadStartingEventArgs)
begin
if FDestroyed then Exit;
OnDownloadStarting(sender, args);
end), TokenDl));
// Shënim: Për heqje më të qëndrueshme, ruani token-et.
// Në shumë projekte kjo është e mjaftueshme nëse host-i jeton vetëm me formën.
end;
procedure TWebView2Host.UnhookEvents;
begin
// Varianti i qëndrueshëm: mbani token-et dhe thërrisni remove_*.
// Këtu si koment, sepse setup-i i import-unit dhe menaxhimi i token-eve ndryshojnë sipas wrapper-it.
end;
procedure TWebView2Host.OnWebMessageReceived(
const sender: ICoreWebView2;
const args: ICoreWebView2WebMessageReceivedEventArgs);
var
Json: PWideChar;
S: string;
Msg: TWebView2JsonMessage;
begin
Json := nil;
OleCheck(args.TryGetWebMessageAsString(Json));
try
S := Json;
finally
CoTaskMemFree(Json);
end;
if Assigned(FOnWebMessage) and TWebView2JsonMessage.TryParse(S, Msg) then
begin
try
FOnWebMessage(Msg);
finally
Msg.Payload.Free;
end;
end;
end;
procedure TWebView2Host.OnDownloadStarting(
const sender: ICoreWebView2;
const args: ICoreWebView2DownloadStartingEventArgs);
var
Dl: ICoreWebView2DownloadOperation;
Uri, Mime, ResultFile: PWideChar;
Total: Int64;
FileName: string;
begin
Uri := nil;
Mime := nil;
ResultFile := nil;
OleCheck(args.get_DownloadOperation(Dl));
OleCheck(Dl.get_TotalBytesToReceive(Total));
// Në praktikë: ResultFileName shpeshherë është bosh në fillim, në varësi të burimit.
OleCheck(Dl.get_ResultFilePath(ResultFile));
OleCheck(Dl.get_MimeType(Mime));
OleCheck(Dl.get_Uri(Uri));
try
FileName := ExtractFileName(string(ResultFile));
if FileName = '' then
FileName := 'download.bin';
if Assigned(FOnDownload) then
FOnDownload(FileName, string(Mime), Total);
// Optional: eigene Download-UI, dann Handled setzen
// args.put_Handled(1);
finally
CoTaskMemFree(Uri);
CoTaskMemFree(Mime);
CoTaskMemFree(ResultFile);
end;
end;
procedure TWebView2Host.Navigate(const Url: string);
begin
EnsureNotDestroyed;
if FWebView = nil then
raise EInvalidOperation.Create('WebView2 nuk është ende i inicializuar.');
OleCheck(FWebView.Navigate(PWideChar(Url)));
end;
procedure TWebView2Host.Resize(const Bounds: TRect);
var
R: tagRECT;
begin
if FController = nil then Exit;
R.Left := Bounds.Left;
R.Top := Bounds.Top;
R.Right := Bounds.Right;
R.Bottom := Bounds.Bottom;
OleCheck(FController.put_Bounds(R));
end;
procedure TWebView2Host.PostJsonToWeb(const Obj: TJSONObject);
var
S: string;
begin
EnsureNotDestroyed;
if FWebView = nil then Exit;
S := Obj.ToJSON;
OleCheck(FWebView.PostWebMessageAsString(PWideChar(S)));
end;
procedure TWebView2Host.SetDevToolsEnabled(const Enabled: Boolean);
var
Settings: ICoreWebView2Settings;
begin
if (FWebView = nil) then Exit;
OleCheck(FWebView.get_Settings(Settings));
OleCheck(Settings.put_AreDevToolsEnabled(Ord(Enabled)));
end;
end.
Qëllimi i qasjes
- Kapsulimi i ciklit të jetës: Forma FMX njeh vetëm „Initialize/Navigate/Resize“, jo detajet e COM.
- Ura me kontratë: Mesazhe JSON me
name, opsionalcid(Correlation-ID) dhepayloadjanë të mirëmbajtshme dhe të testueshme. - Persistencë e sigurt për operim: një kontroll i
UserDataFolderparandalon kolizionet e cache, problemet me të drejtat dhe „funksionon në kompjuterin e zhvilluesit, jo në mjedisin e prodhimit“.
JS↔Delphi-Bridge: pse WebMessage është më i qëndrueshëm se ExecuteScript
WebView2 ofron disa rrugë komunikimi. Në praktikë ExecuteScript është joshës, por i vështirë për versionim: ju dërgoni string-e në një interpretues, pa kanale të qarta përgjigjeje dhe pa një mappim të fortë të gabimeve. PostWebMessageAsString / WebMessageReceived është përkundrazi një kanal i definuar.
Skenar që shpesh paraqitet në mjedise ndërmarrjesh: duhet të nisni nga një Web-Frontend (p.sh. portal i brendshëm) një Delphi-workflow (shtypje, akses pajisjesh, integrim me legacy). Atëherë ju nevojiten:
- një listë të lejuar të emrave të mesazheve
- Correlation-IDs për përgjigje asincrone
- një vend qendror që validon payloads (p.sh. fusha të detyrueshme, kufizime madhësie)
Në host kjo është pika OnWebMessageReceived. Validimi i vërtetë i takon një shtrese më lart (p.sh. Application-Service), në mënyrë që të mbani teknikën UI/WebView2 dhe logjikën e biznesit të ndara (arkitekturë klasike e shtresave: UI → Application → Domain → Infrastruktur).
Downloads und Dateiablage: was im Betrieb oft überrascht
Shkarkimet në WebView2 menaxhohen përmes ICoreWebView2DownloadOperation. Sipas burimit ResultFilePath mund të jetë bosh herët ose të vendoset më vonë. Për më tepër, shumë kompani nuk duan që përdoruesit final të ruajnë në dosje të pakontrolluara.
Modele të provuara:
- Kapja e DownloadStarting dhe marrja e kontrollit të UI me
args.put_Handled(1)(rrugë e vet, konvencion emërtimi, dosje karantine). - Kufijtë e madhësisë së skedarëve dhe kontrollet e llojit MIME, për të shmangur „pa dashje një logfile 4 GB“.
- Auditim: shkruani metadatat e shkarkimit (URI, MIME, bytes) në logimin tuaj, jo përmbajtjen.
Nëse keni procese të rregulluara (p.sh. miratime, gjurmueshmëri), trajtimi përmes ngjarjeve është pika e vetme ku mund të integroni botën e shfletuesit në rregullat tuaja të operimit.
Debugging: DevTools, Remote Debug Port und reproduzierbare Zustände
Debugging i WebView2 shpesh dështojnë sepse gjendjet nuk janë të riprodhueshme. Dy pika rregulluese ndihmojnë:
- Aktivimi/deaktivimi i DevTools përmes
ICoreWebView2Settings(në kod:SetDevToolsEnabled) – në release shpesh i fikur, në rast supporti aktivizohet në mënyrë të synuar. - UserDataFolder i qëndrueshëm: Nëse supporti juaj duhet të riprodhojë një gabim, një rrugë e përcaktuar ka vlerë të madhe. Mund të siguroni/zip-oni dosjen (Kujdes: Datenschutz/PII) dhe të krahasoni gjendjet e synuara.
Opsionalisht (sipas wrapper) mund të konfiguroni EnvironmentOptions me argumente shtesë të shfletuesit, p.sh. një Remote-Debug-Port. Kjo është e dobishme kur duhet të analizoni një aplikacion në një sistem testimi pa mjetet lokale të zhvilluesit. Kufizimet: Në mjedise prodhimi kjo duhet të aktivizohet dhe dokumentohet rregullisht, përndryshe krijoni një sipërfaqe sulmi të panevojshme.
Capçanat në Delphi WebView2 FMX: COM, Threads dhe Form-Lifecycle
1) Callbacks pas mbylljes
Handler-ët asinkronë CompletedHandler mund të arrijnë pasi forma tashmë është duke u mbyllur. Në shembullin e kodit FDestroyed parandalon qasjen në objekte të liruar. Më i qëndrueshëm është gjithashtu:
- Ruani token-at për ngjarjet dhe në
Destroythërrisni në mënyrë të pastërremove_* - Lejoni InitializeAsync vetëm një herë (sistemi i gjendjeve: Created/Initializing/Ready/Disposed)
2) Konteksti i thread-it
Shumë handler dalin „afër UI-së“, por mos u mbështetni që mund të shkruani direkt në komponentët FMX. Nëse përditësoni UI në OnWebMessage, TThread.Queue(nil, ...) është varianti i sigurt. Unë zakonisht i ndaj përgjegjësitë: Host-i mbledh ngjarjen, Application-Service vendos, UI përditësohet ekskluzivisht përmes Queue.
3) DPI/Resize dhe FMX-Layouts
FMX llogarit në njësi logjike, WebView2 pret Pixel-Rects. Në praktikë ju duhet një vend të qartë ku përktheni bounds e FMX-Controls në piksela të vërtetë. Shembulli pranon një TRect; në formen tuaj duhet të nxirrni prej tij koordinatat WinAPI (p.sh. përmes FMX.Platform.Win dhe Handle-API-ve). Nëse aplikacioni shkallëzohet sipas DPI të monitorit, testoni kalimin midis monitorëve: WebView2 është këtu më i ndjeshëm se komponentët e pastër FMX.
Kur ia vlen WebView2 në FMX — dhe kur jo
WebView2 ia vlen kur dëshironi të përdorni teknologji web në mënyrë të synuar brenda një aplikacioni klient të rritur Delphi: pamje admin të të integruara, flukset e hyrjes OAuth/OIDC, raporte HTML, porta të brendshme ose Micro-Frontends të kontrolluara. Gjithashtu si urë modernizimi është praktik, për sa kohë ndani qartë përgjegjësitë dhe ura (bridge) nuk bëhet një derë e pasme e pakontrolluar për logjikën e biznesit.
Kufizimet e qasjes:
- Platforma: Modeli është i përqendruar te Windows. FMX është multiplatformë, WebView2 nuk është. Për macOS/iOS/Android ju duhen WebView të tjera ose një shtresë abstraksioni.
- Security/Hardening: Sapo të ngarkohen përmbajtje të jashtme, duhet të kufizoni më fort navigimin, domain-et e lejuara dhe destinacionet e shkarkimit. Kjo duhet të jetë pjesë e kërkesave, jo „më vonë“.
- Support: UserDataFolder dhe varësitë e runtime (WebView2 Runtime) duhet të jenë pjesë e konceptit tuaj të operimit/rollout.
Përfundim
Delphi WebView2 FMX është më pak një gadget UI dhe më shumë një komponent integrimi me lifecycle të vetin. Nëse kapsuloni në mënyrë të strukturuar inicializimin, eventing-un, UserDataFolder dhe JS-Bridge, WebView2 do të bëhet një bllok i qëndrueshëm për zgjidhjet dixhitale të ndërmarrjeve: Web-UI aty ku ka kuptim, dhe logjika Delphi aty ku i takon. Përndryshe, nëse ekzekutoni skripte pa kontroll, lini rrugët rastësisht dhe nuk shkëputni eventet, do të përballeni me llojin e gabimeve „sporadike në terren“ që konsumojnë kohë dhe hanë besueshmërinë.
Nëse dëshironi të integroni WebView2 në mënyrë të pastër në një aplikacion ekzistues Delphi ose të vlerësoni teknikisht një kufi modernizimi, flisni me ne:
Në kontekstin profesional edhe Webview2 Firemonkey dhe Delphi Fmx Edge Browser luajnë një rol të rëndësishëm, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të punojnë së bashku në mënyrë të pastër.
Hapi tjetër
Kur nga një temë bëhet një projekt real, arkitektura, sistemi ekzistues dhe operimi duhet të merren në konsideratë së bashku që në fazat e hershme.
Ne nuk mbështesim vetëm në çështje të veçanta, por edhe kur nga fragmente të kodit burimor, temat legacy ose idetë për portale duhet të zhvillohen në një projekt korporativ të qëndrueshëm.
- Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
- REST, akses në të dhëna, portalet dhe Rollout nuk shtyhen si pasoja të mëvonshme.
- Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.