Jeśli jedynym zadaniem serwera jest emitowanie plików Excela, nie ma on żadnego interesu w uruchamianiu Excela. Instalowanie Office na agencie kompilacji albo w usłudze raportowej po to, by sterować nim przez automatyzację COM, jest złym projektem i jest złym projektem od tak dawna, jak długo ta praktyka istnieje. Mówi to sam Microsoft, w wytycznych, które nie złagodniały przez dwadzieścia lat: Office nie jest ani zbudowany, ani licencjonowany do automatyzacji z bezobsługowego procesu po stronie serwera. Właściwą odpowiedzią jest zapisywanie bajtów BIFF i OOXML wprost, bez Excela na obrazku w ogóle. To cała przesłanka HotXLS, natywnej biblioteki Object Pascal, która sama czyta i zapisuje formaty arkuszowe, więc nie ma aplikacji desktopowej, która mogłaby się zawiesić, przeciekać albo kosztować za stanowisko
Dlaczego sterowanie EXCEL.EXE z usługi zawodzi
Automatyzacja COM zdalnie steruje programem desktopowym, a program desktopowy po cichu zakłada trzy rzeczy, których usługa Windows nie może mu podać: wczytany profil użytkownika, interaktywną stację okien i człowieka patrzącego na ekran. Odbierz to, a awarie przychodzą w postaci, której żadna maszyna deweloperska nigdy nie odtwarza. Monit odzyskiwania pliku, błąd dodatku albo okno aktywacji licencji otwiera się na pulpicie, którego nikt nie widzi, a wywołanie automatyzacji, które je wyzwoliło, nigdy nie wraca. Wołający w końcu przekracza limit czasu i umiera; instancja Excela często nie, przeżywając jako sierota, która trzyma blokady plików i zatruwa następny przebieg. Każdy, kto patrzył, jak jedenaście zbłąkanych procesów EXCEL.EXE piętrzy się na koncie usługi, zna resztę tej opowieści
Opowieść o skalowaniu nie jest lepsza nawet wtedy, gdy nic się nie wywala. Instancja Excela to potok na jeden skoroszyt, każdy dostęp do właściwości płaci koszt międzyprocesowego marshalingu COM, a maszyna uruchamiająca ten kod niesie licencję Office, której warunki wykluczają dokładnie to zastosowanie. Większość zespołów spotyka te granice po jednej awarii naraz, i mniej więcej tak "wycofanie warstwy COM" ląduje w planie prac
Zanim ten przepis się zacznie, rozstrzygnij jedno pytanie o zakres, bo ono decyduje, ile z tej pracy jest realne. Kod COM prawie nigdy nie ustawia tylko wartości komórek. Woła Workbook.SaveAs ze stałymi formatu, wymusza przeliczenie, wypycha ustawienia wydruku, czasem sięga po schowek. Przejdź stary kod i spisz, które z tych zachowań naprawdę trafiają do wyjścia, bo każde ląduje w innym rogu natywnej biblioteki, a kilka z nich (interop ze schowkiem jest tu oczywisty) nie ma po stronie serwera żadnego sensu i powinno zostać porzucone, a nie przeniesione
Dwa natywne silniki, dwa modele własności
HotXLS wymienia proces Excela na dwie bezpośrednie implementacje formatów. Silnik strumienia rekordów BIFF8 (TXLSWorkbook, jednostka lxHandle) obsługuje .xls. Zapisujący pakiety OOXML (TXLSXWorkbook, jednostka lxHandleX) produkuje .xlsx zgodne z ECMA-376 / ISO/IEC 29500. Nie ma nic do zarejestrowania ani nic do zainstalowania na serwerze, a otwartych skoroszytów naraz możesz trzymać tyle, ile pozwoli pamięć
To, co wcześnie podkłada ludziom nogę, to fakt, że obie fasady inaczej władają swoją pamięcią, a różnica milczy aż do awarii:
var
Book: IXLSWorkbook; // referencja interfejsowa: zwalniana automatycznie
Sheet: IXLSWorksheet;
BookX: TXLSXWorkbook; // zwykły obiekt: zwalniasz go sam
SheetX: TXLSXWorksheet;
begin
// wyjście BIFF8 .xls - bez Free; włada nim licznik referencji interfejsu
Book := TXLSWorkbook.Create;
Sheet := Book.Sheets.Add;
Sheet.Name := 'Report';
Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
Book.SaveAs('report.xls');
// wyjście OOXML .xlsx - jawny czas życia
BookX := TXLSXWorkbook.Create;
try
SheetX := BookX.Sheets.Add('Report');
SheetX.Cells[1, 1].Value := 'Generated without Excel';
BookX.SaveAs('report.xlsx');
finally
BookX.Free;
end;
end;
Fasada XLS jest zliczana referencyjnie przez interfejs IXLSWorkbook. Zadeklaruj zmienną jako typ interfejsowy i nigdy nie wołaj na niej Free; przetrzymaj ten sam obiekt w zwykłej zmiennej obiektowej i zwolnij go sam, a licznik referencji zwolni go po raz drugi. Fasada XLSX to zwyczajny obiekt, który chce zwyczajnego try..finally. Adresowanie komórek liczy się od jedynki po obu stronach, i to jedyne miejsce, w którym obie się zgadzają. Kolekcje arkuszy już nie: Entries po stronie XLS liczy od jedynki, indekser Items w XLSX liczy od zera, a ten błąd o jeden kompiluje się czysto, którąkolwiek stronę pomylisz, i pokazuje się dopiero w czasie działania
Zapis skoroszytu prosto do odpowiedzi HTTP
Eksport po stronie serwera zwykle nie ma powodu, by dotykać dysku. Pliki tymczasowe wymagają polityki sprzątania, kolidują przy równoczesnych żądaniach i zostawiają dane klientów na wolumenach, których nikt nie pomyślał audytować. Obie fasady biorą TStream przez swoje przeciążenia SaveAs, więc skoroszyt może pójść prosto do odpowiedzi:
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
Book.SaveAs(Mem); // zapisuje od BIEŻĄCEJ pozycji strumienia
Mem.Position := 0; // przewiń przed przekazaniem strumienia
Response.ContentType :=
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
Response.ContentStream := Mem; // od teraz Mem należy do szkieletu
finally
Book.Free;
end;
Przewinięcie to linia, która zarabia na swój komentarz. SaveAs(Stream) zapisuje od bieżącej pozycji strumienia i nigdy potem nie wraca do zera. Zapomnij o Mem.Position := 0, a klient dostanie pobranie o zerowej długości albo Excel nazwie plik uszkodzonym. To najczęstszy błąd w kodzie skoroszytowym zwróconym ku sieci i najokrutniejszy, bo przemyka obok każdego testu jednostkowego, który sprawdza tylko, że strumień ma niezerową długość
Jedna procedura budująca skoroszyt sięga do każdego innego formatu dostawy bez restrukturyzacji. SaveAsCSV odpowiada na prośbę "daj mi po prostu surowe dane", SaveAsHTML obsługuje "wrzuć to na stronę portalu", SaveAsRTF zasila potoki dokumentowe, a SaveAsODS pokrywa nakaz OpenDocument, wszystkie z przeciążeniami plikowymi i strumieniowymi. Jedna procedura eksportu plus parametr formatu zastępuje to, co bywało czterema osobnymi makrami COM. TXLSXHtmlExportOptions eksportera HTML niesie tytuł, klasę CSS i przełącznik fragment-albo-pełny-dokument, co trzyma przypadek portalu z dala od edytowania wyeksportowanego znacznika wyrażeniami regularnymi
Wartości formuł bez procesu Excela, który by je policzył
Pod automatyzacją COM Excel przeliczał wszystko za darmo, a porzucenie COM po cichu to cofa. SaveAs przechowuje formuły jako tekst, nie wyliczając ich; liczby pojawiają się dopiero, gdy Excel otworzy plik i przeliczy, a fasada XLS pozwala nastroić to zachowanie przez RecalcOnSave i CalculationMode. Dla pliku idącego do człowieka to dokładnie właściwe. Jest złe dla usługi, która musi potwierdzić sumę, zanim ją wyśle, i złe dla eksportu CSV, który zapisuje tekst formuły zamiast jej wyniku. Każdy z tych przypadków musi wyliczyć na serwerze wbudowanym silnikiem:
SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)'; // fasada XLSX: bez prefiksu '='
Total := BookX.Calculate('SUM(A1:A2)'); // wylicz na serwerze, teraz
if Total <> 2150 then
raise Exception.Create('reconciliation failed before delivery');
Konwencja fasad gryzie tu ponownie. Strona XLSX przypisuje wyrażenia przez Cell.Formula bez znaku równości; strona XLS zapisuje je przez Cell.Value z wiodącym '='. Przenieś kod z jednej do drugiej bez zmian, a niewłaściwa konwencja zapisze ciąg tekstowy jedynie przypominający formułę, bez żadnego błędu, który by to zgłosił. Kiedy formuły skoroszytu muszą sięgnąć w twoją własną logikę biznesową, wywołanie zwrotne OnUserFunction pozwala silnikowi oddać nieznane nazwy funkcji kodowi Delphi w czasie wyliczania. To natywny zamiennik dodatków UDF, które lubią chować się w tych właśnie arkuszach, wokół których wyrósł system z automatyzacją COM
Krawędzie wdrożeniowe, które wychodzą dopiero na serwerze
O tym, czy wdrożenie będzie czyste czy zagadkowe, decyduje kilka szczegółów, a pierwszym z nich jest graf jednostek. Eksporter zbiorów danych typu przeciągnij i upuść, TDataToXLS, wciąga z VCL Forms, Controls i Dialogs. Nieszkodliwe w narzędziu desktopowym; w usłudze konsolowej ciągnie za sobą całe VCL. Rdzeniowe jednostki lxHandle i lxHandleX sięgają tylko po Windows, Classes, SysUtils i Variants, więc czystej usłudze lepiej wyjdzie napisanie własnej pętli po zbiorze danych na rdzeniowym API niż zaimportowanie komponentu dla wygody
Dalej jest wielowątkowość. Instancje skoroszytu nie są bezpieczne wątkowo, ale nie dzielą też żadnego stanu globalnego, więc wzorcem, który się skaluje, jest ten najprostszy: jeden obiekt skoroszytu na zadanie albo na wątek roboczy. To kupuje równoległe generowanie raportów, czego pojedyncza współdzielona instancja Excela nigdy nie potrafi. Obsługa żądania, która tworzy, wypełnia, zapisuje i zwalnia własny skoroszyt, nie potrzebuje żadnych blokad, a promień rażenia awarii kurczy się z "współdzielona instancja Excela zaklinowała się dla wszystkich" do "to jedno żądanie podniosło wyjątek", z czym twoja istniejąca obsługa błędów już wie, co zrobić
Celowanie w format jest ostatnim z nich. TXLSWorkbook.SaveAs zapisuje domyślnie BIFF (xlExcel97), a wpychanie treści XLS do .xlsx idzie przez pomost SaveXLSWorkbookAsXLSX o obniżonej wierności. Wybieraj fasadę według formatu, który zamierzasz wysyłać, na etapie projektowania, zamiast budować w jednym i konwertować na końcu potoku
Dla tej połowy typowego projektu wymiany, która dotyczy wczytywania danych, wzorce eksportu z bazy danych do skoroszytu omawiają zarówno komponent, jak i ręcznie pisaną pętlę, a gdy liczby wierszy sięgną sześciu cyfr, techniki wydajnościowe dla dużych skoroszytów stają się różnicą między minutami a sekundami. Raporty budowane z układów utrzymywanych przez projektanta omawia przewodnik po generowaniu raportów z szablonów
HotXLS jest dostarczany jako kod źródłowy w Object Pascalu dla Delphi i C++Buildera; edycje, licencjonowanie i pełna referencja API są na stronie produktu HotXLS Delphi Component