Node.js و Express

العرض من جانب الخادم باستخدام Node.js

35 دقيقة الدرس 31 من 40

مقدمة إلى العرض من جانب الخادم

العرض من جانب الخادم (SSR) هو تقنية يتم فيها إنشاء محتوى HTML على الخادم قبل إرساله إلى العميل، بدلاً من عرض كل شيء في المتصفح باستخدام JavaScript. يوفر هذا النهج فوائد كبيرة للأداء وتحسين محركات البحث وتجربة المستخدم.

الفرق الرئيسي: في العرض من جانب العميل (CSR)، يتلقى المتصفح HTML بسيط وJavaScript يبني الصفحة. في SSR، يرسل الخادم HTML معروضًا بالكامل يظهر فورًا للمستخدمين ومحركات البحث.

لماذا نستخدم العرض من جانب الخادم؟

يوفر SSR عدة مزايا:

  • تحسين محركات البحث: يمكن لمحركات البحث الزحف وفهرسة محتوى HTML المعروض بالكامل بسهولة
  • تحميل أولي أسرع: يرى المستخدمون المحتوى فورًا دون انتظار تنفيذ JavaScript
  • أداء أفضل على الأجهزة البطيئة: معالجة أقل من جانب العميل
  • مشاركة وسائل التواصل الاجتماعي: علامات Meta وبيانات Open Graph متاحة فورًا
  • التحسين التدريجي: المحتوى متاح حتى إذا فشل JavaScript

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

SSR الأساسي مع Node.js

دعنا نطبق نظام SSR بسيط باستخدام Express ومحركات القوالب:

// app.js - SSR أساسي مع EJS
const express = require('express');
const app = express();

// تعيين محرك العرض
app.set('view engine', 'ejs');
app.set('views', './views');

// محاكاة جلب البيانات
async function getUserData(userId) {
  // في الإنتاج، سيكون هذا استعلام قاعدة بيانات
  return {
    id: userId,
    name: 'أحمد محمد',
    email: 'ahmad@example.com',
    posts: [
      { id: 1, title: 'أول مقال', content: 'مرحباً بالعالم' },
      { id: 2, title: 'مقال ثاني', content: 'تعلم SSR' }
    ]
  };
}

