الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
How To Make It

اعمل خدمة توليد فواتير PDF من HTML بـ Node.js و Puppeteer

متوسط27 يوليو 20266 دقائق قراءة
اعمل خدمة توليد فواتير PDF من HTML بـ Node.js و Puppeteer

هذا المقال يتطلب مستوى: متوسط

اعمل خدمة توليد فواتير PDF من HTML بـ Node.js و Puppeteer

هتخرج من المقال ده بخدمة حقيقية بتحوّل أي فاتورة HTML لملف PDF جاهز للتحميل، بنفس تنسيق صفحتك بالظبط: خط عربي، اتجاه RTL، وجدول أسعار مضبوط. الشغل كله في أقل من 80 سطر.

الافتراض إن عندك Node.js 18 أو أحدث، وحجم الحِمل ≤ 20 فاتورة في الثانية على سيرفر صغير. فوق كده محتاج طابور، وهنقولك امتى بالظبط.

مخطط مبسط يوضح تحويل ملف HTML إلى مستند PDF عبر محرك Chromium بلا واجهة باستخدام Node.js و Puppeteer

المشكلة باختصار

توليد PDF من الصفر بمكتبة رسم — يعني ترسم كل سطر بإحداثيات x و y — بيتحوّل لكابوس أول ما الفاتورة يكون فيها جدول، شعار، أو نص عربي. الطريقة دي بتفشل بسرعة لأنك بتعيد اختراع محرك تنسيق موجود أصلاً في متصفحك.

البديل: تكتب الفاتورة HTML و CSS عادي — اللي انت شاطر فيه أصلاً — وتخلّي المتصفح هو اللي يطبعها PDF. نفس المحرك اللي بيعرض صفحتك بيولّد الملف، فاللي بتشوفه هو اللي بينزل.

الفكرة: متصفح بلا واجهة

تخيّل مطبعة كاملة في أوضة خلفية: فيها كل الماكينات والحبر والورق، بس من غير شاشة عرض قدامها. بتبعتلها ورقة تصميم، وبترجّعلك نسخة مطبوعة، وانت مش شايف الأوضة أصلاً. ده بالظبط المتصفح بلا واجهة (Headless Browser).

علميًا: Puppeteer بيشغّل نسخة حقيقية من Chromium من غير واجهة رسومية. نفس محرك التنسيق (Blink) اللي في كروم العادي بيقرأ الـ DOM، يحسب التخطيط والخطوط والألوان، وبعدين يصدّر الصفحة PDF عبر أمر printToPDF في بروتوكول DevTools. فالنتيجة مطابقة للعرض على الشاشة، مش تقريب ليه.

