Chrome بلا واجهة مستخدم رسومية: حل بديل للمواقع الإلكترونية التي تستخدم JavaScript للعرض من جهة الخادم

Addy Osmani
Addy Osmani

تعرَّف على كيفية استخدام واجهات برمجة تطبيقات Puppeteer لإضافة إمكانات معالجة الرسومات من جهة الخادم (SSR) إلى خادم الويب Express. والميزة الأهم هي أنّه يتطلب تطبيقك إجراء تغييرات بسيطة جدًا على الرمز البرمجي. تُجري أداة Headless جميع عمليات التحميل المُهمّة.

يمكنك استخدام بضعة أسطر من التعليمات البرمجية لإعادة عرض أي صفحة والحصول على علاماتها النهائية.

import puppeteer from 'puppeteer';

async function ssr(url) {
  const browser = await puppeteer.launch({headless: true});
  const page = await browser.newPage();
  await page.goto(url, {waitUntil: 'networkidle0'});
  const html = await page.content(); // serialized HTML of page DOM.
  await browser.close();
  return html;
}

لماذا يجب استخدام "Chrome بلا واجهة مستخدم رسومية"؟

قد يهمّك استخدام Headless Chrome في الحالات التالية:

توفّر بعض الإطارات، مثل Preact، أدوات لمعالجة العرض من جهة الخادم. إذا كان إطار العمل يتضمّن حلّاً للعرض المُسبَق، استخدِمه بدلاً من استخدام Puppeteer وHeadless Chrome في عملك.

الزحف إلى الويب الحديث

في السابق، كانت برامج الزحف في محرّكات البحث ومنصّات المشاركة على وسائل التواصل الاجتماعي حتى المتصفّحات تعتمد حصريًا على ترميز HTML الثابت لفهرسة الويب وعرض المحتوى. لقد تطوّر الويب الحديث ليصبح مختلفًا تمامًا. إنّ التطبيقات المستندة إلى JavaScript موجودة لتبقى، ما يعني أنّه في العديد من الحالات، يمكن أن يكون المحتوى غير مرئي لأدوات الزحف.

يُعدّ Googlebot، وهو الزاحف الخاص بخدمة "بحث Google"، يعالج JavaScript مع الحرص على عدم خفض مستوى تجربة المستخدمين الذين يزورون الموقع الإلكتروني. هناك بعض الاختلافات والقيود التي يجب أخذها في الاعتبار عند تصميم صفحاتك وتطبيقاتك لتكون متوافقة مع آلية وصول برامج الزحف إلى المحتوى وعرضه.

صفحات العرض المُسبَق

تفهم جميع برامج الزحف لغة HTML. لضمان تمكّن برامج الزحف من فهرسة JavaScript، نحتاج إلى أداة:

  • معرفة كيفية تشغيل جميع أنواع JavaScript الحديثة وإنشاء محتوى HTML ثابت
  • تبقى محدّثة باستمرار مع إضافة ميزات جديدة إلى الويب.
  • يتم تشغيلها بدون أي تعديلات على الرمز البرمجي أو بتعديلات قليلة على الرمز البرمجي في تطبيقك.

هل هذا مناسب؟ هذه الأداة هي المتصفّح. لا يهتم Chrome بلا واجهة مستخدم رسومية بالمكتبة أو إطار العمل أو سلسلة الأدوات التي تستخدمها.

على سبيل المثال، إذا تم إنشاء تطبيقك باستخدام Node.js، يُعدّ Puppeteer طريقة سهلة للعمل مع Chrome بلا واجهة مستخدم رسومية.

ابدأ بصفحة ديناميكية تنشئ صفحات HTML باستخدام JavaScript:

public/index.html

<html>
<body>
  <div id="container">
    <!-- Populated by the JS below. -->
  </div>
</body>
<script>
function renderPosts(posts, container) {
  const html = posts.reduce((html, post) => {
    return `${html}
      <li class="post">
        <h2>${post.title}</h2>
        <div class="summary">${post.summary}</div>
        <p>${post.content}</p>
      </li>`;
  }, '');

  // CAREFUL: this assumes HTML is sanitized.
  container.innerHTML = `<ul id="posts">${html}</ul>`;
}

