واجهة برمجة تطبيقات مراحل نشاط الصفحة

توافق المتصفّح

  • Chrome: 68
  • ‫Edge: 79
  • Firefox: غير متوافق
  • Safari: غير متوافق

قد تعلّق المتصفحات الحديثة في بعض الأحيان الصفحات أو تتجاهلها تمامًا عند محدودية موارد النظام. في المستقبل، تريد المتصفّحات تنفيذ ذلك بشكل استباقي، حتى تستهلك طاقة وذاكرة أقل. توفّر Page Lifecycle API عناصر ربط دورة الحياة لكي تتمكّن صفحاتك من التعامل بأمان مع هذه التدخلات في المتصفّح بدون التأثير في تجربة المستخدم. اطّلِع على واجهة برمجة التطبيقات لتحديد ما إذا كان عليك تنفيذ هذه الميزات في تطبيقك.

الخلفية

تمثل دورة حياة التطبيق إحدى الطرق الرئيسية التي تدير بها أنظمة التشغيل الحديثة مواردها. على أجهزة Android وiOS وأحدث إصدارات Windows، يمكن للنظام التشغيل بدء التطبيقات و stopped في أي وقت. ويسمح ذلك لهذه المنصات بتبسيط الموارد وإعادة تخصيصها حيث تعود بالفائدة على المستخدم على أفضل وجه.

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

على الرغم من أنّ منصة الويب كانت تتضمّن منذ فترة طويلة أحداثًا مرتبطة بحالات دورة النشاط ، مثل load، unload، visibilitychange ، إلا أنّ هذه الأحداث لا تسمح للمطوّرين سوى بالاستجابة لتغييرات حالة دورة النشاط التي يبدأها المستخدم. لكي تعمل الويب بطريقة موثوقة على الأجهزة ذات الطاقة المنخفضة (وتكون أكثر حرصًا على الموارد بشكل عام على جميع المنصات)، تحتاج المتصفّحات إلى طريقة لاسترداد موارد النظام وإعادة تخصيصها بشكل استباقي.

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

تكمن المشكلة في أنّه لا تتوفّر للمطوّرين طريقة للاستعداد لهذه الأنواع من التدخلات التي يبدأها النظام أو حتى معرفة أنّها تحدث. وهذا يعني أنّه يجب أن تكون المتصفّحات حذرة في التعامل مع هذه العناصر، وإلا قد يؤدي ذلك إلى تعطُّل صفحات الويب.

تحاول Page Lifecycle API حلّ هذه المشكلة من خلال:

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

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

ستتناول بقية هذه المشاركة ميزات "رحلة الصفحة" الجديدة، وستستكشف كيفية ارتباطها بجميع حالات منصّة الويب والأحداث الحالية. وسيقدّم أيضًا اقتراحات وأفضل الممارسات بشأن أنواع العمل الذي يجب أن ينفّذه المطوّرون (وما لا يجب أن ينفّذوه) في كل حالة.

نظرة عامة على حالات وأحداث دورة حياة الصفحة

جميع حالات مسار المستخدِم للصفحة منفصلة ومتعارضة مع بعضها، ما يعني أنّ الصفحة لا يمكن أن تكون في أكثر من حالة واحدة في الوقت نفسه. ويمكن رصد معظم التغييرات في حالة دورة حياة الصفحة بشكل عام من خلال أحداث DOM (اطّلِع على اقتراحات المطوّرين لكل حالة للاطّلاع على الاستثناءات).

ربما تكون الطريقة الأسهل لشرح حالات مسار المستخدِم للصفحة، بالإضافة إلى الأحداث التي تشير إلى الانتقالات بينها، هي من خلال مخطّط بياني:

تمثيل مرئي للحالة ومسار الحدث الموضّحَين في هذا المستند
حالة Page Lifecycle API ومسار الأحداث

الولايات

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

الحالة الوصف
نشطة

تكون الصفحة في الحالة نشطة إذا كانت مرئية وكانت محطّ تركيز الإدخال.

الحالات السابقة المحتمَلة:
سلبي (من خلال الحدث focus)
مجمّد (من خلال الحدث resume، ثم الحدث pageshow)

