إذا كانت الوظيفة الوحيدة للخادم هي إصدار ملفات Excel، فليس من شأنه تشغيل Excel. إن تثبيت Office على وكيل بناء أو خدمة تقارير لتوجيهه من خلال COM automation هو تصميم خاطئ، وقد كان تصميماً خاطئاً طالما كانت هذه الممارسة موجودة. تقول Microsoft ذلك بنفسها، في توجيهات لم تتغير منذ عشرين عاماً: لم يتم بناء Office ولا ترخيصه ليتم أتمتته من عملية غير خاضعة للرقابة من جانب الخادم (server-side process). الإجابة الصحيحة هي كتابة بايتات BIFF و OOXML مباشرة، دون وجود Excel في الصورة على الإطلاق. هذا هو الأساس الكامل لـ HotXLS، وهي مكتبة Object Pascal أصلية تقرأ وتكتب تنسيقات جداول البيانات بنفسها، لذلك لا يوجد تطبيق سطح مكتب قد يتعطل، أو يسرب الذاكرة، أو يتطلب الدفع لكل مستخدم
لماذا يفشل توجيه EXCEL.EXE من خدمة
تتحكم COM automation عن بُعد في برنامج سطح مكتب، ويفترض برنامج سطح المكتب بهدوء ثلاثة أشياء لا يمكن لخدمة Windows تقديمها: ملف تعريف مستخدم محمل (user profile)، ومحطة نافذة تفاعلية (interactive window station)، وإنسان يراقب الشاشة. جرّد هذه الأشياء وستصل الإخفاقات في شكل لا يمكن لأي جهاز مطور إعادة إنتاجه على الإطلاق. تفتح مطالبة استرداد الملفات، أو خطأ في الوظيفة الإضافية، أو مربع حوار تنشيط الترخيص على سطح مكتب لا يمكن لأحد رؤيته، ومكالمة الأتمتة التي أطلقتها لا تعود أبدًا. ينتهي الأمر بالمتصل بانتهاء المهلة الزمنية ويموت؛ بينما في كثير من الأحيان لا يحدث ذلك لنسخة Excel، وتعيش كعملية يتيمة تحتفظ بأقفال الملفات وتسمم التشغيل التالي. أي شخص شاهد أحد عشر عملية EXCEL.EXE ضالة تتراكم تحت حساب خدمة يعرف بقية تلك القصة
قصة التوسع (scaling) ليست أفضل حتى عندما لا يتعطل أي شيء. نسخة Excel هي مسار لمصنف واحد (single-workbook pipeline)، ويدفع كل وصول إلى خاصية تكلفة تجميع COM عبر العمليات (cross-process COM marshaling)، ويحمل الجهاز الذي يشغل الكود ترخيص Office تستبعد شروطه هذا الاستخدام الدقيق. تواجه معظم الفرق هذه الحدود كانقطاع تلو الآخر، وهو تقريبًا كيف ينتهي الأمر بـ "إلغاء طبقة COM" على خارطة الطريق
قبل أن تبدأ عملية إعادة الكتابة تلك، قم بتسوية سؤال واحد حول النطاق، لأنه يحدد مقدار العمل الحقيقي. كود COM نادراً ما يقوم فقط بتعيين قيم الخلايا. إنه يستدعي Workbook.SaveAs مع ثوابت التنسيق، ويجبر إعادة الحساب، ويدفع إعداد الطباعة، ويصل أحياناً إلى الحافظة (clipboard). تصفح الكود القديم واكتب أي من هذه السلوكيات يتم شحنه فعلياً في المخرجات، لأن كل منها يقع في زاوية مختلفة من المكتبة الأصلية، واثنان منها (التعامل مع الحافظة كونه الواضح) ليس لهما معنى من جانب الخادم ويجب إسقاطهما بدلاً من نقلهما
محركان أصليان، نموذجان للملكية
يستبدل HotXLS عملية Excel بتنفيذين مباشرين للتنسيق. يتعامل محرك تدفق سجلات BIFF8 (TXLSWorkbook، الوحدة lxHandle) مع .xls. وينتج كاتب حزمة OOXML (TXLSXWorkbook، الوحدة lxHandleX) ملفات .xlsx تتوافق مع ECMA-376 / ISO/IEC 29500. لا يوجد شيء للتسجيل ولا شيء للتثبيت على الخادم، ويمكنك إبقاء أي عدد تريده من المصنفات مفتوحاً في وقت واحد طالما تسمح الذاكرة بذلك
ما يوقع الناس في الخطأ مبكراً هو أن الواجهتين تملكان ذاكرتهما بشكل مختلف، والفرق يكون صامتاً حتى يتعطل:
var
Book: IXLSWorkbook; // interface reference: released automatically
Sheet: IXLSWorksheet;
BookX: TXLSXWorkbook; // plain object: you free it
SheetX: TXLSXWorksheet;
begin
// BIFF8 .xls output - no Free; the interface refcount owns it
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 output - explicit lifetime
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 يتم حساب مرجعها (reference-counted) من خلال واجهة IXLSWorkbook. صرح عن المتغير كنوع الواجهة ولا تستدعي Free عليه أبداً؛ واحتفظ بنفس الكائن في متغير كائن عادي وقم بتحريره بنفسك، وسيقوم حساب المرجع بتحريره مرة ثانية. واجهة XLSX عبارة عن كائن عادي يتطلب try..finally عادية. عنونة الخلايا تعتمد على الأساس 1 على كلا الجانبين، وهو المكان الوحيد الذي يتفقان فيه. لا تتفق مجموعات الأوراق (sheet collections): تعتمد Entries على الجانب XLS على الأساس 1، ومفهرس Items الخاص بـ XLSX يعتمد على الأساس 0، وهذا الخطأ بمقدار واحد يتم تجميعه (compiles) بسلاسة بغض النظر عن الطريقة التي تخطئ بها ولا يظهر إلا في وقت التشغيل (runtime)
كتابة مصنف مباشرة في استجابة HTTP
عادة لا يوجد سبب يجعل تصدير جانب الخادم يلمس القرص. تتطلب الملفات المؤقتة سياسة تنظيف، وتتصادم تحت الطلبات المتزامنة، وتترك بيانات العملاء جالسة على وحدات تخزين لم يفكر أحد في تدقيقها. تأخذ كلتا الواجهتين TStream من خلال الأحمال الزائدة (overloads) لـ SaveAs الخاصة بهما، بحيث يمكن للمصنف الانتقال مباشرة إلى الاستجابة:
Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
Book.SaveAs(Mem); // writes from the CURRENT stream position
Mem.Position := 0; // rewind before handing the stream over
Response.ContentType :=
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
Response.ContentStream := Mem; // the framework now owns Mem
finally
Book.Free;
end;
الإرجاع (rewind) هو السطر الذي يستحق تعليقه. يكتب SaveAs(Stream) من الموضع الحالي للتدفق ولا يعود أبدًا إلى الصفر بعد ذلك. انسَ Mem.Position := 0 وسيحصل العميل على تنزيل بصفر بايت، أو يسمي Excel الملف تالفاً. هذا هو الخطأ الأكثر شيوعاً في كود المصنف المواجه للويب، والأكثر قسوة، لأنه يتجاوز أي اختبار وحدة (unit test) يؤكد فقط أن التدفق له طول غير صفري
يصل روتين بناء مصنف واحد إلى كل تنسيق تسليم آخر دون إعادة هيكلة. يجيب SaveAsCSV على طلب "أعطني البيانات الخام فقط"، ويتعامل SaveAsHTML مع "أسقطه في صفحة بوابة"، ويغذي SaveAsRTF مسارات المستندات، ويغطي SaveAsODS تفويض OpenDocument، كل ذلك مع كل من أحمال الملفات والتدفقات (stream overloads). روتين تصدير واحد بالإضافة إلى معلمة تنسيق يحل محل ما كان يميل إلى أن يكون أربع وحدات ماكرو COM منفصلة. يحمل TXLSXHtmlExportOptions الخاص بمصدر HTML العنوان، وفئة CSS، ومفتاح تبديل بين جزء أو مستند كامل، مما يبقي حالة البوابة خارج عمل تحرير العلامات المصدرة باستخدام regex
قيم الصيغة بدون عملية Excel لحسابها
في ظل COM automation، يعيد 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 facade: no '=' prefix
Total := BookX.Calculate('SUM(A1:A2)'); // evaluate on the server, now
if Total <> 2150 then
raise Exception.Create('reconciliation failed before delivery');
تتسبب اتفاقية الواجهة في مشكلة مرة أخرى هنا. يقوم الجانب XLSX بتعيين التعبيرات من خلال Cell.Formula بدون علامة يساوي؛ ويكتبها الجانب XLS من خلال Cell.Value مع '=' بادئة. انقل الكود من أحدهما إلى الآخر دون تغيير وستخزن الاتفاقية الخاطئة سلسلة نصية تشبه الصيغة فقط، مع عدم وجود خطأ للإشارة إليها. عندما تحتاج صيغ المصنف إلى الوصول إلى منطق الأعمال الخاص بك، فإن الاستدعاء OnUserFunction يتيح للمحرك تسليم أسماء الوظائف غير المعروفة إلى كود Delphi في وقت التقييم. هذا هو البديل الأصلي للوظائف الإضافية UDF التي تميل إلى الاختباء داخل جداول البيانات ذاتها التي نشأ حولها نظام COM-automation
حواف النشر التي تظهر فقط على الخادم
تحدد بعض التفاصيل ما إذا كان الطرح نظيفاً أم محيراً، وأولها هو الرسم البياني للوحدة (unit graph). يسحب مُصدِّر مجموعة البيانات بالسحب والإفلات TDataToXLS كل من Forms و Controls و Dialogs لـ VCL. هذا غير ضار في أداة سطح المكتب؛ ولكن في خدمة وحدة التحكم (console service) فإنه يسحب VCL بالكامل وراءه. تصل الوحدات الأساسية lxHandle و lxHandleX فقط إلى Windows و Classes و SysUtils و Variants، لذلك من الأفضل لخدمة خالصة كتابة حلقة مجموعة البيانات (dataset loop) الخاصة بها مقابل واجهة برمجة التطبيقات الأساسية (core API) بدلاً من استيراد المكون من أجل الراحة
ثم هناك الترابط (threading). مثيلات المصنف ليست آمنة لمؤشر الترابط (thread-safe)، لكنها لا تشترك في أي حالة عامة أيضاً، لذا فإن النمط الذي يتوسع هو الأبسط: كائن مصنف لكل مهمة، أو لكل مؤشر ترابط عامل (worker thread). هذا يشتري إنشاء تقارير متوازية، وهو ما لا يمكن لنسخة Excel مشتركة واحدة القيام به أبداً. لا يحتاج معالج الطلب الذي ينشئ ويملأ ويحفظ ويحرر مصنفه الخاص إلى أقفال على الإطلاق، وينهار نصف قطر الانفجار للفشل من "نسخة Excel المشتركة عالقة للجميع" إلى "هذا الطلب الواحد أثار استثناءً"، والذي تعرف معالجة الأخطاء الحالية لديك بالفعل ما يجب فعله معه
استهداف التنسيق هو الأخير منها. يكتب TXLSWorkbook.SaveAs التنسيق BIFF (xlExcel97) افتراضياً، ودفع محتوى XLS إلى .xlsx يمر عبر جسر SaveXLSWorkbookAsXLSX بدقة منخفضة. اختر الواجهة حسب التنسيق الذي تنوي شحنه، في وقت التصميم، بدلاً من البناء في تنسيق والتحويل في نهاية المسار
بالنسبة للنصف الخاص بتحميل البيانات من مشروع استبدال نموذجي، تغطي أنماط تصدير قاعدة البيانات إلى المصنف كلاً من المكون والحلقة المكتوبة يدوياً، وبمجرد أن تصل أعداد الصفوف إلى ستة أرقام تصبح تقنيات أداء المصنفات الكبيرة هي الفارق بين الدقائق والثواني. تتم تغطية التقارير المبنية من التخطيطات التي يحتفظ بها المصمم في دليل إنشاء تقارير القوالب
يتم شحن HotXLS كمصدر Object Pascal لـ Delphi و C++Builder؛ تتوفر الإصدارات والترخيص والمرجع الكامل لواجهة برمجة التطبيقات على صفحة منتج مكون HotXLS