اذهب إلى المحتوى

تحميل الموارد الخارجية في صفحات الويب وتتبع حالتها عبر جافاسكربت


محمد أمين بوقرة

يمكّننا المتصفّح من تتبّع تحميل الموارد الخارجيّة، مثل السكربتات والعناصر iframes والصور وما إلى ذلك، وهناك حدثان لهذا الغرض:

  • onload -- نجاح التحميل
  • onerror -- حصل خطأ ما

تحميل سكربت

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

let script = document.createElement('script');
script.src = "my.js";

document.head.append(script);

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


ملاحظة:

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


script.onload

المساعد الرئيسيّ هو الحدث load ويقع عند الانتهاء من تحميل السكربت وتنفيذه. على سبيل المثال:

let script = document.createElement('script');

// يمكن أن يحمّل أيّ سكربت، من أيّ نطاق
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
document.head.append(script);

script.onload = function() {
  // "_" ينشئ السكربت المتغيّر
  alert( _.VERSION ); // shows library version
};

بداخل onload، يمكننا إذًا استخدام متغيّرات السكربت وتنفيذ الدوالّ وما إلى ذلك. لكن ماذا لو فشل التحميل؟ على سبيل المثال، إذا كان لا وجود لهذا السكربت (الخطأ 404) أو الخادم معطّل (غير متاح).

script.onerror

يمكن تتبّع الأخطاء التي تحصل خلال تحميل السكربت في حدث error.

على سبيل المثال، لنقم بطلب سكربت غير موجود:

let script = document.createElement('script');
script.src = "https://example.com/404.js"; // لا وجود لهذا السكربت
document.head.append(script);

script.onerror = function() {
  alert("Error loading " + this.src); // https://example.com/404.js خطأ في تحميل
};

يُرجى التنبّه إلى أنّه لا يمكننا الحصول على تفاصيل خطأ HTTP هنا. لا نعلم إن كان خطأ 400 أو 500 أو شيئا آخر. نعلم فقط أنّ التحميل قد فشل.


تنبيه: لا تتبّع الأحداثُ onload/onerror سوى التحميل نفسه.

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


الموارد الأخرى

تعمل الأحداث load وerror مع الموارد الأخرى كذلك، وتعمل أساسا مع أيّ مورد له src خارجيّ. مثلا:

let img = document.createElement('img');
img.src = "https://js.cx/clipart/train.gif"; // (*)

img.onload = function() {
  alert(`Image loaded, size ${img.width}x${img.height}`);
};

img.onerror = function() {
  alert("Error occurred while loading image");
};

لكنّ هناك بعض الملاحظات تعود لأسباب تاريخيّة:

  • تبدأ معظم الموارد في التحميل عندما تُضاف إلى المستند لكنّ <img> تُعدّ استثناءً، حيث تبدأ في التحميل عندما تحصل على الرابط أو المصدر src (*).
  • بالنسبة لـ <iframe>، يقع الحدث iframe.onload عندما ينتهي تحميل iframe، سواء بنجاح التحميل أو في حالة خطأ.

سياسة تعدد المصادر Crossorigin policy

هناك قاعدة تقول: لا يمكن لسكربتات موقع ما الوصول إلى محتويات موقع آخر. فمثلا، لا يستطيع سكربت موجود في https://facebook.com قراءة صندوق بريد المستخدم في https://gmail.com.

أو بشكل أدقّ، لا يمكن لأحد المصادر (الثلاثيّة domain/port/protocol البروتوكول/المنفذ/النطاق) الوصول إلى محتوى مصدر آخر. فحتى لو كان لدينا نطاق فرعيّ أو اختلف المنفذ فقط، فستُعدّ هذه مصادر مختلفة ولا يكون لديها مدخل إلى بعضها البعض.

تشمل هذه القاعدة أيضا الموارد التي هي من نطاقات أخرى.

إذا كنا نستخدم سكربتا من نطاق آخر، وكان فيه خطأ ما، فلا يمكننا الحصول على تفاصيل ذلك الخطأ. على سبيل المثال، لنأخذ السكربت (الفاسد) error.js المكوّن من مجرّد استدعاء لدالّة:

// ? error.js
noSuchFunction();

ثمّ لنحمّله من نفس الموقع الموجود فيه:

<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="/article/onload-onerror/crossorigin/error.js"></script>

يمكننا رؤية تقرير جيّد للخطأ، هكذا:

Uncaught ReferenceError: noSuchFunction is not defined
https://javascript.info/article/onload-onerror/crossorigin/error.js, 1:1