الحالات التالية المحتمَلة:
passive (من خلال الحدث blur)

متابعة غير نشطة

تكون الصفحة في الحالة سلبية إذا كانت مرئية ولم يكن عليها تركيز الإدخال.

الحالات السابقة المحتمَلة:
نشط (من خلال الحدث blur)
مخفي (من خلال الحدث visibilitychange)
مجمّد (من خلال الحدث resume، ثم الحدث pageshow)

الحالات التالية المحتمَلة:
نشط (من خلال الحدث focus)
مخفي (من خلال الحدث visibilitychange)

مخفية

تكون الصفحة في الحالة مخفية إذا لم تكن مرئية (وإذا لم يتم تجميدها أو تجاهلها أو إنهائها).

الحالات السابقة المحتمَلة:
سلبي (من خلال الحدث visibilitychange)
مجمّد (من خلال الحدث resume، ثم الحدث pageshow)

الحالات التالية المحتمَلة:
passive (من خلال الحدث visibilitychange)
frozen (من خلال الحدث freeze)
discarded (لم يتم تشغيل أي أحداث)
terminated (لم يتم تشغيل أي أحداث)

مُعلَّق

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

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

الحالات السابقة المحتمَلة:
مخفي (من خلال الحدث freeze)

الحالات التالية المحتمَلة:
نشط (من خلال الحدث resume، ثم الحدث pageshow)
غير نشط (من خلال الحدث resume، ثم الحدث pageshow)
مخفي (من خلال الحدث resume)
محذوف (لم يتم تشغيل أي أحداث)

تم إنهاء

تكون الصفحة في حالة متوقفة بعد أن يبدأ المتصفّح في إلغاء تحميلها وتنظيفها من الذاكرة. لا يمكن بدء أي مهام جديدة في هذه الحالة، وقد يتم إيقاف المهام قيد التنفيذ إذا استغرقت وقتًا طويلاً جدًا.

الحالات السابقة المحتمَلة:
مخفي (من خلال الحدث pagehide)

الحالات التالية المحتمَلة:
NONE

تم تجاهلها

تكون الصفحة في الحالة تم تجاهلها عندما يُلغي المتصفّح تحميلها للحفاظ على الموارد. لا يمكن تنفيذ أي مهام أو عمليات استدعاء للأحداث أو رموز JavaScript من أي نوع في هذه الحالة، لأنّ عمليات الحذف تتم عادةً في ظل قيود على الموارد، حيث يكون بدء عمليات جديدة مستحيلاً.

في الحالة مُهمَلة، تكون علامة التبويب نفسها (بما في ذلك عنوان علامة التبويب ورمز الشارة) مرئية للمستخدم عادةً على الرغم من اختفاء الصفحة.

الحالات السابقة المحتمَلة:
مخفي (لم يتم تشغيل أي أحداث)
مجمّد (لم يتم تشغيل أي أحداث)

الحالات التالية المحتمَلة:
NONE

الفعاليات

تُرسِل المتصفّحات الكثير من الأحداث، ولكن نسبة صغيرة فقط منها تشير إلى احتمال حدوث تغيير في حالة مسار الصفحة. يوضّح الجدول التالي جميع الأحداث التي تتعلّق بالرحلة ويسرد الحالات التي يمكن أن تنتقل إليها منها.

الاسم التفاصيل
focus

تم التركيز على عنصر DOM.

ملاحظة: لا يشير حدث focus بالضرورة إلى تغيير في الحالة. ولا يشير إلى تغيير الحالة إلا إذا لم تكن الصفحة تركّز على الإدخال في السابق.

الحالات السابقة المحتمَلة:
passive

الحالات الحالية المحتمَلة:
نشط

blur

فقد عنصر DOM التركيز.

ملاحظة: لا يشير حدث blur بالضرورة إلى تغيير في الحالة. ولا يشير هذا الحدث إلى تغيير الحالة إلا إذا لم تعُد الصفحة تركّز على الإدخال (أي أنّ الصفحة لم تبدّل التركيز من عنصر إلى آخر).

الحالات السابقة المحتمَلة:
نشط

الحالات الحالية المحتمَلة:
passive