// مسار SSR
app.get('/user/:id', async (req, res) => {
  try {
    const userId = req.params.id;
    const userData = await getUserData(userId);

    // عرض HTML على الخادم
    res.render('user', {
      user: userData,
      pageTitle: \`الملف الشخصي - ${userData.name}\`,
      timestamp: new Date().toISOString()
    });
  } catch (error) {
    console.error('خطأ SSR:', error);
    res.status(500).send('خطأ في الخادم');
  }
});

app.listen(3000, () => {
  console.log('خادم SSR يعمل على المنفذ 3000');
});

إنشاء قالب EJS (views/user.ejs):

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= pageTitle %></title>
  <meta name="description" content="صفحة الملف الشخصي لـ <%= user.name %>">
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    .post { border: 1px solid #ddd; padding: 15px; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>الملف الشخصي للمستخدم</h1>

  <div class="user-info">
    <h2><%= user.name %></h2>
    <p>البريد الإلكتروني: <%= user.email %></p>
    <p>معرف المستخدم: <%= user.id %></p>
  </div>

  <h3>المقالات</h3>
  <% user.posts.forEach(post => { %>
    <div class="post">
      <h4><%= post.title %></h4>
      <p><%= post.content %></p>
    </div>
  <% }); %>

  <footer>
    <small>تم العرض في: <%= timestamp %></small>
  </footer>
</body>
</html>

عرض React من جانب الخادم

توفر React دعمًا مدمجًا لـ SSR من خلال واجهة برمجة التطبيقات ReactDOMServer:

// server.js - React SSR
const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const App = require('./App');

const app = express();

// تقديم الملفات الثابتة
app.use(express.static('public'));

// المكون للعرض
const UserProfile = ({ user }) => (
  React.createElement('div', null,
    React.createElement('h1', null, user.name),
    React.createElement('p', null, `البريد الإلكتروني: ${user.email}`),
    React.createElement('div', null,
      React.createElement('h2', null, 'المقالات'),
      user.posts.map(post =>
        React.createElement('div', { key: post.id, className: 'post' },
          React.createElement('h3', null, post.title),
          React.createElement('p', null, post.content)
        )
      )
    )
  )
);

app.get('/user/:id', async (req, res) => {
  const userData = await getUserData(req.params.id);

  // عرض مكون React إلى نص
  const html = ReactDOMServer.renderToString(
    React.createElement(UserProfile, { user: userData })
  );

  // إرسال HTML كامل
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>${userData.name} - الملف الشخصي</title>
      <link rel="stylesheet" href="/styles.css">
    </head>
    <body>
      <div id="root">${html}</div>
      <script src="/bundle.js"></script>
    </body>
    </html>
  `);
});

نصيحة احترافية: استخدم renderToStaticMarkup() بدلاً من renderToString() إذا كنت لا تحتاج إلى hydration من جانب العميل - ينتج HTML أصغر بدون سمات خاصة بـ React.

Hydration في React SSR

Hydration هي العملية التي "تربط" فيها React بـ HTML المعروض من الخادم، مما يجعله تفاعليًا:

// client.js - Hydration
import React from 'react';
import ReactDOM from 'react-dom';
import UserProfile from './UserProfile';

// الحصول على البيانات الأولية من الخادم
const initialData = window.__INITIAL_DATA__;

// Hydrate بدلاً من render
ReactDOM.hydrate(
  <UserProfile user={initialData} />,
  document.getElementById('root')
);

// من جانب الخادم: حقن البيانات في HTML
app.get('/user/:id', async (req, res) => {
  const userData = await getUserData(req.params.id);
  const html = ReactDOMServer.renderToString(
    <UserProfile user={userData} />
  );

  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>${userData.name}</title>
    </head>
    <body>
      <div id="root">${html}</div>
      <script>
        window.__INITIAL_DATA__ = ${JSON.stringify(userData).replace(/</g, '\\u003c')};
      </script>
      <script src="/bundle.js"></script>
    </body>
    </html>
  `);
});

الأمان: قم دائمًا بتنقية البيانات قبل حقنها في HTML. استبدل < بـ \u003c لمنع هجمات XSS عند تضمين JSON في علامات script.

مقدمة إلى Next.js

Next.js هو إطار عمل React يجعل SSR بسيطًا مع ميزات مدمجة:

// التثبيت
npm install next react react-dom

// سكريبتات package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

// pages/user/[id].js - صفحة Next.js مع SSR
export async function getServerSideProps(context) {
  const { id } = context.params;

  // جلب البيانات على الخادم
  const res = await fetch(`https://api.example.com/users/${id}`);
  const user = await res.json();

  return {
    props: { user } // يتم تمريره إلى المكون
  };
}

export default function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>البريد الإلكتروني: {user.email}</p>

      <h2>المقالات</h2>
      {user.posts.map(post => (
        <div key={post.id} className="post">
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
}

استراتيجيات العرض في Next.js

يدعم Next.js استراتيجيات عرض متعددة:

// 1. العرض من جانب الخادم (SSR) - عند كل طلب
export async function getServerSideProps(context) {
  const data = await fetchData();
  return { props: { data } };
}

// 2. توليد الموقع الثابت (SSG) - في وقت البناء
export async function getStaticProps() {
  const data = await fetchData();
  return {
    props: { data },
    revalidate: 60 // إعادة التوليد الثابت التدريجي
  };
}

// 3. ثابت مع مسارات ديناميكية
export async function getStaticPaths() {
  const users = await fetchUsers();
  const paths = users.map(user => ({
    params: { id: user.id.toString() }
  }));

  return {
    paths,
    fallback: 'blocking' // أو true أو false
  };
}

export async function getStaticProps({ params }) {
  const user = await fetchUser(params.id);
  return { props: { user } };
}

// 4. العرض من جانب العميل (CSR)
import { useEffect, useState } from 'react';

export default function Dashboard() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/dashboard')
      .then(res => res.json())
      .then(setData);
  }, []);

  if (!data) return <div>جاري التحميل...</div>;
  return <div>{data.content}</div>;
}

اختيار الاستراتيجية: استخدم SSR للمحتوى الشخصي، SSG للمحتوى الثابت، ISR للمحتوى الثابت المحدث بشكل متكرر، و CSR للبيانات الخاصة/الخاصة بالمستخدم.

تحسين الأداء في SSR

تحسين أداء SSR باستخدام هذه التقنيات:

// 1. بث SSR (React 18+)
import { renderToPipeableStream } from 'react-dom/server';

app.get('/user/:id', async (req, res) => {
  const userData = await getUserData(req.params.id);

  res.setHeader('Content-Type', 'text/html');

  const { pipe } = renderToPipeableStream(
    <UserProfile user={userData} />,
    {
      onShellReady() {
        res.statusCode = 200;
        pipe(res);
      },
      onError(err) {
        console.error(err);
        res.statusCode = 500;
      }
    }
  );
});

// 2. التخزين المؤقت على مستوى المكون
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 });