(async() => {
  const container = document.querySelector('#container');
  const posts = await fetch('/posts').then(resp => resp.json());
  renderPosts(posts, container);
})();
</script>
</html>

دالة SSR

بعد ذلك، استخدِم الدالة ssr() من القسم السابق وطوِّرها قليلاً:

ssr.mjs

import puppeteer from 'puppeteer';

// In-memory cache of rendered pages. Note: this will be cleared whenever the
// server process stops. If you need true persistence, use something like
// Google Cloud Storage (https://firebase.google.com/docs/storage/web/start).
const RENDER_CACHE = new Map();

async function ssr(url) {
  if (RENDER_CACHE.has(url)) {
    return {html: RENDER_CACHE.get(url), ttRenderMs: 0};
  }

  const start = Date.now();

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  try {
    // networkidle0 waits for the network to be idle (no requests for 500ms).
    // The page's JS has likely produced markup by this point, but wait longer
    // if your site lazy loads, etc.
    await page.goto(url, {waitUntil: 'networkidle0'});
    await page.waitForSelector('#posts'); // ensure #posts exists in the DOM.
  } catch (err) {
    console.error(err);
    throw new Error('page.goto/waitForSelector timed out.');
  }

  const html = await page.content(); // serialized HTML of page DOM.
  await browser.close();

  const ttRenderMs = Date.now() - start;
  console.info(`Headless rendered page in: ${ttRenderMs}ms`);

  RENDER_CACHE.set(url, html); // cache rendered page.

  return {html, ttRenderMs};
}

export {ssr as default};

في ما يلي التغييرات الرئيسية:

  • تمّت إضافة ميزة التخزين المؤقت. إنّ تخزين رمز HTML المعروض مؤقتًا هو أكبر ميزة لتسريع زمن الردّ. عند إعادة طلب الصفحة، يمكنك تجنُّب تشغيل Chrome بدون واجهة مستخدم تمامًا. سأناقش التحسينات الأخرى لاحقًا.
  • أضِف معالجة أساسية للأخطاء في حال انتهاء مهلة تحميل الصفحة.
  • أضِف طلبًا إلى page.waitForSelector('#posts'). يضمن ذلك توفّر المشاركات في نموذج DOM قبل تفريغ الصفحة التسلسلية.
  • أضِف معلومات علمية. سجِّل الوقت الذي يستغرقه العارض عديم الواجهة لعرض الصفحة، ثم عرض وقت المعالجة مع رمز HTML.
  • الصِق الرمز في وحدة باسم ssr.mjs.

مثال على خادم ويب

أخيرًا، إليك الخادم السريع الصغير الذي يجمع كل هذه العناصر معًا. يعرض المعالج الرئيسي عنوان URL http://localhost/index.html (الصفحة الرئيسية) مُسبَقًا ويعرض النتيجة كاستجابة. تظهر المشاركات للمستخدمين على الفور عند وصولهم إلى الصفحة لأنّ الترميز الثابت أصبح الآن جزءًا من الاستجابة.

server.mjs

import express from 'express';
import ssr from './ssr.mjs';

const app = express();

app.get('/', async (req, res, next) => {
  const {html, ttRenderMs} = await ssr(`${req.protocol}://${req.get('host')}/index.html`);
  // Add Server-Timing! See https://w3c.github.io/server-timing/.
  res.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"`);
  return res.status(200).send(html); // Serve prerendered page as response.
});

app.listen(8080, () => console.log('Server started. Press Ctrl+C to quit'));

لتنفيذ هذا المثال، ثبِّت الملحقات (npm i --save puppeteer express) وشغِّل الخادم باستخدام Node 8.5.0 والعلامة --experimental-modules:

في ما يلي مثال على الردّ الذي أرسله هذا الخادم:

<html>
<body>
  <div id="container">
    <ul id="posts">
      <li class="post">
        <h2>Title 1</h2>
        <div class="summary">Summary 1</div>
        <p>post content 1</p>
      </li>
      <li class="post">
        <h2>Title 2</h2>
        <div class="summary">Summary 2</div>
        <p>post content 2</p>
      </li>
      ...
    </ul>
  </div>