visibilitychange

تم تغيير قيمة visibilityState للمستند. ويمكن أن يحدث ذلك عندما ينتقل المستخدِم إلى صفحة جديدة أو يبدّل علامات التبويب أو يغلق علامة تبويب أو يُصغّر المتصفّح أو يغلقه أو يبدّل التطبيقات على أنظمة التشغيل الجوّالة.

الحالات السابقة المحتمَلة:
passive
hidden

الحالات الحالية المحتمَلة:
سلبي
مخفي

freeze *

تم تجميد الصفحة للتو. لن يتم بدء أي مهمة يمكن تجميدها في قوائم انتظار مهام الصفحة.

الحالات السابقة المحتمَلة:
مخفي

الحالات الحالية المحتمَلة:
مجمّد

resume *

استأنف المتصفّح عرض صفحة مجمّدة.

الحالات السابقة المحتمَلة:
مجمّد

الحالات الحالية المحتمَلة:
نشط (إذا تبعه الحدث pageshow)
غير نشط (إذا تبعه الحدث pageshow)
مخفي

pageshow

يتم الانتقال إلى إدخال في سجلّ الجلسات.

يمكن أن يكون هذا الإجراء عبارة عن تحميل صفحة جديدة تمامًا أو صفحة تم أخذها من ذاكرة التخزين المؤقت للصفحات. إذا تم أخذ الصفحة من ذاكرة التخزين المؤقت للصفحات، تكون قيمة سمة persisted للحدث هي true، وإلا ستكون false.

الحالات السابقة المحتمَلة:
مجمّد (كان من المفترض أيضًا أن يتم تشغيل حدث resume )

الحالات الحالية المحتمَلة:
نشط
سلبي
مخفي

pagehide

يتمّ التنقّل من إدخال في سجلّ الجلسات.

إذا كان المستخدِم ينتقل إلى صفحة أخرى وكان المتصفّح قادرًا على إضافة الصفحة الحالية إلى ذاكرة التخزين المؤقت للرجوع/الانتقال إلى الأماملإعادة استخدامها لاحقًا، تكون قيمة السمة persisted للحدث هي true. عندما يكون true، تدخل الصفحة في حالة مجمّدة، وإلا ستدخل في حالة متوقفة.

الحالات السابقة المحتمَلة:
مخفي

الحالات الحالية المحتمَلة:
مجمّد (event.persisted صحيح، يليه الحدث freeze)
متوقف (event.persisted خطأ، يتبعه الحدث unload)

beforeunload

سيتم قريبًا إلغاء تحميل النافذة والمستند وموارده. سيظل المستند مرئيًا وسيظل بإمكانك إلغاء الحدث في هذه المرحلة.

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

الحالات السابقة المحتمَلة:
مخفي

الحالات الحالية المحتمَلة:
انتهت

unload

يتمّ تفريغ الصفحة.

تحذير: لا يُنصح أبدًا باستخدام الحدث unload لأنّه غير موثوق به ويمكن أن يضرّ بالأداء في بعض الحالات. اطّلِع على قسم واجهات برمجة التطبيقات القديمة للاطّلاع على مزيد من التفاصيل.

الحالات السابقة المحتمَلة:
مخفي

الحالات الحالية المحتمَلة:
تم إنهاء

* يشير إلى حدث جديد محدّد من خلال واجهة برمجة التطبيقات Page Lifecycle API

الميزات الجديدة التي تمت إضافتها في الإصدار 68 من Chrome

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

في الإصدار 68 من Chrome، يمكن للمطوّرين الآن رصد حالات تجميد علامة تبويب مخفية وتفعيلها مجددًا من خلال الاستماع إلى حدثَي freeze وresume في document.

document.addEventListener('freeze', (event) => {
  // The page is now frozen.
});

document.addEventListener('resume', (event) => {
  // The page has been unfrozen.
});

اعتبارًا من الإصدار 68 من Chrome، يتضمّن عنصر document الآن سمة wasDiscarded في متصفّح Chrome للكمبيوتر المكتبي (يتم تتبُّع مدى توافق Android في هذه المشكلة). لتحديد ما إذا تم تجاهل صفحة أثناء الانتقال إلى علامة تبويب مخفية، يمكنك فحص قيمة هذه السمة في وقت تحميل الصفحة (ملاحظة: يجب إعادة تحميل الصفحات المرفوضة لاستخدامها مرة أخرى).