async function getCachedUser(userId) {
  const cacheKey = `user_${userId}`;

  // فحص التخزين المؤقت
  let user = cache.get(cacheKey);
  if (user) {
    console.log('إصابة التخزين المؤقت');
    return user;
  }

  // جلب وتخزين مؤقت
  user = await fetchUserFromDB(userId);
  cache.set(cacheKey, user);
  return user;
}

// 3. Hydration انتقائي (React 18)
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header /> {/* يتم hydrate فورًا */}

      <Suspense fallback={<Spinner />}>
        <HeavyComponent /> {/* يتم hydrate عندما يكون جاهزًا */}
      </Suspense>

      <Footer /> {/* يتم hydrate قبل HeavyComponent */}
    </div>
  );
}

SSR مع جلب البيانات

التعامل مع جلب البيانات بكفاءة في SSR:

// جلب البيانات المتوازي
export async function getServerSideProps({ params }) {
  const [user, posts, comments] = await Promise.all([
    fetchUser(params.id),
    fetchUserPosts(params.id),
    fetchUserComments(params.id)
  ]);

  return {
    props: { user, posts, comments }
  };
}

// معالجة الأخطاء
export async function getServerSideProps({ params }) {
  try {
    const user = await fetchUser(params.id);

    if (!user) {
      return { notFound: true };
    }

    return { props: { user } };
  } catch (error) {
    console.error('خطأ SSR:', error);
    return {
      props: { error: 'فشل تحميل المستخدم' }
    };
  }
}

// إعادة التوجيه
export async function getServerSideProps({ req }) {
  const session = await getSession(req);

  if (!session) {
    return {
      redirect: {
        destination: '/login',
        permanent: false
      }
    };
  }

  return { props: { session } };
}

تصحيح الأخطاء ومراقبة SSR

تصحيح ومراقبة تطبيقات SSR:

// مراقبة الأداء
const perfMonitor = (componentName) => {
  return async (req, res, next) => {
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(`${componentName} SSR: ${duration}ms`);

      if (duration > 1000) {
        console.warn(`تم اكتشاف SSR بطيء لـ ${componentName}`);
      }
    });

    next();
  };
};

app.use('/user/:id', perfMonitor('UserProfile'));

// حدود الخطأ لـ SSR
class SSRErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  render() {
    if (this.state?.hasError) {
      return (
        <div>
          <h1>حدث خطأ ما</h1>
          <p>{this.state.error.message}</p>
        </div>
      );
    }

    return this.props.children;
  }
}

// الاستخدام في SSR
const html = ReactDOMServer.renderToString(
  <SSRErrorBoundary>
    <App />
  </SSRErrorBoundary>
);

تمرين: بناء مدونة SSR

أنشئ مدونة معروضة من جانب الخادم بالميزات التالية:

  • صفحة رئيسية تسرد جميع منشورات المدونة (SSR مع التخزين المؤقت)
  • صفحات المنشورات الفردية (SSR مع hydration)
  • نظام التعليقات (من جانب العميل فقط)
  • علامات SEO meta لكل صفحة
  • مراقبة الأداء

نفذ باستخدام إما React SSR الخالص أو Next.js. قم بتضمين معالجة الأخطاء وحالات التحميل.