</body>
<script>
...
</script>
</html>

حالة استخدام مثالية لواجهة برمجة التطبيقات الجديدة Server-Timing API

تُرسِل واجهة برمجة التطبيقات Server-Timing ومقاييس أداء الخادم (مثل أوقات الطلب والاستجابة أو عمليات البحث في قاعدة بيانات) إلى المتصفّح. يمكن أن يستخدم رمز العميل هذه المعلومات لتتبُّع الأداء العام لتطبيق الويب.

من حالات الاستخدام المثالية لمقياس Server-Timing هي الإبلاغ عن الوقت الذي يستغرقه Chrome بلا واجهة مستخدم رسومية في معالجة الصفحة مسبقًا. لإجراء ذلك، ما عليك سوى إضافة العنوان Server-Timing إلى استجابة الخادم:

res.set('Server-Timing', `Prerender;dur=1000;desc="Headless render time (ms)"`);

على العميل، يمكن استخدام Performance API وPerformanceObserver للوصول إلى هذه المقاييس:

const entry = performance.getEntriesByType('navigation').find(
    e => e.name === location.href);
console.log(entry.serverTiming[0].toJSON());

{
  "name": "Prerender",
  "duration": 3808,
  "description": "Headless render time (ms)"
}

نتائج الأداء

تتضمن النتائج التالية معظم تحسينات الأداء التي تمت مناقشتها لاحقًا.

في أحد الأمثلة على التطبيقات، يستغرق Chromium بلا واجهة مستخدم رسومية حوالي ثانية واحدة لعرض الصفحة على الخادم. بعد وضع الصفحة في ذاكرة التخزين المؤقت، تُظهر أداة DevTools محاكاة بطيئة لشبكة الجيل الثالث FCP في 8.37 ثانية أسرع من الإصدار من جهة العميل.

سرعة عرض الصفحة (FP)First Contentful Paint (FCP)
التطبيق من جهة العميلالنقاط الرباعية 11 ثانية
إصدار SSR2.3 ثانية‫2.3 ثانية تقريبًا

هذه النتائج مبشرة. يشاهد المستخدمون المحتوى المهم بشكل أسرع بكثير لأنّه لم تعُد الصفحة المعروضة من جهة الخادم تعتمد على JavaScript لتحميل المشاركات وعرضها.

منع إعادة الترطيب

هل تذكر عندما قلت "لم نُجري أي تغييرات على الرمز في التطبيق من جهة العميل"؟ هذا كذب.

يتلقّى تطبيق Express طلبًا، ويستخدم Puppeteer لتحميل الصفحة في headless، ويعرض النتيجة كاستجابة. ولكن هناك مشكلة في هذا الإعداد.

يتم تنفيذ JavaScript نفسه الذي يتم تنفيذه في Chrome الذي لا يتضمّن واجهة مستخدم رسومية على الخادم مرة أخرى عندما يحمِّل متصفّح المستخدم الصفحة على الواجهة الأمامية. لدينا مكانان لإنشاء الترميز. #doublerender

لحلّ هذه المشكلة، أخبِر الصفحة بأنّ رمز HTML متوفّر فيها. ومن بين الحلول التي يمكن اتّخاذها أن يتحقق JavaScript في الصفحة مما إذا كان <ul id="posts"> مضمّنًا في DOM في وقت التحميل. إذا كان الأمر كذلك، يعني ذلك أنّه تمّ نقل بيانات الصفحة من خلال ميزة "الاستجابة السريعة للطلبات" ويمكنك تجنُّب إعادة إضافة المشاركات مرة أخرى. 👍

public/index.html

<html>
<body>
  <div id="container">
    <!-- Populated by JS (below) or by prerendering (server). Either way,
         #container gets populated with the posts markup:
      <ul id="posts">...</ul>
    -->
  </div>