if (document.wasDiscarded) {
  // Page was previously discarded by the browser while in a hidden tab.
}

للحصول على نصائح حول الإجراءات المهمة التي يجب اتّخاذها في حدثَي freeze وresume ، بالإضافة إلى كيفية التعامل مع الصفحات التي يتم تجاهلها والاستعداد لذلك، يمكنك الاطّلاع على اقتراحات المطوّرين لكل حالة.

تقدّم الأقسام القليلة التالية نظرة عامة على كيفية ملاءمة هذه الميزات الجديدة لحالات منصّة الويب والأحداث الحالية.

كيفية مراقبة حالات مسار المستخدِم على الصفحة في الرمز البرمجي

في الحالات نشط وسلبي ومخفي ، من الممكن تنفيذ رمز JavaScript يحدّد الحالة الراهنة لمسار حياة الصفحة من واجهات برمجة تطبيقات منصة الويب الحالية.

const getState = () => {
  if (document.visibilityState === 'hidden') {
    return 'hidden';
  }
  if (document.hasFocus()) {
    return 'active';
  }
  return 'passive';
};

على صعيدٍ آخر، لا يمكن رصد حالتَي توقُّف وإنهاء إلا في أداة معالجة الحدث المعنيّة (freeze وpagehide) أثناء تغيُّر الحالة.

كيفية رصد تغييرات الحالة

استنادًا إلى دالة getState() المحدّدة سابقًا، يمكنك مراقبة جميع التغييرات في حالة Page Lifecycle باستخدام الرمز البرمجي التالي.

// Stores the initial state using the `getState()` function (defined above).
let state = getState();

// Accepts a next state and, if there's been a state change, logs the
// change to the console. It also updates the `state` value defined above.
const logStateChange = (nextState) => {
  const prevState = state;
  if (nextState !== prevState) {
    console.log(`State change: ${prevState} >>> ${nextState}`);
    state = nextState;
  }
};

// Options used for all event listeners.
const opts = {capture: true};

// These lifecycle events can all use the same listener to observe state
// changes (they call the `getState()` function to determine the next state).
['pageshow', 'focus', 'blur', 'visibilitychange', 'resume'].forEach((type) => {
  window.addEventListener(type, () => logStateChange(getState()), opts);
});

// The next two listeners, on the other hand, can determine the next
// state from the event itself.
window.addEventListener('freeze', () => {
  // In the freeze event, the next state is always frozen.
  logStateChange('frozen');
}, opts);

window.addEventListener('pagehide', (event) => {
  // If the event's persisted property is `true` the page is about
  // to enter the back/forward cache, which is also in the frozen state.
  // If the event's persisted property is not `true` the page is
  // about to be unloaded.
  logStateChange(event.persisted ? 'frozen' : 'terminated');
}, opts);

تُجري هذه التعليمات البرمجية ثلاثة إجراءات:

  • تُستخدَم لضبط الحالة الأولية باستخدام الدالة getState().
  • تحدِّد دالة تقبل حالة تالية، وفي حال حدوث تغيير، تسجِّل تغييرات الحالة في وحدة التحكّم.
  • تُضيف معالجة الأحداث التقاط جميع أدوات معالجة أحداث رحلة المستخدِم اللازمة، والتي بدورها تستدعي logStateChange()، مع تمرير الحالة التالية.

يُرجى العِلم أنّه تتم إضافة جميع أدوات معالجة الأحداث إلى window وجميعها تمر بحالة {capture: true}. وهناك بضعة أسباب لذلك:

  • لا تتضمّن جميع أحداث مسار المستخدِم على الصفحة الهدف نفسه. يتم تشغيل pagehide و pageshow على window، ويتم تشغيل visibilitychange وfreeze و resume على document، ويتم تشغيل focus وblur على عناصر DOM الخاصة بهما.
  • لا يتم تصعيد معظم هذه الأحداث، ما يعني أنّه من المستحيل إضافة أدوات معالجة أحداث لا تُسجّل الأحداث إلى عنصر سلف مشترك ومراقبة كل الأحداث.
  • يتم تنفيذ مرحلة الالتقاط قبل مرحلة الاستهداف أو مرحلة الفقاعة، لذا فإنّ إضافة المستمعِين هناك يساعد في ضمان تنفيذها قبل أن تلغيها رموز أخرى.

