دليل شامل لواجهة برمجة تطبيقات ResizeObserver في JavaScript لإنشاء مكونات متجاوبة حقًا ومدركة للعناصر وإدارة التخطيطات الديناميكية بأداء عالٍ.
واجهة ResizeObserver API: سر الويب الحديث لتتبع حجم العناصر والتخطيطات المتجاوبة بسهولة
في عالم تطوير الويب الحديث، نبني التطبيقات باستخدام المكونات. نفكر بمنطق الكتل البرمجية المستقلة والقابلة لإعادة الاستخدام لواجهة المستخدم—مثل البطاقات، ولوحات المعلومات، والويدجت، والأشرطة الجانبية. ومع ذلك، لسنوات، كانت أداتنا الأساسية للتصميم المتجاوب، وهي استعلامات الوسائط في CSS، منفصلة بشكل أساسي عن هذا الواقع القائم على المكونات. استعلامات الوسائط تهتم بشيء واحد فقط: حجم منطقة العرض (viewport) العامة. هذا القيد وضع المطورين في زاوية ضيقة، مما أدى إلى حسابات معقدة، وتخطيطات هشة، واختراقات JavaScript غير فعالة.
ماذا لو كان بإمكان المكون أن يكون على دراية بحجمه الخاص؟ ماذا لو كان بإمكانه تكييف تخطيطه ليس لأن نافذة المتصفح تم تغيير حجمها، بل لأن الحاوية التي يعيش فيها تم ضغطها بواسطة عنصر مجاور؟ هذه هي المشكلة التي تحلها واجهة ResizeObserver API بأناقة. إنها توفر آلية متصفح أصلية وموثوقة وعالية الأداء للتفاعل مع التغييرات في حجم أي عنصر من عناصر DOM، مما يبشر بعصر من الاستجابة الحقيقية على مستوى العنصر.
هذا الدليل الشامل سيستكشف واجهة ResizeObserver API من الألف إلى الياء. سنغطي ماهيتها، ولماذا تعتبر تحسينًا هائلاً مقارنة بالطرق السابقة، وكيفية استخدامها من خلال أمثلة عملية من العالم الحقيقي. بنهاية هذا الدليل، ستكون مجهزًا لبناء تخطيطات أكثر قوة ومرونة وديناميكية من أي وقت مضى.
الطريقة القديمة: قيود الاستجابة المعتمدة على منطقة العرض
لتقدير قوة ResizeObserver بشكل كامل، يجب علينا أولاً فهم التحديات التي تتغلب عليها. لأكثر من عقد من الزمان، هيمنت على مجموعة أدواتنا للتصميم المتجاوب طريقتان: استعلامات الوسائط في CSS والاستماع إلى الأحداث باستخدام JavaScript.
قيود استعلامات الوسائط في CSS
تعتبر استعلامات الوسائط في CSS حجر الزاوية في تصميم الويب المتجاوب. فهي تسمح لنا بتطبيق أنماط مختلفة بناءً على خصائص الجهاز، وأكثرها شيوعًا هو عرض وارتفاع منطقة العرض.
يبدو استعلام الوسائط النموذجي كما يلي:
/* If the browser window is 600px wide or less, make the body's background lightblue */
@media screen and (max-width: 600px) {
body {
background-color: lightblue;
}
}
يعمل هذا بشكل رائع لتعديلات تخطيط الصفحة عالية المستوى. ولكن، فكر في مكون بطاقة `UserInfo` قابل لإعادة الاستخدام. قد ترغب في أن تعرض هذه البطاقة صورة رمزية بجوار اسم المستخدم في تخطيط عريض، ولكن أن تضع الصورة الرمزية فوق الاسم في تخطيط ضيق. إذا تم وضع هذه البطاقة في منطقة محتوى رئيسية واسعة، فيجب أن تستخدم التخطيط العريض. وإذا تم وضع نفس البطاقة بالضبط في شريط جانبي ضيق، فيجب أن تتبنى تلقائيًا التخطيط الضيق، بغض النظر عن العرض الإجمالي لمنطقة العرض.
مع استعلامات الوسائط، هذا مستحيل. فالبطاقة ليس لديها أي معرفة بسياقها الخاص. يتم تحديد تصميمها بالكامل بواسطة منطقة العرض العامة. وهذا يجبر المطورين على إنشاء فئات متغيرة مثل .user-card--narrow
وتطبيقها يدويًا، مما يكسر الطبيعة المستقلة للمكون.
مشاكل الأداء في اختراقات JavaScript
كانت الخطوة التالية الطبيعية للمطورين الذين يواجهون هذه المشكلة هي اللجوء إلى JavaScript. كانت الطريقة الأكثر شيوعًا هي الاستماع إلى حدث `resize` لكائن `window`.
window.addEventListener('resize', () => {
// For every component on the page that needs to be responsive...
// Get its current width
// Check if it crosses a threshold
// Apply a class or change styles
});
هذه الطريقة لها عدة عيوب خطيرة:
- كابوس أداء: يمكن أن يتم إطلاق حدث `resize` عشرات أو حتى مئات المرات خلال عملية سحب واحدة لتغيير الحجم. إذا كانت دالة المعالجة الخاصة بك تقوم بحسابات معقدة أو تعديلات على DOM لعدة عناصر، فيمكنك بسهولة التسبب في مشاكل أداء خطيرة وتقطعات (jank) وتضارب في التخطيط.
- لا تزال تعتمد على منطقة العرض: يرتبط الحدث بكائن `window`، وليس بالعنصر نفسه. لا يزال مكونك يتغير فقط عند تغيير حجم النافذة بأكملها، وليس عندما يتغير حجم حاويته الرئيسية لأسباب أخرى (مثل إضافة عنصر شقيق، أو توسيع أكورديون، إلخ).
- استقصاء غير فعال: لالتقاط تغييرات الحجم التي لا يسببها تغيير حجم النافذة، لجأ المطورون إلى حلقات `setInterval` أو `requestAnimationFrame` للتحقق بشكل دوري من أبعاد العنصر. هذا غير فعال للغاية، حيث يستهلك باستمرار دورات وحدة المعالجة المركزية ويستنزف عمر البطارية على الأجهزة المحمولة، حتى عندما لا يتغير شيء.
كانت هذه الطرق حلولًا مؤقتة وليست حلولًا جذرية. كان الويب بحاجة إلى طريقة أفضل—واجهة برمجة تطبيقات فعالة تركز على العنصر لمراقبة تغييرات الحجم. وهذا بالضبط ما توفره ResizeObserver.
ظهور ResizeObserver: حل حديث وعالي الأداء
ما هي واجهة ResizeObserver API؟
واجهة ResizeObserver API هي واجهة متصفح تسمح لك بتلقي إشعار عندما يتغير حجم صندوق المحتوى أو صندوق الإطار الخاص بالعنصر. إنها توفر طريقة غير متزامنة وعالية الأداء لمراقبة العناصر بحثًا عن تغييرات الحجم دون عيوب الاستقصاء اليدوي أو حدث `window.resize`.
فكر فيها على أنها `IntersectionObserver` للأبعاد. فبدلاً من إخبارك عندما يظهر عنصر ما في منطقة العرض عند التمرير، فإنها تخبرك عندما يتم تعديل حجم صندوقه. يمكن أن يحدث هذا لأي عدد من الأسباب:
- تم تغيير حجم نافذة المتصفح.
- تمت إضافة محتوى إلى العنصر أو إزالته منه (مثل التفاف النص إلى سطر جديد).
- تم تغيير خصائص CSS الخاصة بالعنصر مثل `width` أو `height` أو `padding` أو `font-size`.
- تغير حجم العنصر الأب، مما تسبب في تقلصه أو نموه.
المزايا الرئيسية على الطرق التقليدية
إن ResizeObserver ليس مجرد تحسين طفيف؛ بل هو نقلة نوعية في إدارة التخطيط على مستوى المكونات.
- أداء عالٍ للغاية: يتم تحسين الواجهة بواسطة المتصفح. فهي لا تطلق رد نداء لكل تغيير بكسل واحد. بدلاً من ذلك، تقوم بتجميع الإشعارات وتقديمها بكفاءة ضمن دورة عرض المتصفح (عادةً قبل الرسم مباشرة)، مما يمنع تضارب التخطيط الذي تعاني منه معالجات `window.resize`.
- خاص بالعنصر: هذه هي قوتها الخارقة. أنت تراقب عنصرًا محددًا، ويتم إطلاق رد النداء فقط عندما يتغير حجم ذلك العنصر. هذا يفصل منطق مكونك عن منطقة العرض العامة، مما يتيح نمطية حقيقية ومفهوم "استعلامات العنصر".
- بسيطة وتصريحية: الواجهة سهلة الاستخدام بشكل ملحوظ. تقوم بإنشاء مراقب، وتخبره بالعناصر التي يجب مراقبتها، وتوفر دالة رد نداء واحدة للتعامل مع جميع الإشعارات.
- دقيقة وشاملة: يوفر المراقب معلومات مفصلة حول الحجم الجديد، بما في ذلك صندوق المحتوى وصندوق الإطار والحشو، مما يمنحك تحكمًا دقيقًا في منطق التخطيط الخاص بك.
كيفية استخدام ResizeObserver: دليل عملي
يتضمن استخدام الواجهة ثلاث خطوات بسيطة: إنشاء مراقب، ومراقبة عنصر أو أكثر من العناصر المستهدفة، وتحديد منطق رد النداء. دعنا نحلل ذلك.
الصيغة الأساسية
جوهر الواجهة هو مُنشئ `ResizeObserver` وتوابعه.
// 1. Select the element you want to watch
const myElement = document.querySelector('.my-component');
// 2. Define the callback function that will run when a size change is detected
const observerCallback = (entries) => {
for (let entry of entries) {
// The 'entry' object contains information about the observed element's new size
console.log('Element size has changed!');
console.log('Target element:', entry.target);
console.log('New content rect:', entry.contentRect);
console.log('New border box size:', entry.borderBoxSize[0]);
}
};
// 3. Create a new ResizeObserver instance, passing it the callback function
const observer = new ResizeObserver(observerCallback);
// 4. Start observing the target element
observer.observe(myElement);
// To stop observing a specific element later:
// observer.unobserve(myElement);
// To stop observing all elements tied to this observer:
// observer.disconnect();
فهم دالة رد النداء ومدخلاتها
دالة رد النداء التي تقدمها هي قلب منطقك. تتلقى مصفوفة من كائنات `ResizeObserverEntry`. إنها مصفوفة لأن المراقب يمكنه تقديم إشعارات لعدة عناصر مراقبة في دفعة واحدة.
يحتوي كل كائن `entry` على معلومات قيمة:
entry.target
: مرجع إلى عنصر DOM الذي تغير حجمه.entry.contentRect
: كائن `DOMRectReadOnly` يوفر أبعاد صندوق محتوى العنصر (العرض، الارتفاع، x، y، أعلى، يمين، أسفل، يسار). هذه خاصية قديمة ويوصى عمومًا باستخدام خصائص حجم الصندوق الأحدث أدناه.entry.borderBoxSize
: مصفوفة تحتوي على كائن به `inlineSize` (العرض) و `blockSize` (الارتفاع) لصندوق إطار العنصر. هذه هي الطريقة الأكثر موثوقية والمستقبلية للحصول على الحجم الإجمالي للعنصر. إنها مصفوفة لدعم حالات الاستخدام المستقبلية مثل التخطيطات متعددة الأعمدة حيث قد يتم تقسيم العنصر إلى أجزاء متعددة. في الوقت الحالي، يمكنك دائمًا استخدام العنصر الأول بأمان: `entry.borderBoxSize[0]`.entry.contentBoxSize
: مشابه لـ `borderBoxSize`، ولكنه يوفر أبعاد صندوق المحتوى (داخل الحشو).entry.devicePixelContentBoxSize
: يوفر حجم صندوق المحتوى بوحدات بكسل الجهاز.
أفضل ممارسة رئيسية: فضل استخدام `borderBoxSize` و `contentBoxSize` على `contentRect`. فهي أكثر قوة، وتتوافق مع خصائص CSS المنطقية الحديثة (`inlineSize` للعرض، و `blockSize` للارتفاع)، وهي المسار المستقبلي للواجهة.
حالات استخدام وأمثلة من العالم الحقيقي
النظرية رائعة، لكن ResizeObserver يلمع حقًا عندما تراه قيد التنفيذ. دعنا نستكشف بعض السيناريوهات الشائعة حيث يوفر حلاً نظيفًا وقويًا.
1. تخطيطات المكونات الديناميكية (مثال "البطاقة")
دعنا نحل مشكلة بطاقة `UserInfo` التي ناقشناها سابقًا. نريد أن تتحول البطاقة من تخطيط أفقي إلى تخطيط رأسي عندما تصبح ضيقة جدًا.
HTML:
<div class="card-container">
<div class="user-card">
<img src="avatar.jpg" alt="User Avatar" class="user-card-avatar">
<div class="user-card-info">
<h3>Jane Doe</h3>
<p>Senior Frontend Developer</p>
</div>
</div>
</div>
CSS:
.user-card {
display: flex;
align-items: center;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
transition: all 0.3s ease;
}
/* Vertical layout state */
.user-card.is-narrow {
flex-direction: column;
text-align: center;
}
.user-card-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-right: 1rem;
}
.user-card.is-narrow .user-card-avatar {
margin-right: 0;
margin-bottom: 1rem;
}
JavaScript مع ResizeObserver:
const card = document.querySelector('.user-card');
const cardObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { inlineSize } = entry.borderBoxSize[0];
// If the card's width is less than 350px, add the 'is-narrow' class
if (inlineSize < 350) {
entry.target.classList.add('is-narrow');
} else {
entry.target.classList.remove('is-narrow');
}
}
});
cardObserver.observe(card);
الآن، لا يهم أين يتم وضع هذه البطاقة. إذا وضعتها في حاوية عريضة، فستكون أفقية. إذا سحبت الحاوية لتصغيرها، فسيكتشف `ResizeObserver` التغيير ويطبق تلقائيًا فئة `.is-narrow`، ويعيد ترتيب المحتوى. هذه هي التغليف الحقيقي للمكونات.
2. تصورات البيانات والرسوم البيانية المتجاوبة
غالبًا ما تحتاج مكتبات تصور البيانات مثل D3.js أو Chart.js أو ECharts إلى إعادة رسم نفسها عندما يتغير حجم عنصر الحاوية الخاص بها. هذه حالة استخدام مثالية لـ `ResizeObserver`.
const chartContainer = document.getElementById('chart-container');
// Assume 'myChart' is an instance of a chart from a library
// with a 'redraw(width, height)' method.
const myChart = createMyChart(chartContainer);
const chartObserver = new ResizeObserver(entries => {
const entry = entries[0];
const { inlineSize, blockSize } = entry.contentBoxSize[0];
// Debouncing is often a good idea here to avoid redrawing too frequently
// although ResizeObserver already batches calls.
requestAnimationFrame(() => {
myChart.redraw(inlineSize, blockSize);
});
});
chartObserver.observe(chartContainer);
يضمن هذا الكود أنه بغض النظر عن كيفية تغيير حجم `chart-container`—عبر لوحة مقسمة في لوحة معلومات، أو شريط جانبي قابل للطي، أو تغيير حجم النافذة—سيتم دائمًا إعادة عرض الرسم البياني ليلائم حدوده تمامًا، دون أي مستمعين `window.onresize` يقتلون الأداء.
3. الطباعة التكيفية
في بعض الأحيان تريد أن يملأ عنوان ما مساحة أفقية محددة، مع تكييف حجم الخط الخاص به مع عرض الحاوية. بينما يحتوي CSS الآن على `clamp()` ووحدات استعلام الحاوية لهذا الغرض، يمنحك `ResizeObserver` تحكمًا دقيقًا باستخدام JavaScript.
const adaptiveHeading = document.querySelector('.adaptive-heading');
const headingObserver = new ResizeObserver(entries => {
const entry = entries[0];
const containerWidth = entry.borderBoxSize[0].inlineSize;
// A simple formula to calculate font size.
// You can make this as complex as you need.
const newFontSize = Math.max(16, containerWidth / 10);
entry.target.style.fontSize = `${newFontSize}px`;
});
headingObserver.observe(adaptiveHeading);
4. إدارة الاقتطاع وروابط "اقرأ المزيد"
نمط شائع في واجهة المستخدم هو عرض مقتطف من النص وزر "اقرأ المزيد" فقط إذا تجاوز النص الكامل حاويته. يعتمد هذا على كل من حجم الحاوية وطول المحتوى.
const textBox = document.querySelector('.truncatable-text');
const textContent = textBox.querySelector('p');
const truncationObserver = new ResizeObserver(entries => {
const entry = entries[0];
const target = entry.target;
// Check if the scroll height is greater than the client height
const isOverflowing = target.scrollHeight > target.clientHeight;
target.classList.toggle('is-overflowing', isOverflowing);
});
truncationObserver.observe(textContent);
يمكن لـ CSS الخاص بك بعد ذلك استخدام فئة `.is-overflowing` لإظهار تلاشي متدرج وزر "اقرأ المزيد". يضمن المراقب تشغيل هذا المنطق تلقائيًا كلما تغير حجم الحاوية، مما يظهر أو يخفي الزر بشكل صحيح.
اعتبارات الأداء وأفضل الممارسات
بينما يتميز `ResizeObserver` بأداء عالٍ بطبيعته، هناك بعض أفضل الممارسات والمخاطر المحتملة التي يجب الانتباه إليها.
تجنب الحلقات اللانهائية
الخطأ الأكثر شيوعًا هو تعديل خاصية للعنصر المراقب داخل رد النداء والتي بدورها تسبب تغيير حجم آخر. على سبيل المثال، إذا أضفت حشوًا إلى العنصر، سيتغير حجمه، مما سيؤدي إلى تشغيل رد النداء مرة أخرى، والذي يضيف المزيد من الحشو، وهكذا.
// DANGER: Infinite loop!
const badObserver = new ResizeObserver(entries => {
const el = entries[0].target;
// Changing padding resizes the element, which triggers the observer again.
el.style.paddingLeft = parseInt(el.style.paddingLeft || 0) + 1 + 'px';
});
المتصفحات ذكية وستكتشف ذلك. بعد بضع استدعاءات سريعة في نفس الإطار، ستتوقف وتطلق خطأ: `ResizeObserver loop limit exceeded`.
كيفية تجنب ذلك:
- تحقق قبل التغيير: قبل إجراء تغيير، تحقق مما إذا كان ضروريًا بالفعل. على سبيل المثال، في مثال البطاقة لدينا، نقوم فقط بإضافة/إزالة فئة، ولا نغير خاصية العرض باستمرار.
- عدّل عنصرًا ابنًا: إذا أمكن، ضع المراقب على غلاف أب وقم بإجراء تعديلات الحجم على عنصر ابن. هذا يكسر الحلقة لأن العنصر المراقب نفسه لا يتم تغييره.
- استخدم `requestAnimationFrame`:** في بعض الحالات المعقدة، يمكن أن يؤدي تغليف تعديل DOM الخاص بك في `requestAnimationFrame` إلى تأجيل التغيير إلى الإطار التالي، مما يكسر الحلقة.
متى تستخدم `unobserve()` و `disconnect()`
تمامًا كما هو الحال مع `addEventListener`، من الضروري تنظيف المراقبين لمنع تسرب الذاكرة، خاصة في تطبيقات الصفحة الواحدة (SPAs) المبنية بأطر عمل مثل React أو Vue أو Angular.
عندما يتم إلغاء تحميل مكون أو تدميره، يجب عليك استدعاء `observer.unobserve(element)` أو `observer.disconnect()` إذا لم يعد المراقب مطلوبًا على الإطلاق. في React، يتم ذلك عادةً في دالة التنظيف لخطاف `useEffect`. في Angular، يمكنك استخدام خطاف دورة الحياة `ngOnDestroy`.
دعم المتصفحات
حتى اليوم، يتم دعم `ResizeObserver` في جميع المتصفحات الحديثة الرئيسية، بما في ذلك Chrome و Firefox و Safari و Edge. الدعم ممتاز للجماهير العالمية. بالنسبة للمشاريع التي تتطلب دعمًا للمتصفحات القديمة جدًا مثل Internet Explorer 11، يمكن استخدام polyfill، ولكن بالنسبة لمعظم المشاريع الجديدة، يمكنك استخدام الواجهة محليًا بثقة.
ResizeObserver مقابل المستقبل: استعلامات الحاوية في CSS
من المستحيل مناقشة `ResizeObserver` دون ذكر نظيره التصريحي: استعلامات الحاوية في CSS. تتيح لك استعلامات الحاوية (`@container`) كتابة قواعد CSS التي تنطبق على عنصر بناءً على حجم حاويته الرئيسية، وليس منطقة العرض.
بالنسبة لمثال البطاقة الخاص بنا، يمكن أن يبدو CSS كما يلي مع استعلامات الحاوية:
.card-container {
container-type: inline-size;
}
/* The card itself is not the container, its parent is */
.user-card {
display: flex;
/* ... other styles ... */
}
@container (max-width: 349px) {
.user-card {
flex-direction: column;
}
}
هذا يحقق نفس النتيجة المرئية لمثال `ResizeObserver` الخاص بنا، ولكن بالكامل في CSS. فهل هذا يجعل `ResizeObserver` قديمًا؟ بالطبع لا.
فكر فيهما كأدوات تكميلية لوظائف مختلفة:
- استخدم استعلامات الحاوية في CSS عندما تحتاج إلى تغيير تصميم عنصر بناءً على حجم حاويته. يجب أن يكون هذا هو خيارك الافتراضي للتغييرات الشكلية البحتة.
- استخدم ResizeObserver عندما تحتاج إلى تشغيل منطق JavaScript استجابة لتغيير الحجم. هذا ضروري للمهام التي لا يمكن لـ CSS التعامل معها، مثل:
- تشغيل مكتبة رسوم بيانية لإعادة العرض.
- إجراء تعديلات معقدة على DOM.
- حساب مواضع العناصر لمحرك تخطيط مخصص.
- التفاعل مع واجهات برمجة تطبيقات أخرى بناءً على حجم العنصر.
إنهما يحلان نفس المشكلة الأساسية من زوايا مختلفة. `ResizeObserver` هي الواجهة البرمجية الأمرية، بينما استعلامات الحاوية هي الحل التصريحي الأصلي في CSS.
الخاتمة: تبني التصميم المدرك للعناصر
تعد واجهة `ResizeObserver` API لبنة أساسية للويب الحديث القائم على المكونات. إنها تحررنا من قيود منطقة العرض وتمكننا من بناء مكونات نمطية حقًا ومدركة لذاتها يمكنها التكيف مع أي بيئة توضع فيها. من خلال توفير طريقة عالية الأداء وموثوقة لمراقبة أبعاد العناصر، فإنها تقضي على الحاجة إلى اختراقات JavaScript الهشة وغير الفعالة التي عانى منها تطوير الواجهة الأمامية لسنوات.
سواء كنت تبني لوحة معلومات بيانات معقدة، أو نظام تصميم مرن، أو مجرد ويدجت واحد قابل لإعادة الاستخدام، يمنحك `ResizeObserver` التحكم الدقيق الذي تحتاجه لإدارة التخطيطات الديناميكية بثقة وكفاءة. إنها أداة قوية، عند دمجها مع تقنيات التخطيط الحديثة واستعلامات الحاوية القادمة في CSS، تتيح نهجًا أكثر مرونة وقابلية للصيانة وتطورًا للتصميم المتجاوب. لقد حان الوقت للتوقف عن التفكير في الصفحة فقط والبدء في بناء مكونات تفهم مساحتها الخاصة.