</body>
<script>
...
(async() => {
  const container = document.querySelector('#container');

  // Posts markup is already in DOM if we're seeing a SSR'd.
  // Don't re-hydrate the posts here on the client.
  const PRE_RENDERED = container.querySelector('#posts');
  if (!PRE_RENDERED) {
    const posts = await fetch('/posts').then(resp => resp.json());
    renderPosts(posts, container);
  }
})();
</script>
</html>

تحسينات

بالإضافة إلى الاحتفاظ بذاكرة التخزين المؤقت للنتائج المعروضة، هناك الكثير من التحسينات المثيرة للاهتمام التي يمكننا إجراؤها على ssr(). وبعضها يحقّق أرباحًا سريعة، في حين أنّ بعضها الآخر قد يكون أكثر توفّرًا. قد تعتمد مزايا الأداء التي تلاحظها في نهاية المطاف على أنواع الصفحات التي تُعدّها مسبقًا وتعقيد التطبيق.

إيقاف الطلبات غير الضرورية

في الوقت الحالي، يتم تحميل الصفحة بالكامل (وجميع الموارد التي تطلبها) بشكل غير مشروط في Chrome بدون واجهة مستخدم. ومع ذلك، لا يهمّنا سوى مسألتَين:

  1. الترميز المعروض
  2. طلبات JavaScript التي أدّت إلى إنشاء هذا الترميز

إنّ طلبات الشبكة التي لا تُنشئ نموذج DOM هي طلبات غير مجدية. لا تساهم الموارد، مثل الصور والخطوط وأوراق الأنماط والوسائط، في إنشاء رمز HTML للصفحة. وهي تُضفي تنسيقًا على بنية الصفحة وتُكملها، ولكنّها لا تنشئها صراحةً. علينا أن نطلب من المتصفّح تجاهُل هذين المصدرَين. يقلل ذلك من عبء العمل في متصفّح Chrome بلا واجهة مستخدم رسومية، ويوفّر معدل نقل البيانات، ويُحسِّن في بعض الحالات سرعة التقديم المُسبَق للصفحات الأكبر حجمًا.

يتيح بروتوكول أدوات المطوّرين استخدام ميزة فعّالة تُعرف باسم اعتراض الشبكة ويمكن استخدامها لتعديل الطلبات قبل أن يُصدرها المتصفّح. تتيح Puppeteer اعتراض الشبكة من خلال تفعيل page.setRequestInterception(true) والاستماع إلى حدث request للصفحة. يتيح لنا ذلك إلغاء طلبات الحصول على موارد معيّنة والسماح للطلبات الأخرى بمواصلة المعالجة.

ssr.mjs

async function ssr(url) {
  ...
  const page = await browser.newPage();

  // 1. Intercept network requests.
  await page.setRequestInterception(true);

  page.on('request', req => {
    // 2. Ignore requests for resources that don't produce DOM
    // (images, stylesheets, media).
    const allowlist = ['document', 'script', 'xhr', 'fetch'];
    if (!allowlist.includes(req.resourceType())) {
      return req.abort();
    }

    // 3. Pass through all other requests.
    req.continue();
  });

  await page.goto(url, {waitUntil: 'networkidle0'});
  const html = await page.content(); // serialized HTML of page DOM.
  await browser.close();

  return {html};
}

تضمين الموارد المهمة

من الشائع استخدام أدوات إنشاء منفصلة (مثل gulp) لمعالجة تطبيق و تضمين ملفّات CSS وJavaScript المهمة في الصفحة أثناء مرحلة الإنشاء. ويمكن أن يؤدي ذلك إلى تسريع سرعة عرض أوّل محتوى مفيد على الصفحة لأنّ المتصفّح يُجري عددًا أقل من الطلبات أثناءتحميل الصفحة الأولي.

بدلاً من استخدام أداة إنشاء منفصلة، يمكنك استخدام المتصفّح كداة إنشاء. يمكننا استخدام Puppeteer للتلاعب بنموذج كائن المستند (DOM) للصفحة أو تضمين الأنماط أو JavaScript أو أيّ شيء آخر تريد تضمينه في الصفحة قبل عرضها مسبقًا.