لنحمّل الآن نفس السكربت من نطاق آخر:

<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>

التقرير الآن مختلف:

Script error.
, 0:0

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

لماذا قد نحتاج إلى تفاصيل الخطأ؟

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

تُفرض نفس سياسة تعدّد المصادر CORS على أنواع الموارد الأخرى كذلك. لإتاحة الوصول متعدّد المصادر، يجب أن يكون للوسم <script> السمة crossorigin، ويجب كذلك أن يقدّم الخادم ترويسات Headers خاصّة. هناك ثلاثة مستويات للوصول متعدّد المصادر:

  1. لا وجود للسمة crossorigin -- الوصول ممنوع.
  2. crossorigin="anonymous"‎ -- الوصول متاح إذا أجاب الخادم بالترويسة Access-Control-Allow-Origin مع * أو مع مصدرنا. لا يرسل المتصفّح معلومات الترخيص authorization وملفّات الارتباط cookies إلى الخادم البعيد.
  3. crossorigin="use-credentials"‎ -- الوصول متاح إذا أجاب الخادم بالترويسة Access-Control-Allow-Origin مع مصدرنا و Access-Control-Allow-Credentials: true. يرسل المتصفّح معلومات الترخيص وملفّات الارتباط إلى الخادم البعيد.

في حالتنا هذه، لم يكن لدينا أيّ وسم crossorigin. لذا فإنّ الوصول متعدّد المصادر قد مُنع. لنضفه الآن.

يمكننا الاختيار بين "anonymous" (لا إرسال لملفات الارتباط، يُحتاج إلى ترويسة واحدة من جانب الخادم) و"use-credentials" (يرسل ملفّات الارتباط أيضا، يُحتاج إلى ترويستين من جانب الخادم).

إذا لم نكن نهتمّ بملفّات الارتباط، فإنّ "anonymous" هي الخيار:

<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>

حاليّا، على افتراض أنّ الخادم يوفّر ترويسة Access-Control-Allow-Origin، كلّ شيء على ما يرام، لدينا تقرير الخطأ الكامل.

الملخص

توفّر الصور <img> والتنسيقات الخارجيّة والسكربتات والموارد الأخرى الأحداث load وerror لتتبّع تحميلها:

  • يقع load عند نجاح التحميل.
  • يقع error عند فشل التحميل.

الاستثناء الوحيد هو <iframe>: لأسباب تاريخيّة يحدث load على الدوام، عند أيّ انتهاء للتحميل، حتى لو لم توجد الصفحة.

يعمل الحدث readystatechange أيضا مع الموارد، لكن يندر استخدامه، لأنّ الأحداث load/error أبسط.

التمارين

حمل الصور بواسطة ردود النداء callbacks

الأهميّة: 4

عادة، تُحمّل الصور عند إنشائها. فعندما نضيف <img> إلى الصفحة، لا يرى المستخدم الصورة فورا. يحتاج المتصفّح إلى تحميلها أوّلا.

لإظهار الصورة فورا، يمكننا إنشاؤها "مسبقا"، هكذا:

let img = document.createElement('img');
img.src = 'my.jpg';

يبدأ المتصفّح تحميل الصورة ويحفظها في الذاكرة المؤقّتة. لاحقا، عندما تحضر نفس الصورة في المستند (مهما كانت الطريقة)، فإنّها تظهر فورا.

أنشئ الدالّة preloadImages(sources, callback)‎ التي تحمّل جميع الصور التي في المصفوفة sources، وعندما تجهز، تنفّذ callback.

على سبيل المثال، سيظهر هذا التنبيه alert بعدما تُحمّل الصور:

function loaded() {
  alert("Images loaded")
}

preloadImages(["1.jpg", "2.jpg", "3.jpg"], loaded);

حتى في حالة خطأ، يجب أن تفترض الدالّة أنّ الصورة "محمّلة". بعبارة أخرى، تُنفّذ callback سواء عند نجاح تحميل الصور أو فشله.

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

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

أنجز الحلّ في البيئة التجريبية

الحل

الخوارزميّة:

  1. اجعل img لكلّ مصدر.
  2. أضف onload/onerror لكلّ صورة.
  3. زد في العدّاد كلّما اشتغل onload أوonerror.
  4. عندما تصير قيمة العدّاد تساوي عدد المصادر، نكون قد انتهينا: callback()‎.

افتح الحلّ في البيئة التجريبية

ترجمة -وبتصرف- للمقال Resource loading: onload and onerror من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...