Chrome بلا واجهة مستخدم رسومية: حلّ مشكلة العرض على مواقع JS من جهة الخادم

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

في سطرين من الرمز، يمكنك SSR أي صفحة والحصول على ترميزها النهائي.

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 بلا واجهة مستخدم رسومية

قد تفضّل استخدام Chrome بلا واجهة مستخدم رسومية في الحالات التالية:

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

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

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

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

الصفحات المعروضة مُسبقًا

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

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

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

على سبيل المثال، إذا تم إنشاء تطبيقك باستخدام Node.js، فإن Puppeteer هو طريقة سهلة للعمل مع 0.heads 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) وتشغيل الخادم باستخدام الإصدار 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)"
}

نتائج الأداء

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

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

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

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

منع إعادة شرب الماء

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

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

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

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

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 للعرض المسبق بشكل دوري

في تطبيق لوحة بيانات App Engine، تم إعداد معالج cron لإعادة عرض أهم الصفحات على الموقع بشكل دوري. يساعد ذلك الزائرين دائمًا في رؤية محتوى سريع وجديد دائمًا، كما يساعدهم في تجنب رؤية "تكلفة بدء التشغيل" للعرض المُسبَق الجديد. سيكون إنشاء نسخ متعددة من Chrome هدرًا في هذه الحالة. بدلاً من ذلك، أستخدم مثيل متصفح مشترك لعرض عدة صفحات في وقت واحد:

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 بلا واجهة مستخدم رسومية على الخادم، قد يكون من المفيد لمنطق الصفحة من جهة العميل أن يعرف ذلك. في تطبيقي، استخدمت عنصر الجذب هذا لـ "إيقاف" الأجزاء من صفحتي التي لا تؤدي أي دور في عرض ترميز المشاركات. على سبيل المثال، أوقفت الرمز الذي يؤدي إلى التحميل الكسول 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 بلا واجهة مستخدم رسومية للصفحة، وأخرى عند عرضها من خلال متصفّح المستخدم.

إذًا ما الحل في ذلك؟ استخدِم ميزة اعتراض الشبكة لإلغاء أي طلبات يحاول تحميل مكتبة "إحصاءات 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 التي كتبها الأشخاص والمخصّصة للموضوع. هناك عدد لا يحصى من الأنماط وtools والخدمات المتاحة للمساعدة في تطبيقات SSRing JS.

المتماثل / JavaScript عام

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

يعمل Chrome بلا واجهة مستخدم رسومية على تفعيل "JS isomorphic JS" بين الخادم والعميل. يعد خيارًا رائعًا إذا لم تعمل مكتبتك على الخادم (العقدة).

أدوات العرض المُسبَق

أنشأ منتدى العُقد الكثير من الأدوات للتعامل مع تطبيقات SSR JS. لا مفاجآت! شخصيًا، وجدت أن YMMV مع بعض هذه الأدوات، لذا عليك بالتأكيد أداء واجبك قبل الالتزام بإحدى هذه الأدوات. على سبيل المثال، بعض أدوات 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 يفعل ذلك. أواجه أيضًا مشكلات في الخدمة عبر الإنترنت التي تعرض بعض تطبيقاتي:

chromestatus معروضة في متصفح
عرض الموقع الإلكتروني في متصفّح
حالة chromestatus المعروضة من خلال العرض المُسبَق
الموقع الإلكتروني نفسه المعروض من خلال prerender.io