يوضّح هذا المثال كيفية اعتراض الاستجابات لجداول الأنماط المحلية ودمج هذه الموارد في الصفحة كعلامات <style>:

ssr.mjs

import urlModule from 'url';
const URL = urlModule.URL;

async function ssr(url) {
  ...
  const stylesheetContents = {};

  // 1. Stash the responses of local stylesheets.
  page.on('response', async resp => {
    const responseUrl = resp.url();
    const sameOrigin = new URL(responseUrl).origin === new URL(url).origin;
    const isStylesheet = resp.request().resourceType() === 'stylesheet';
    if (sameOrigin && isStylesheet) {
      stylesheetContents[responseUrl] = await resp.text();
    }
  });

  // 2. Load page as normal, waiting for network requests to be idle.
  await page.goto(url, {waitUntil: 'networkidle0'});

  // 3. Inline the CSS.
  // Replace stylesheets in the page with their equivalent <style>.
  await page.$$eval('link[rel="stylesheet"]', (links, content) => {
    links.forEach(link => {
      const cssText = content[link.href];
      if (cssText) {
        const style = document.createElement('style');
        style.textContent = cssText;
        link.replaceWith(style);
      }
    });
  }, stylesheetContents);

  // 4. Get updated serialized HTML of page.
  const html = await page.content();
  await browser.close();

  return {html};
}

This code:

  1. Use a page.on('response') handler to listen for network responses.
  2. Stashes the responses of local stylesheets.
  3. Finds all <link rel="stylesheet"> in the DOM and replaces them with an equivalent <style>. See page.$$eval API docs. The style.textContent is set to the stylesheet response.

Auto-minify resources

Another trick you can do with network interception is to modify the responses returned by a request.

As an example, say you want to minify the CSS in your app but also want to keep the convenience having it unminified when developing. Assuming you've setup another tool to pre-minify styles.css, one can use Request.respond() to rewrite the response of styles.css to be the content of styles.min.css.

ssr.mjs

import fs from 'fs';

async function ssr(url) {
  ...

  // 1. Intercept network requests.
  await page.setRequestInterception(true);

  page.on('request', req => {
    // 2. If request is for styles.css, respond with the minified version.
    if (req.url().endsWith('styles.css')) {
      return req.respond({
        status: 200,
        contentType: 'text/css',
        body: fs.readFileSync('./public/styles.min.css', 'utf-8')
      });
    }
    ...

    req.continue();
  });
  ...

  const html = await page.content();
  await browser.close();

  return {html};
}

إعادة استخدام نسخة واحدة من Chrome في جميع عمليات التقديم

يؤدي تشغيل متصفّح جديد لكل عملية معالجة مسبقة إلى زيادة كبيرة في وقت الاستجابة. بدلاً من ذلك، يمكنك تشغيل نسخة واحدة وإعادة استخدامها لعرض صفحات متعددة.

يمكن لواجهة Puppeteer إعادة الربط بنسخة حالية من Chrome من خلال استدعاء puppeteer.connect() وإرسال عنوان URL الخاص بميزة تصحيح الأخطاء عن بُعد إلى النسخة. للحفاظ على مثيل متصفح يعمل لفترة طويلة، يمكننا نقل الرمز الذي يشغّل Chrome من دالة ssr() إلى خادم Express:

server.mjs

import express from 'express';
import puppeteer from 'puppeteer';
import ssr from './ssr.mjs';

let browserWSEndpoint = null;
const app = express();

app.get('/', async (req, res, next) => {
  if (!browserWSEndpoint) {
    const browser = await puppeteer.launch();
    browserWSEndpoint = await browser.wsEndpoint();
  }

  const url = `${req.protocol}://${req.get('host')}/index.html`;
  const {html} = await ssr(url, browserWSEndpoint);

  return res.status(200).send(html);
});

ssr.mjs

import puppeteer from 'puppeteer';

/**
 * @param {string} url URL to prerender.
 * @param {string} browserWSEndpoint Optional remote debugging URL. If
 *     provided, Puppeteer's reconnects to the browser instance. Otherwise,
 *     a new browser instance is launched.
 */
async function ssr(url, browserWSEndpoint) {
  ...
  console.info('Connecting to existing Chrome instance.');
  const browser = await puppeteer.connect({browserWSEndpoint});

  const page = await browser.newPage();
  ...
  await page.close(); // Close the page we opened here (not the browser).

  return {html};
}

مثال: وظيفة cron لعرض المحتوى مسبقًا بشكل دوري

لعرض عدد من الصفحات في آنٍ واحد، يمكنك استخدام مثيل مشترَك للمتصفّح.

import puppeteer from 'puppeteer';
import * as prerender from './ssr.mjs';
import urlModule from 'url';
const URL = urlModule.URL;

app.get('/cron/update_cache', async (req, res) => {
  if (!req.get('X-Appengine-Cron')) {
    return res.status(403).send('Sorry, cron handler can only be run as admin.');
  }

  const browser = await puppeteer.launch();
  const homepage = new URL(`${req.protocol}://${req.get('host')}`);

  // Re-render main page and a few pages back.
  prerender.clearCache();
  await prerender.ssr(homepage.href, await browser.wsEndpoint());
  await prerender.ssr(`${homepage}?year=2018`);
  await prerender.ssr(`${homepage}?year=2017`);
  await prerender.ssr(`${homepage}?year=2016`);
  await browser.close();

  res.status(200).send('Render cache updated!');
});

أضِف أيضًا عملية تصدير clearCache() إلى ssr.js:

...
function clearCache() {
  RENDER_CACHE.clear();
}

export {ssr, clearCache};

اعتبارات أخرى

أنشئ إشارة للصفحة: "يتم عرض الصفحة في وضع "بدون إطار"".

عندما يتم عرض صفحتك بواسطة Chrome بدون واجهة مستخدم على الخادم، قد يكون مفيدًا أن يعرف منطق الصفحة من جهة العميل ذلك. في تطبيقي، استخدمت هذا الخطاف "لتعطيل" أجزاء من صفحتي التي لا تساهم في عرض markup المشاركات. على سبيل المثال، أوقفت الرمز الذي يحمّل firebase-auth.js بشكلٍ بطيء. ما مِن مستخدم لتسجيل الدخول.

إنّ إضافة مَعلمة ?headless إلى عنوان URL لعرض الصفحة هي طريقة بسيطة لمنح الصفحة رابطًا:

ssr.mjs

import urlModule from 'url';
const URL = urlModule.URL;

async function ssr(url) {
  ...
  // Add ?headless to the URL so the page has a signal
  // it's being loaded by headless Chrome.
  const renderUrl = new URL(url);
  renderUrl.searchParams.set('headless', '');
  await page.goto(renderUrl, {waitUntil: 'networkidle0'});
  ...

  return {html};
}

في الصفحة، يمكننا البحث عن هذه المَعلمة:

public/index.html

<html>
<body>
  <div id="container">
    <!-- Populated by the JS below. -->
  </div>
</body>
<script>
...

(async() => {
  const params = new URL(location.href).searchParams;

  const RENDERING_IN_HEADLESS = params.has('headless');
  if (RENDERING_IN_HEADLESS) {
    // Being rendered by headless Chrome on the server.
    // e.g. shut off features, don't lazy load non-essential resources, etc.
  }

  const container = document.querySelector('#container');
  const posts = await fetch('/posts').then(resp => resp.json());
  renderPosts(posts, container);
})();
</script>
</html>

تجنُّب تضخيم مشاهدات الصفحة على ويب في "إحصاءات Google"

يجب الانتباه إذا كنت تستخدم "إحصاءات Google" على موقعك الإلكتروني. قد يؤدي العرض المُسبَق للصفحات إلى زيادة عدد مشاهدات الصفحة. وعلى وجه التحديد، ستظهر لك مرتَين عدد النتائج، نتيجة واحدة عندما يعرض متصفّح Chrome بدون واجهة المستخدم الصفحة ونتيجة أخرى عندما يعرضها متصفّح المستخدم.

ما هو الحلّ؟ استخدِم اعتراض الشبكة لإيقاف أي طلبات تحاول carregar مكتبة "إحصاءات Google".

page.on('request', req => {
  // Don't load Google Analytics lib requests so pageviews aren't 2x.
  const blockist = ['www.google-analytics.com', '/gtag/js', 'ga.js', 'analytics.js'];
  if (blocklist.find(regex => req.url().match(regex))) {
    return req.abort();
  }
  ...
  req.continue();
});

لا يتم تسجيل مرّات ظهور الصفحة مطلقًا إذا لم يتم تحميل الرمز مطلقًا. رائع 💥.

بدلاً من ذلك، يمكنك مواصلة تحميل مكتبات "إحصاءات Google" للحصول على إحصاءات عن عدد عمليات التقديم المُسبَق التي يُجريها خادمك.

الخاتمة

تسهّل مكتبة Puppeteer عرض الصفحات من جهة الخادم من خلال تشغيل Chrome بلا واجهة مستخدم رسومية، بصفته ملحقًا، على خادم الويب. إنّ "الميزة" المفضّلة لديّ في هذا النهج هي تحسين أداء التحميل وقابلية الفهرسة لتطبيقك بدون تغييرات كبيرة في الرموز البرمجية.

إذا كنت مهتمًا برؤية تطبيق يعمل يستخدم الأساليب الموضّحة هنا، يمكنك الاطّلاع على تطبيق devwebfeed.

الملحق

مناقشة المعلومات السابقة

من الصعب عرض التطبيقات من جهة العميل من جهة الخادم. ما مدى صعوبة ذلك؟ ما عليك سوى الاطّلاع على عدد حِزم npm التي كتبها المستخدمون والمخصّصة لهذا الموضوع. تتوفّر عدّة نماذج و أدوات و خدمات للمساعدة في استخدام تطبيقات JavaScript التي تعمل بالاستجابة السريعة.

Isomorphic / Universal JavaScript

يشير مفهوم Universal JavaScript إلى أنّ الرمز نفسه الذي يتم تشغيله على الخادم يتم تشغيله أيضًا على العميل (المتصفح). يمكنك مشاركة الرمز بين الخادم والعميل، ويشعر الجميع بالسعادة.

يتيح متصفّح Chrome الذي لا يتضمّن واجهة مستخدم رسومية استخدام "JavaScript المماثل" بين الخادم والعميل. هذا خيار جيد إذا كانت مكتبتك لا تعمل على الخادم (Node).

أدوات التقديم المُسبَق

أنشأ منتدى Node الكثير من الأدوات للتعامل مع تطبيقات SSR JS. ليس هناك مفاجآت. لقد تبيّن لي شخصيًا أنّه قد تختلف النتائج من شخص لآخر عند استخدام بعض هذه الأدوات، لذا عليك إجراء البحث اللازم قبل الالتزام باستخدام أيّ منها. على سبيل المثال، بعض أدوات SSR قديمة ولا تستخدم "Chrome بلا واجهة مستخدم رسومية" (أو أي متصفّح بلا واجهة مستخدم رسومية في هذا الشأن). بدلاً من ذلك، يستخدمون PhantomJS (المعروف أيضًا باسم Safari القديم)، ما يعني أنّه لن يتم عرض صفحاتك بشكل صحيح إذا كانت تستخدم ميزات أحدث.

ومن بين الاستثناءات البارزة العرض المُسبَق. تُعدّ ميزة "العرض المُسبَق" مثيرة للاهتمام لأنّها تستخدِم متصفّح Chrome بلا واجهة مستخدم رسومية، كما أنّها تأتي مع وسيط تدخُّل لإطار عمل Express:

const prerender = require('prerender');
const server = prerender();
server.use(prerender.removeScriptTags());
server.use(prerender.blockResources());
server.start();

تجدر الإشارة إلى أنّ ميزة "العرض المُسبَق" لا تتضمّن تفاصيل تنزيل Chrome و تثبيته على الأنظمة الأساسية المختلفة. في كثير من الأحيان، يكون من الصعب جدًا تنفيذ ذلك بشكل صحيح، وهذا هو أحد الأسباب التي تجعل Puppeteer ينفّذ ذلك نيابةً عنك.