مستوى المقال: مبتدئ
في آخر المقال هيبقى عندك مسار (endpoint) واحد بيحوّل جدول في قاعدة بياناتك لملف Excel ينزّله المستخدم على جهازه في ثانية. وكمان هتعرف الحيلة اللي تمنع السيرفر يقع لما البيانات تكبر.
تصدير بيانات قاعدتك إلى ملف Excel في Node.js
المشكلة باختصار
زر "تصدير Excel" مطلوب في أي لوحة تحكم تقريبًا. المستخدم عايز يفتح البيانات في Excel، يفلتر، ويبعتها للمدير. الطريقة الغلط الشائعة إنك ترجّع البيانات JSON وتسيب الفرونت إند يبنيها، فتطلع ملف مكسور أو بترميز عربي غلط.
خلّينا نبسّطها بمثال قبل الكود. تخيّل أمين مخزن عنده دفتر ضخم فيه كل الأصناف (دي قاعدة البيانات). المدير طلب منه كشف بأسماء وأسعار بس. أمين المخزن بينسخ الأعمدة المطلوبة في ورقة منسّقة ويسلّمها. تصدير Excel بالظبط كده: تقرا صفوف من قاعدة البيانات، تكتبها بصيغة ملف xlsx، وتبعتها للمستخدم كتنزيل.
وبشكل أدق: ملف xlsx مش نص عادي، ده ملف مضغوط بصيغة OpenXML فيها XML منظّم للورقة والخلايا. عشان كده متكتبوش بإيدك، بتستخدم مكتبة جاهزة زي exceljs تبني الصيغة الصح.
الخطوات: من جدول لملف xlsx
- ثبّت المكتبة:
npm i exceljs. - اقرأ الصفوف اللي عايز تصدّرها من قاعدة البيانات.
- ابنِ ورقة، عرّف الأعمدة (العنوان + المفتاح + العرض).
- ضيف الصفوف، ثم اكتب الملف أو ابعته في الاستجابة.
أبسط نسخة تكتب الملف على القرص عشان تجرّب:
// npm i exceljs
const ExcelJS = require('exceljs');
async function buildReport(rows) {
const wb = new ExcelJS.Workbook();
const sheet = wb.addWorksheet('المستخدمون');
sheet.views = [{ rightToLeft: true }]; // ورقة عربية من اليمين لليسار
sheet.columns = [
{ header: 'الاسم', key: 'name', width: 28 },
{ header: 'الإيميل', key: 'email', width: 32 },
{ header: 'تاريخ التسجيل', key: 'created_at', width: 20 },
];
sheet.getRow(1).font = { bold: true }; // صف العناوين bold
rows.forEach(r => sheet.addRow(r));
await wb.xlsx.writeFile('report.xlsx');
}
ملاحظة مهمة للمبتدئ: key في تعريف العمود لازم يطابق اسم الحقل في كائن الصف. لو الحقل اسمه email والمفتاح mail، الخلية هتطلع فاضية.
خلّيه endpoint حقيقي ينزّل الملف
عشان المتصفح ينزّل الملف بدل ما يعرضه نص، محتاج تظبط هيدرين: نوع المحتوى، واسم الملف للتنزيل.
const express = require('express');
const ExcelJS = require('exceljs');
const app = express();
app.get('/export/users', async (req, res) => {
const rows = await db.query('SELECT name, email, created_at FROM users');
const wb = new ExcelJS.Workbook();
const sheet = wb.addWorksheet('Users');
sheet.views = [{ rightToLeft: true }];
sheet.columns = [
{ header: 'الاسم', key: 'name', width: 28 },
{ header: 'الإيميل', key: 'email', width: 32 },
{ header: 'تاريخ التسجيل', key: 'created_at', width: 20 },
];
sheet.getRow(1).font = { bold: true };
rows.forEach(r => sheet.addRow(r));
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader('Content-Disposition', 'attachment; filename="users.xlsx"');
await wb.xlsx.write(res); // اكتب الملف مباشرة في الاستجابة
res.end();
});
خلاص. المستخدم يفتح /export/users فينزّل users.xlsx يفتحه في Excel أو Google Sheets على طول.
لما الجدول يكبر: التصدير بالتدفّق (Streaming)
الكود اللي فوق بيشتغل تمام على آلاف الصفوف. لكن لو الجدول فيه مئات الآلاف، فيه مشكلة مخبّية.
ارجع لأمين المخزن: لو قرّر ينسخ كل الأصناف على أوراق مبعثرة على مكتبه الأول قبل ما يسلّم أي حاجة، المكتب هيغرق ويقع. ده اللي بيحصل لما تحمّل كل الصفوف وتبني الملف كامل في الرام قبل ما تبعته.
الأرقام بتوضّح الفرق. على جدول فيه 200 ألف صف بثلاثة أعمدة، الطريقة اللي بتبني الملف كامل في الذاكرة ممكن تستهلك حوالي 850 ميجابايت لكل طلب، ولو طلبين وصلوا مع بعض السيرفر يقع بـ OOM. التصدير بالتدفّق بيثبّت الذاكرة عند حوالي 70 ميجابايت تقريبًا مهما كبر الجدول. (أرقام تقديرية بتختلف حسب عدد الأعمدة وطول النص، لكن الفرق في الترتيب حقيقي ومتكرر.)
الفكرة إنك تقرا صفًا صفًا من قاعدة البيانات، تكتبه في الملف، وتحرّر ذاكرته فورًا. exceljs عندها WorkbookWriter مخصص لده:
app.get('/export/users', async (req, res) => {
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader('Content-Disposition', 'attachment; filename="users.xlsx"');
const wb = new ExcelJS.stream.xlsx.WorkbookWriter({ stream: res });
const sheet = wb.addWorksheet('Users');
sheet.views = [{ rightToLeft: true }];
sheet.columns = [
{ header: 'الاسم', key: 'name', width: 28 },
{ header: 'الإيميل', key: 'email', width: 32 },
];
// اقرأ صفًا صفًا (cursor/stream) بدل ما تحمّل الكل
const cursor = db.queryStream('SELECT name, email FROM users');
for await (const row of cursor) {
sheet.addRow(row).commit(); // اكتب الصف وحرّر ذاكرته
}
sheet.commit();
await wb.commit(); // يقفل الملف وينهي الاستجابة
});
الافتراض هنا إن قاعدة بياناتك بتدعم قراءة بالتدفّق (cursor)، زي pg-query-stream مع PostgreSQL أو createReadStream في بعض المشغّلات. من غير ده، هتفضل تحمّل الكل في الرام حتى لو الكتابة streaming.
الـ trade-offs وما يجب الانتباه له
- Excel مقابل CSV. لو مش محتاج تنسيق ولا أكتر من ورقة، الـ CSV أخف وأسرع بمراحل: 200 ألف صف بيطلعوا في ملف بضعة ميجابايت وفي ثوانٍ، من غير مكتبة أصلًا. بتكسب البساطة والسرعة، بتخسر التنسيق وتحديد أنواع الخلايا. استخدم xlsx لما تحتاج عناوين bold أو عدة شيتات أو أنواع أرقام وتواريخ.
- اسم الملف العربي. لو حطيت اسم عربي في
filenameمباشرة، بعض المتصفحات بتكسره. خلّي الاسم الأساسي إنجليزي، ولو محتاج عربي استخدم صيغةfilename*=UTF-8''المرمّزة. - الأنواع. exceljs بتحترم أنواع القيم: لو بعتّ
Dateحقيقي هيتخزّن كتاريخ، ولو بعتّه نص هيفضل نص ومش هيتفلتر صح في Excel.
متى لا تستخدم هذه الطريقة
لو التصدير ضخم ومتكرر (تقارير مليون صف كل ساعة)، ماتولّدوش داخل الـ request. حوّله لـ background job (زي BullMQ) يبني الملف ويرفعه على تخزين، وابعت للمستخدم رابط تنزيل جاهز. وكمان لو كل اللي محتاجه المستخدم بيانات خام بسيطة، الـ CSV أنسب. ولو محتاج رسوم بيانية معقّدة داخل الملف، فكّر في قالب xlsx جاهز أو أداة تقارير متخصصة بدل ما تبنيها بإيدك.
التحقق من أنه يعمل
- افتح
/export/usersفي المتصفح، لازم ينزّل ملف.xlsxمش صفحة نص. - افتحه في Excel: العناوين bold، والعربي ظاهر من اليمين لليسار.
- على جدول كبير، افتح مراقب الذاكرة أثناء التصدير. لو الرام بتقفز بشكل خطي مع عدد الصفوف، انت لسه بتحمّل الكل في الذاكرة، ارجع لنسخة الـ WorkbookWriter.
الخطوة التالية
افتح تطبيقك دلوقتي وضيف مسار تصدير واحد بنسخة writeFile المحلية عشان تتأكد إن الأعمدة ظاهرة صح. بعد ما تشتغل، حوّلها لـ endpoint بالهيدرين. ولو الجدول اللي بتصدّره أكبر من حوالي 50 ألف صف، انقل مباشرة للـ WorkbookWriter من البداية بدل ما تنتظر السيرفر يقع.
مصادر
- توثيق مكتبة exceljs الرسمي (Workbook، Worksheet، وStreaming WorkbookWriter): github.com/exceljs/exceljs
- هيدر Content-Disposition وترميز أسماء الملفات (MDN): MDN Content-Disposition
- نوع MIME الرسمي لملفات xlsx (OpenXML) من IANA: IANA media types
- قراءة PostgreSQL بالتدفّق عبر pg-query-stream: node-postgres cursor/stream