Якщо єдине завдання сервера — видавати файли Excel, йому нема чого запускати сам Excel. Встановлювати Office на build-агенті чи в службі звітів, щоб керувати ним через COM-автоматизацію, — хибне рішення, і воно лишається хибним стільки, скільки існує ця практика. Про це прямо каже сама Microsoft у настановах, які не пом’якшилися за двадцять років: Office не створений і не ліцензований для автоматизації з неконтрольованого серверного процесу без користувача. Правильна відповідь — писати байти BIFF та OOXML напряму, без жодного Excel у картині. Саме на цьому будується HotXLS — нативна бібліотека Object Pascal, яка сама читає й записує формати електронних таблиць, тож немає десктопного застосунку, який може зависнути, витекти пам’яттю чи вимагати оплати за кожне робоче місце
Чому запуск EXCEL.EXE зі служби приречений на провал
COM-автоматизація дистанційно керує десктопною програмою, а десктопна програма мовчки покладається на три речі, яких служба Windows дати їй не може: завантажений профіль користувача, інтерактивну віконну станцію та людину, що дивиться на екран. Прибери це — і збої проявляються у формі, яку жодна машина розробника ніколи не відтворює. Запит на відновлення файлу, помилка надбудови чи діалог активації ліцензії відкривається на робочому столі, якого ніхто не бачить, і виклик автоматизації, що його спричинив, ніколи не повертається. Викликач зрештою обриває чекання за таймаутом і завершується; екземпляр Excel часто не завершується, лишаючись сиротою, що тримає блокування файлів і отруює наступний запуск. Кожен, хто бачив, як одинадцять безхазяйних процесів EXCEL.EXE накопичуються під обліковим записом служби, знає решту цієї історії
Історія з масштабуванням не краща, навіть коли нічого не падає. Екземпляр Excel — це конвеєр з однією книгою, кожне звернення до властивості платить ціну міжпроцесного маршалінгу COM, а машина, що виконує код, несе ліцензію Office, умови якої виключають саме такий спосіб використання. Більшість команд натрапляють на ці межі одна відмова за раз, і саме так «прибрати шар COM» зазвичай і потрапляє у план розвитку
Перш ніж починати цей переписаний код, вирішіть одне питання щодо обсягу, бо воно визначає, скільки роботи насправді потрібно. Код COM майже ніколи просто не встановлює значення клітинок. Він викликає Workbook.SaveAs з константами формату, примусово перераховує значення, задає параметри друку, а іноді звертається до буфера обміну. Пройдіться старим кодом і випишіть, яка з цих поведінок насправді потрапляє у вивід, адже кожна лягає у свій куточок нативної бібліотеки, а декілька з них (найочевидніший приклад — взаємодія з буфером обміну) не мають серверного сенсу, і їх варто відкинути, а не переносити
Два нативних рушії, дві моделі володіння
HotXLS замінює процес Excel двома прямими реалізаціями формату. Рушій потоку записів BIFF8 (TXLSWorkbook, модуль lxHandle) обробляє .xls. Пакетний записувач OOXML (TXLSXWorkbook, модуль lxHandleX) видає .xlsx, що відповідає ECMA-376 / ISO/IEC 29500. На сервері немає нічого, що треба реєструвати чи встановлювати, і можна тримати відкритими стільки книг одночасно, скільки дозволяє пам’ять
Що спантеличує на початку — це те, що два фасади по-різному володіють своєю пам’яттю, і ця різниця мовчить, поки не спричинить збій:
var
Book: IXLSWorkbook; // посилання на інтерфейс: звільняється автоматично
Sheet: IXLSWorksheet;
BookX: TXLSXWorkbook; // звичайний об’єкт: звільняєте самі
SheetX: TXLSXWorksheet;
begin
// вивід BIFF8 .xls - без Free; лічильник посилань інтерфейсу сам володіє ним
Book := TXLSWorkbook.Create;
Sheet := Book.Sheets.Add;
Sheet.Name := 'Report';
Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
Book.SaveAs('report.xls');
// вивід OOXML .xlsx - явний час життя
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;
Фасад XLS підраховує посилання через інтерфейс IXLSWorkbook. Оголосіть змінну як тип інтерфейсу і ніколи не викликайте на ній Free; тримайте той самий об’єкт у змінній звичайного об’єктного типу й звільняйте його самостійно — і лічильник посилань звільнить його вдруге. Фасад XLSX — звичайний об’єкт, який хоче звичайного try..finally. Адресація клітинок з обох боків 1-базована, і це єдине місце, де вони збігаються. А от колекції аркушів — ні: Entries з боку XLS 1-базований, індексатор Items у XLSX — 0-базований, і ця помилка на одиницю компілюється однаково чисто, хоч би в який бік ви помилилися, і проявляє себе лише під час виконання
Запис книги напряму у відповідь HTTP
Серверний експорт зазвичай не має причин торкатися диска. Тимчасові файли вимагають політики очищення, конфліктують під час одночасних запитів і лишають дані клієнтів на томах, які ніхто не здогадався перевірити. Обидва фасади приймають TStream через перевантаження SaveAs, тож книга може піти прямо у відповідь:
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
Book.SaveAs(Mem); // пише від ПОТОЧНОЇ позиції потоку
Mem.Position := 0; // перемотати назад, перш ніж передати потік далі
Response.ContentType :=
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
Response.ContentStream := Mem; // тепер Mem належить фреймворку
finally
Book.Free;
end;
Саме перемотування — той рядок, що заслуговує на коментар. SaveAs(Stream) пише від поточної позиції потоку і ніколи потім не перемотується на нуль. Забудьте Mem.Position := 0 — і клієнт отримає завантаження нульового розміру, або Excel назве файл пошкодженим. Це найпоширеніша помилка в коді книг, орієнтованому на веб, і найжорстокіша, бо вона проходить повз будь-який модульний тест, що перевіряє лише ненульову довжину потоку
Одна процедура побудови книги охоплює кожен інший формат доставки без перебудови. SaveAsCSV відповідає на запит «просто дайте мені сирі дані», SaveAsHTML обробляє «вставте це на сторінку порталу», SaveAsRTF живить конвеєри документів, а SaveAsODS покриває вимогу OpenDocument — і все це з перевантаженнями як для файлу, так і для потоку. Один-єдиний експортний метод плюс параметр формату замінює те, що зазвичай було чотирма окремими макросами COM. TXLSXHtmlExportOptions HTML-експортера несе заголовок, CSS-клас і перемикач «фрагмент чи повний документ», що тримає сценарій порталу подалі від редагування експортованої розмітки регулярними виразами
Значення формул без процесу Excel, який їх обчислює
Під COM-автоматизацією Excel перераховував усе безкоштовно, і відмова від COM тихо скасовує це. SaveAs зберігає формули як текст, не обчислюючи їх; числа з’являються лише тоді, коли Excel відкриває файл і перераховує його — поведінку, яку фасад XLS дозволяє налаштувати через RecalcOnSave і CalculationMode. Для файлу, що йде до людини, це саме те, що треба. Це неправильно для служби, яка мусить підтвердити суму до відправлення, і неправильно для експорту CSV, який записує текст формули, а не її результат. В обох випадках обчислення треба виконати на сервері вбудованим рушієм:
SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)'; // фасад XLSX: без префікса '='
Total := BookX.Calculate('SUM(A1:A2)'); // обчислити на сервері прямо зараз
if Total <> 2150 then
raise Exception.Create('reconciliation failed before delivery');
Домовленість фасадів кусається і тут. Бік XLSX присвоює вирази через Cell.Formula без знака рівності; бік XLS записує їх через Cell.Value з провідним '='. Перенесіть код з одного боку на інший без змін — і неправильна домовленість збереже текстовий рядок, який лише нагадує формулу, без жодної помилки, яка б це позначила. Коли формулам книги потрібно дотягнутися до вашої власної бізнес-логіки, зворотний виклик OnUserFunction дозволяє рушію передавати невідомі імена функцій коду Delphi під час обчислення. Це нативна заміна надбудовам UDF, які зазвичай ховаються саме в тих таблицях, навколо яких виросла система COM-автоматизації
Нюанси розгортання, що проявляються лише на сервері
Кілька деталей вирішують, чи буде розгортання чистим, чи спантеличливим, і перша з них — граф модулів. Експортер набору даних методом перетягування TDataToXLS тягне за собою VCL-модулі Forms, Controls і Dialogs. Нешкідливо в десктопному інструменті; у консольній службі це тягне за собою весь VCL. Основні модулі lxHandle і lxHandleX звертаються лише до Windows, Classes, SysUtils і Variants, тож чистій службі краще написати власний цикл проти набору даних, спираючись на базовий API, ніж імпортувати компонент заради зручності
Далі йде багатопотоковість. Екземпляри книг не є потокобезпечними, але й не мають спільного глобального стану, тож масштабується найпростіший підхід: один об’єкт книги на завдання або на робочий потік. Це дає паралельну генерацію звітів, чого один спільний екземпляр Excel ніколи не зможе зробити. Обробник запиту, що створює, наповнює, зберігає і звільняє власну книгу, не потребує жодних блокувань, а радіус ураження від збою звужується з «спільний екземпляр Excel заклинило для всіх» до «цей один запит викликав виняток», з чим уже вміє впоратися ваша наявна обробка помилок
Останнє — вибір цільового формату. TXLSWorkbook.SaveAs за замовчуванням пише BIFF (xlExcel97), а проштовхування вмісту XLS у .xlsx проходить через міст SaveXLSWorkbookAsXLSX зі зниженою точністю. Обирайте фасад за форматом, який плануєте постачати, на етапі проєктування, а не будуйте в одному форматі й конвертуйте наприкінці конвеєра
Для половини типового проєкту заміни, що стосується завантаження даних, шаблони експорту з бази даних у книгу охоплюють і компонент, і написаний руками цикл, а щойно кількість рядків сягає шести цифр, техніки продуктивності для великих книг стають різницею між хвилинами й секундами. Звіти, побудовані з макетів, що підтримуються в дизайнері, розглянуто в огляді генерації звітів за шаблонами
HotXLS постачається як вихідний код Object Pascal для Delphi та C++Builder; редакції, ліцензування та повний довідник API — на сторінці продукту HotXLS Delphi Component