اقتراحات المطوّرين لكل حالة

بصفتك مطوّرًا، من المهم فهم حالات دورة حياة الصفحة ومعرفة كيفية رصدها في الرمز البرمجي لأنّ نوع العمل الذي يجب (والذي يجب عدم) تنفيذه يعتمد إلى حد كبير على الحالة التي تكون فيها صفحتك.

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

الحالة اقتراحات للمطوّرين
Active

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

يجب إزالة الأولوية من أي عمل غير واجهة المستخدم قد يؤدي إلى حظر السلسلة الرئيسية وتحديد وقت الخمول له أو نقله إلى عامل ويب.

Passive

في الحالة السلبية، لا يتفاعل المستخدم مع الصفحة، ولكن يظل بإمكانه رؤيتها. وهذا يعني أنّه يجب أن تظل تعديلات واجهة المستخدم والرسومات المتحركة سلسة، ولكن توقيت حدوث هذه التعديلات أقل أهمية.

عندما تتغيّر الحالة من نشط إلى غير نشط، يكون ذلك هو الوقت المناسب للحفاظ على حالة التطبيق غير المحفوظة.

Hidden

عندما تتغيّر الحالة من سلبية إلى مخفية، من المحتمل ألا يتفاعل المستخدم معها مرة أخرى إلى أن تتم إعادة تحميلها.

غالبًا ما يكون الانتقال إلى الحالة مخفي هو أيضًا آخر تغيير في الحالة يمكن للمطوّرين رصده بشكل موثوق (ينطبق ذلك بشكل خاص على الأجهزة الجوّالة، لأنّه يمكن للمستخدمين إغلاق علامات التبويب أو تطبيق المتصفّح نفسه، ولا يتم بدء أحداث beforeunload وpagehide وunload في هذه الحالات).

وهذا يعني أنّه عليك التعامل مع الحالة مخفي على أنّها نهاية محتمله لجلسة المستخدم. بعبارة أخرى، يجب الاحتفاظ بأي حالة تطبيق لم يتم حفظها وإرسال أي بيانات تحليلية لم يتم إرسالها.

يجب أيضًا إيقاف إجراء تعديلات على واجهة المستخدم (لأنّ المستخدم لن يراها)، ويجب إيقاف أي مهام لا يريد المستخدم تنفيذها في الخلفية.

Frozen

في الحالة مجمّدة، يتم تعليق المهام القابلة للتجميد في قوائم المهام إلى أن يتم إلغاء تجميد الصفحة، وهو ما قد لا يحدث أبدًا (مثلاً إذا تم تجاهل الصفحة).

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

وعلى وجه الخصوص، من المهم أن:

  • إغلاق كل اتصالات IndexedDB المفتوحة
  • أغلِق اتصالات BroadcastChannel المفتوحة.
  • أغلِق اتصالات WebRTC النشطة.
  • أوقِف أي عمليات استطلاع للشبكة أو أغلِق أي اتصالات مفتوحة عبر Web Socket.
  • أزِل أي قفل ويب تم تعليقه.

يجب أيضًا الاحتفاظ بأي حالة عرض ديناميكية (مثل موضع التمرير في عرض قائمة لا نهائية) في sessionStorage (أو IndexedDB من خلال commit()) التي تريد استعادتها في حال تم تجاهل الصفحة وإعادة تحميلها لاحقًا.

إذا عادت الصفحة من الحالة مجمّدة إلى الحالة مخفية، يمكنك إعادة فتح أي اتصالات مغلقة أو إعادة بدء أي عمليات استطلاع stopped عند تجميد الصفحة في البداية.

Terminated

بشكل عام، ليس عليك اتّخاذ أي إجراء عند انتقال صفحة إلى الحالة انتهت.

بما أنّ الصفحات التي يتمّ إلغاء تحميلها نتيجةً لفعل المستخدِم تمرّ دائمًا بالحالة مخفية قبل الدخول إلى الحالة مُنهاة، فإنّ الحالة مخفية هي الحالة التي يجب فيها تنفيذ منطق إنهاء الجلسة (مثل الحفاظ على حالة التطبيق وإعداد التقارير في "إحصاءات Google").

بالإضافة إلى ذلك (كما هو موضّح في الاقتراحات المتعلّقة بالحالة مخفي)، من المهم جدًا أن يدرك المطوّرون أنّه لا يمكن رصد الانتقال إلى الحالة منتهي بشكل موثوق في العديد من الحالات (خاصةً على الأجهزة الجوّالة)، لذا من المحتمل أن يفقد المطوّرون الذين يعتمدون على أحداث الإنهاء (مثل beforeunload pagehide وunload) البيانات.

Discarded

لا يمكن للمطوّرين ملاحظة الحالة مُهمَلة في أثناء تجاهل الصفحة. ويعود السبب في ذلك إلى أنّه يتم عادةً تجاهل الصفحات بسبب قيود الموارد، ولا يمكن ببساطة في معظم الحالات إلغاء تجميد صفحة للسماح بتشغيل النص البرمجي استجابةً لحدث تجاهل.

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

مرة أخرى، بما أنّ موثوقية أحداث دورة الحياة وترتيبها لا يتم تنفيذهما باستمرار في جميع المتصفّحات، فإنّ أسهل طريقة لاتّباع النصيحة الواردة في الجدول هي استخدام PageLifecycle.js.

واجهات برمجة التطبيقات القديمة لمراحل النشاط التي يجب تجنُّبها

يجب تجنُّب الأحداث التالية كلما أمكن ذلك.

حدث Unload

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

لهذا السبب، من الأفضل دائمًا الاعتماد على حدث visibilitychange لتحديد وقت انتهاء جلسة، والنظر في الحالة المخفية على أنّها آخر وقت موثوق به لحفظ بيانات التطبيق والمستخدم.

بالإضافة إلى ذلك، يمكن أن يؤدي مجرد توفّر معالِج حدث unload مسجَّل (من خلال onunload أو addEventListener()) إلى منع المتصفّحات من وضع الصفحات في التخزين المؤقت للصفحات لتحميل الصفحات بشكلٍ أسرع للرجوع والتقديم.

في جميع المتصفّحات الحديثة، ننصحك باستخدام الحدث pagehide دائمًا لرصد عمليات إزالة التحميل المحتمَلة للصفحات (المعروفة أيضًا باسم الحالة terminated) بدلاً من الحدث unload. إذا كنت بحاجة إلى إتاحة الإصدار 10 من Internet Explorer والإصدارات الأقدم، عليك رصد حدث pagehide واستخدام unload فقط إذا كان المتصفّح لا يتيح pagehide:

const terminationEvent = 'onpagehide' in self ? 'pagehide' : 'unload';

window.addEventListener(terminationEvent, (event) => {
  // Note: if the browser is able to cache the page, `event.persisted`
  // is `true`, and the state is frozen rather than terminated.
});

حدث beforeunload

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

يتمثل أحد الاختلافات بين beforeunload وunload في أنّ هناك استخدامات مشروعة لـ beforeunload. على سبيل المثال، عندما تريد تحذير المستخدم بأنّ لديه تغييرات غير محفوظة ستفقدها إذا واصل تفريغ الصفحة.

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

بعبارة أخرى، لا تفعل ذلك (لأنّه يضيف مستمعًا beforeunload بشكل غير مشروط):

addEventListener('beforeunload', (event) => {
  // A function that returns `true` if the page has unsaved changes.
  if (pageHasUnsavedChanges()) {
    event.preventDefault();

    // Legacy support for older browsers.
    return (event.returnValue = true);
  }
});

بدلاً من ذلك، يمكنك إجراء ما يلي (بما أنّه لا تتم إضافة مستمع beforeunload إلا عند الحاجة إليه، وإزالته عند عدم الحاجة إليه):

const beforeUnloadListener = (event) => {
  event.preventDefault();
  
  // Legacy support for older browsers.
  return (event.returnValue = true);
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  removeEventListener('beforeunload', beforeUnloadListener);
});

الأسئلة الشائعة

لماذا لا تتوفّر حالة "جارٍ التحميل"؟

تحدِّد واجهة برمجة التطبيقات Page Lifecycle API الحالات على أنّها منفصلة ومتعارضة مع بعضها. بما أنّه يمكن تحميل الصفحة في الحالة النشطة أو السلبية أو المخفية، وبما أنّه يمكن أن تتغيّر حالاتها أو حتى يتم إنهاء تحميلها قبل اكتمالها، فإنه ليس من المنطقي أن يكون هناك حالة تحميل منفصلة ضمن هذا المنهج.

تؤدي صفحتي مهمة مهمة عندما تكون مخفية، كيف يمكنني منع تجميدها أو تجاهلها؟

هناك العديد من الأسباب المشروعة التي تجعل من غير المقبول تجميد صفحات الويب أثناء تشغيلها في الحالة المخفية. ولعلّ أبرز مثال على ذلك هو تطبيق يشغّل الموسيقى.

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

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

  • تشغيل الصوت
  • استخدام WebRTC
  • تعديل عنوان الجدول أو رمز الشارة
  • عرض التنبيهات
  • إرسال إشعارات فورية

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

ما هي ميزة "التخزين المؤقت للصفحات"؟

التخزين المؤقت للصفحات هو مصطلح يُستخدَم لوصف ميزة تحسين التنقّل التي تطبّقها بعض المتصفّحات، ما يجعل استخدام زرَّي الرجوع والمتابعة أسرع.

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

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

إذا لم أتمكّن من تشغيل واجهات برمجة التطبيقات غير المتزامنة في الحالات "مجمّد" أو "منتهي"، كيف يمكنني حفظ البيانات في IndexedDB؟

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

في المستقبل، سنضيف طريقة commit() إلى عناصر IDBTransaction، ما سيمنح المطوّرين طريقة لإجراء معاملات للكتابة فقط بشكل فعّال لا تتطلّب عمليات استدعاء. بعبارة أخرى، إذا كان المطوّر يكتب البيانات فقط في IndexedDB ولا يُجري معاملة معقّدة تتألف من عمليات قراءة وكتابة، سيتمكّن الأسلوب commit() من الانتهاء قبل تعليق قوائم المهام (على افتراض أنّ قاعدة بيانات IndexedDB مفتوحة).

بالنسبة إلى الرموز البرمجية التي يجب أن تعمل اليوم، يتوفّر للمطوّرين خياران:

  • استخدام ميزة "تخزين الجلسة": تخزين الجلسة هو إجراء متزامن ويتم الاحتفاظ به عند تجاهل الصفحات.
  • استخدام IndexedDB من خدمة Worker: يمكن لخدمة Worker تخزين البيانات في IndexedDB بعد إغلاق الصفحة أو تجاهلها. في مستمع أحداث freeze أو pagehide، يمكنك إرسال البيانات إلى عامل الخدمة من خلال postMessage()، ويمكن لعامل الخدمة التعامل مع حفظ البيانات.

اختبار تطبيقك في حالتَي التجميد والتجاهل

لاختبار أداء تطبيقك في حالتَي التجميد والتجاهل، يمكنك الانتقال إلى chrome://discards لتجميد أيّ من علامات التبويب المفتوحة أو تجاهلها.

واجهة مستخدم Chrome التي تم تجاهلها
واجهة مستخدم Chrome Discards

يتيح لك ذلك التأكّد من أنّ صفحتك تعالج بشكلٍ صحيح حدثَي freeze وresume بالإضافة إلى علامة document.wasDiscarded عند إعادة تحميل الصفحات بعد التخلص منها.

ملخّص

على المطوّرين الذين يريدون الحفاظ على موارد النظام في أجهزة المستخدمين إنشاء تطبيقاتهم مع مراعاة حالات دورة حياة الصفحة. من المهم أن لا تستهلك صفحات الويب موارد النظام بشكل مفرط في حالات لا يتوقعها المستخدم.

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