ابنيها خطوة بخطوة

  1. جهّز المشروع وثبّت المكتبات. النتيجة المتوقعة: مجلد فيه Express و Puppeteer، ومعاه نسخة Chromium متحمّلة.
    Bash
    mkdir invoice-pdf && cd invoice-pdf
    npm init -y
    npm install express puppeteer
    Puppeteer بيحمّل نسخته من Chromium أول مرة (حوالي 170 ميجابايت)، فالتثبيت الأول بياخد شوية وقت. ده طبيعي.
  2. اكتب قالب الفاتورة HTML. ملف يبني الـ HTML من بياناتك، باتجاه RTL ودالة تهرّب النص. ليه نهرّب النص؟ لو حطّيت اسم العميل في الـ HTML على طول من غير تهذيب، أي عميل اسمه فيه رموز زي علامة أصغر من ممكن يكسر فاتورتك أو يحقن كود. زي ما بتفلتر أي مدخل مستخدم، هنا بنحوّل الرموز الخاصة لصيغتها الآمنة.
    JavaScript
    // template.js
    const esc = (s) => String(s).replace(/[&<>]/g,
      c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
    
    function renderInvoice(data) {
      const rows = data.items.map(it =>
        `<tr><td>${esc(it.name)}</td><td>${it.qty}</td><td>${it.price.toFixed(2)}</td></tr>`
      ).join('');
      const total = data.items.reduce((s, it) => s + it.qty * it.price, 0);
      return `<!doctype html>
    <html lang="ar" dir="rtl"><head><meta charset="utf-8">
    <style>
      body { font-family: 'Cairo', sans-serif; padding: 40px; color:#1e293b; }
      h1 { color:#4338ca; }
      table { width:100%; border-collapse:collapse; margin-top:16px; }
      td, th { border:1px solid #e2e8f0; padding:8px; text-align:right; }
      .total { font-size:20px; font-weight:bold; margin-top:20px; }
    </style></head><body>
      <h1>فاتورة #${esc(data.id)}</h1>
      <p>العميل: ${esc(data.customer)}</p>
      <table>
        <tr><th>الصنف</th><th>الكمية</th><th>السعر</th></tr>
        ${rows}
      </table>
      <div class="total">الإجمالي: ${total.toFixed(2)} ج.م</div>
    </body></html>`;
    }
    module.exports = { renderInvoice };
    النتيجة المتوقعة: دالة بترجّع نص HTML كامل من أي كائن بيانات.
  3. شغّل متصفحًا واحدًا وأعد استخدامه. ده أهم سطر في الأداء. متفتحش متصفح جديد لكل طلب — افتح واحد وخليه عايش، وافتح صفحة جديدة بس لكل فاتورة.
    JavaScript
    // pdf.js
    const puppeteer = require('puppeteer');
    const { renderInvoice } = require('./template');
    
    let browserPromise;
    const getBrowser = () => (browserPromise ??= puppeteer.launch({
      headless: 'new',
      args: ['--no-sandbox'],
    }));
    
    async function invoiceToPdf(data) {
      const browser = await getBrowser();
      const page = await browser.newPage();
      try {
        await page.setContent(renderInvoice(data), { waitUntil: 'networkidle0' });
        return await page.pdf({
          format: 'A4',
          printBackground: true,
          margin: { top: '10mm', bottom: '10mm', left: '10mm', right: '10mm' },
        });
      } finally {
        await page.close(); // اقفل الصفحة، مش المتصفح
      }
    }
    module.exports = { invoiceToPdf };
    النتيجة المتوقعة: دالة بترجّع Buffer فيه بايتات الـ PDF.
  4. اربطها بمسار Express يبثّ الملف.
    JavaScript
    // server.js
    const express = require('express');
    const { invoiceToPdf } = require('./pdf');
    const app = express();
    
    // بيانات وهمية — بدّلها باستعلام قاعدة بياناتك
    const getInvoice = (id) => ({
      id,
      customer: 'شركة النور',
      items: [
        { name: 'استضافة سنوية', qty: 1, price: 1200 },
        { name: 'دومين', qty: 1, price: 50 },
      ],
    });
    
    app.get('/invoice/:id', async (req, res) => {
      try {
        const pdf = await invoiceToPdf(getInvoice(req.params.id));
        res.set({
          'Content-Type': 'application/pdf',
          'Content-Disposition': `inline; filename="invoice-${req.params.id}.pdf"`,
        });
        res.send(pdf);
      } catch (e) {
        console.error(e);
        res.status(500).send('failed to render pdf');
      }
    });
    
    app.listen(3000, () => console.log('on http://localhost:3000'));
    النتيجة المتوقعة: GET /invoice/42 بيرجّع ملف PDF جاهز.
  5. اضبط تنسيق الطباعة. المتصفح بيطبع بوسائط print مش screen. استخدم @page للتحكم في الحجم والهوامش، و-webkit-print-color-adjust عشان ألوان الخلفيات متضيعش في الملف.
    CSS
    @page { size: A4; margin: 0; }
    @media print {
      body { -webkit-print-color-adjust: exact; }
    }

التحقق من أنه يعمل

  1. شغّل node server.js.
  2. افتح http://localhost:3000/invoice/42 في المتصفح — المفروض تشوف الـ PDF جوّه الصفحة.
  3. من التيرمينال: curl -o test.pdf http://localhost:3000/invoice/42 ثم افتح test.pdf. لو فتح وفيه الجدول والعربي مظبوط في اتجاهه، تمام كله شغّال.

الأرقام والمقايضات

على سيرفر صغير (2 vCPU)، فتح متصفح جديد لكل طلب بيضيف حوالي 800 مللي ثانية إلى 1.5 ثانية لكل فاتورة. لما تعيد استخدام نفس المتصفح وتفتح صفحة جديدة بس، وقت توليد الفاتورة الواحدة بينزل لحوالي 120 إلى 300 مللي ثانية. يعني تحسّن 4 إلى 8 أضعاف من قرار واحد: إنك متقفلش المتصفح بعد كل طلب.

المقايضة صريحة: Puppeteer بيجرّ Chromium كامل معاه. بتكسب وفاء تنسيق 100% (Flexbox، خطوط ويب، اتجاه RTL عربي جاهز من غير مجهود)؛ وبتخسر حوالي 150 إلى 250 ميجابايت رام لكل نسخة متصفح، و170 ميجابايت مساحة قرص، وشوية تعقيد في إعداد الـ Docker (محتاج مكتبات النظام اللي Chromium بيعتمد عليها). الافتراض إن عندك ≥ 1 جيجابايت رام فاضية مخصّصة للخدمة دي.

متى لا تستخدم هذه الطريقة

  • حجم ضخم جدًا (آلاف الملفات في الثانية): متصفح واحد مش هيكفي. محتاج مجموعة متصفحات (browser pool) وطابور معالجة، أو خدمة توليد PDF مخصّصة.
  • إيصالات نصية بسيطة من غير تنسيق أو جداول: مكتبة خفيفة زي pdfkit أرخص بكثير في الرام ووقت التشغيل.
  • بيئات Serverless بميزانية بدء ضيّقة: تشغيل Chromium بيتقّل الـ cold start. استخدم بناء مخصّص زي @sparticuz/chromium، أو خدمة PDF مستضافة تنادي عليها بـ API.

الخطوة التالية

خُد ملف pdf.js فوق كما هو، بدّل دالة getInvoice باستعلام قاعدة بياناتك الحقيقي، وشغّل curl -o out.pdf http://localhost:3000/invoice/<رقم فاتورة حقيقي>. لو الـ PDF طلع بخط عربي مكسور أو حروف منفصلة، ضيف خط ويب عربي (زي Cairo) داخل الـ <head> وأعد التوليد — دي أشهر مشكلة وحلها سطر واحد.

المصادر

  • توثيق Puppeteer الرسمي — دالة page.pdf وخياراتها: pptr.dev/api/puppeteer.page.pdf
  • Chrome for Developers — وضع المتصفح بلا واجهة (Headless): developer.chrome.com/docs/chromium/headless
  • MDN — قاعدة @page وأنماط الطباعة: developer.mozilla.org/.../CSS/@page
  • Express — إرسال الردود (res.send و res.set): expressjs.com/en/api.html
  • @sparticuz/chromium — تشغيل Chromium في بيئات Serverless: github.com/Sparticuz/chromium

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة