واجهات برمجة تطبيقات JavaScript الحديثة
توفر المتصفحات الحديثة واجهات برمجة تطبيقات قوية تمكن من وظائف متقدمة في تطبيقات الويب. في هذا الدرس، سنستكشف واجهات برمجة تطبيقات JavaScript الحديثة الأساسية التي يجب على كل مطور معرفتها.
واجهة IntersectionObserver
تسمح لك واجهة IntersectionObserver بمراقبة التغييرات في تقاطع عنصر مع سلف أو منفذ العرض:
// استخدام IntersectionObserver الأساسي
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible:', entry.target);
// تنفيذ إجراء عندما يصبح العنصر مرئياً
entry.target.classList.add('visible');
// اختياري: إيقاف المراقبة بعد أول تقاطع
observer.unobserve(entry.target);
}
});
});
// مراقبة العناصر
const elements = document.querySelectorAll('.observe-me');
elements.forEach(el => observer.observe(el));
// التحميل البطيء للصور
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// خيارات متقدمة
const advancedObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
console.log('Intersection ratio:', entry.intersectionRatio);
console.log('Is intersecting:', entry.isIntersecting);
// العنصر مرئي بنسبة 50%
if (entry.intersectionRatio >= 0.5) {
entry.target.classList.add('half-visible');
}
});
},
{
root: null, // استخدام منفذ العرض كجذر
rootMargin: '0px 0px -100px 0px', // التشغيل قبل 100 بكسل من دخول العنصر منفذ العرض
threshold: [0, 0.25, 0.5, 0.75, 1] // التشغيل عند 0%، 25%، 50%، 75%، 100% من الرؤية
}
);
// تنفيذ التمرير اللانهائي
const loadMoreObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreContent();
}
});
});
const sentinel = document.querySelector('#load-more-sentinel');
loadMoreObserver.observe(sentinel);
async function loadMoreContent() {
const newItems = await fetchMoreItems();
renderItems(newItems);
}
// تحريك العناصر عند التمرير
const animateObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.animation = 'fadeInUp 0.6s ease-out';
animateObserver.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('.animate-on-scroll').forEach(el => {
animateObserver.observe(el);
});
نصيحة: IntersectionObserver أكثر كفاءة بكثير من مستمعي أحداث التمرير لاكتشاف رؤية العنصر. فضّله دائماً على أحداث التمرير عندما يكون ذلك ممكناً.
واجهة MutationObserver
يراقب MutationObserver التغييرات في شجرة DOM ويخطرك عند حدوثها:
// MutationObserver الأساسي
const targetNode = document.getElementById('container');
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Type:', mutation.type);
if (mutation.type === 'childList') {
console.log('Added nodes:', mutation.addedNodes);
console.log('Removed nodes:', mutation.removedNodes);
}
if (mutation.type === 'attributes') {
console.log('Attribute changed:', mutation.attributeName);
console.log('Old value:', mutation.oldValue);
}
if (mutation.type === 'characterData') {
console.log('Text content changed');
}
});
});
// التكوين
const config = {
childList: true, // مراقبة إضافات/إزالات العقد الفرعية
attributes: true, // مراقبة تغييرات السمات
characterData: true, // مراقبة تغييرات محتوى النص
subtree: true, // مراقبة جميع الأحفاد
attributeOldValue: true, // تسجيل قيم السمات السابقة
characterDataOldValue: true // تسجيل محتوى النص السابق
};
// بدء المراقبة
mutationObserver.observe(targetNode, config);
// إيقاف المراقبة
// mutationObserver.disconnect();
// مثال عملي: اكتشاف متى يتم إضافة عنصر إلى DOM
function waitForElement(selector) {
return new Promise((resolve) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
// الاستخدام
waitForElement('#dynamic-element').then(element => {
console.log('Element found:', element);
});
// مراقبة تغييرات الفئة
function onClassChange(element, callback) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.attributeName === 'class') {
callback(element.className);
}
});
});
observer.observe(element, {
attributes: true,
attributeFilter: ['class']
});
return observer;
}
// الاستخدام
const button = document.querySelector('#my-button');
onClassChange(button, (classes) => {
console.log('Classes changed:', classes);
});
// الحفظ التلقائي لتغييرات المحتوى
const editor = document.querySelector('#editor');
let saveTimeout;
const saveObserver = new MutationObserver(() => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
saveContent(editor.innerHTML);
}, 1000);
});
saveObserver.observe(editor, {
childList: true,
characterData: true,
subtree: true
});
واجهة ResizeObserver
يبلغ ResizeObserver عن التغييرات في أبعاد العنصر:
// ResizeObserver الأساسي
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`Element resized: ${width}x${height}`);
// الاستجابة لتغييرات الحجم
if (width < 600) {
entry.target.classList.add('mobile');
} else {
entry.target.classList.remove('mobile');
}
});
});
const container = document.querySelector('#responsive-container');
resizeObserver.observe(container);
// مكون متجاوب
class ResponsiveChart {
constructor(element) {
this.element = element;
this.chart = null;
this.resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
this.updateChartSize(width, height);
});
});
this.resizeObserver.observe(this.element);
}
updateChartSize(width, height) {
if (this.chart) {
this.chart.resize(width, height);
}
}
destroy() {
this.resizeObserver.disconnect();
}
}
// تحجيم النص المتجاوب
const textResizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width } = entry.contentRect;
const fontSize = Math.max(16, width / 30);
entry.target.style.fontSize = `${fontSize}px`;
});
});
document.querySelectorAll('.responsive-text').forEach(el => {
textResizeObserver.observe(el);
});
// مراقبة تغييرات حجم صندوق العنصر
const boxObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
// أحجام صناديق مختلفة
const contentBox = entry.contentBoxSize[0];
const borderBox = entry.borderBoxSize[0];
console.log('Content box:', {
width: contentBox.inlineSize,
height: contentBox.blockSize
});
console.log('Border box:', {
width: borderBox.inlineSize,
height: borderBox.blockSize
});
});
});
// التنظيف
// resizeObserver.disconnect();
ملاحظة: ResizeObserver أكثر كفاءة من أحداث إعادة حجم النافذة لمراقبة أحجام العناصر الفردية. يُطلق فقط عندما يتغير حجم العنصر المراقب فعلياً.
Web Workers
تسمح لك Web Workers بتشغيل JavaScript في خيوط خلفية، مع الحفاظ على استجابة واجهة المستخدم:
// الخيط الرئيسي (main.js)
const worker = new Worker('worker.js');
// إرسال البيانات إلى worker
worker.postMessage({
action: 'calculate',
data: [1, 2, 3, 4, 5]
});
// استقبال النتائج من worker
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
};
// معالجة الأخطاء
worker.onerror = (error) => {
console.error('Worker error:', error);
};
// إنهاء worker عند الانتهاء
worker.terminate();
// ملف Worker (worker.js)
self.onmessage = (event) => {
const { action, data } = event.data;
if (action === 'calculate') {
// حساب ثقيل
const result = performHeavyCalculation(data);
// إرسال النتيجة إلى الخيط الرئيسي
self.postMessage(result);
}
};
function performHeavyCalculation(data) {
// محاكاة عملية مكلفة
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += data.reduce((a, b) => a + b, 0);
}
return sum;
}
// مثال عملي: معالجة الصور في worker
// main.js
const imageWorker = new Worker('image-worker.js');
async function processImage(imageFile) {
const imageBitmap = await createImageBitmap(imageFile);
imageWorker.postMessage(
{ image: imageBitmap, filter: 'grayscale' },
[imageBitmap] // نقل الملكية
);
}
imageWorker.onmessage = (event) => {
const processedImage = event.data;
displayImage(processedImage);
};
// Shared Workers (مشتركة بين علامات التبويب/النوافذ)
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.start();
sharedWorker.port.postMessage('Hello from tab');
sharedWorker.port.onmessage = (event) => {
console.log('Message from shared worker:', event.data);
};
LocalStorage و SessionStorage
توفر واجهات برمجة تطبيقات Web Storage تخزين مفتاح-قيمة بسيط في المتصفح:
// LocalStorage (يستمر بعد إغلاق المتصفح)
// تخزين البيانات
localStorage.setItem('username', 'john_doe');
localStorage.setItem('theme', 'dark');
// تخزين الكائنات (يجب تحويلها إلى سلسلة)
const user = { name: 'John', age: 30 };
localStorage.setItem('user', JSON.stringify(user));
// استرجاع البيانات
const username = localStorage.getItem('username');
const theme = localStorage.getItem('theme');
// استرجاع وتحليل الكائن
const storedUser = JSON.parse(localStorage.getItem('user'));
// إزالة عنصر
localStorage.removeItem('theme');
// مسح الكل
localStorage.clear();
// التحقق من وجود المفتاح
if (localStorage.getItem('username')) {
console.log('Username exists');
}
// الحصول على عدد العناصر
console.log('Items in storage:', localStorage.length);
// التكرار على العناصر
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
// SessionStorage (يُمسح عند إغلاق علامة التبويب)
sessionStorage.setItem('sessionId', 'abc123');
const sessionId = sessionStorage.getItem('sessionId');
// فئة أداة التخزين
class Storage {
constructor(storage = localStorage) {
this.storage = storage;
}
set(key, value) {
try {
this.storage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
get(key, defaultValue = null) {
try {
const item = this.storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Storage error:', error);
return defaultValue;
}
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
has(key) {
return this.storage.getItem(key) !== null;
}
}
// الاستخدام
const store = new Storage();
store.set('user', { name: 'John', age: 30 });
const user = store.get('user');
// الاستماع لتغييرات التخزين (من علامات تبويب أخرى)
window.addEventListener('storage', (event) => {
console.log('Storage changed:', {
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url
});
});
تحذير: لدى LocalStorage حد حجم (عادة 5-10 ميجابايت) وهو متزامن، مما قد يحجب الخيط الرئيسي. للكميات الكبيرة من البيانات، استخدم IndexedDB بدلاً من ذلك.
أساسيات IndexedDB
IndexedDB هي واجهة برمجة تطبيقات منخفضة المستوى لتخزين كميات كبيرة من البيانات المنظمة:
// فتح قاعدة البيانات
const request = indexedDB.open('MyDatabase', 1);
// معالجة ترقية قاعدة البيانات (المرة الأولى أو تغيير الإصدار)
request.onupgradeneeded = (event) => {
const db = event.target.result;
// إنشاء مخزن كائنات
const objectStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// إنشاء الفهارس
objectStore.createIndex('email', 'email', { unique: true });
objectStore.createIndex('name', 'name', { unique: false });
};
// معالجة الفتح الناجح
request.onsuccess = (event) => {
const db = event.target.result;
// إضافة البيانات
const transaction = db.transaction(['users'], 'readwrite');
const objectStore = transaction.objectStore('users');
const user = {
name: 'John Doe',
email: 'john@example.com',
age: 30
};
const addRequest = objectStore.add(user);
addRequest.onsuccess = () => {
console.log('User added with ID:', addRequest.result);
};
// الحصول على البيانات
const getRequest = objectStore.get(1);
getRequest.onsuccess = () => {
console.log('User:', getRequest.result);
};
// تحديث البيانات
const updateRequest = objectStore.put({
id: 1,
name: 'Jane Doe',
email: 'jane@example.com',
age: 28
});
// حذف البيانات
const deleteRequest = objectStore.delete(1);
// الحصول على جميع البيانات
const getAllRequest = objectStore.getAll();
getAllRequest.onsuccess = () => {
console.log('All users:', getAllRequest.result);
};
// البحث بالفهرس
const emailIndex = objectStore.index('email');
const emailRequest = emailIndex.get('john@example.com');
emailRequest.onsuccess = () => {
console.log('User by email:', emailRequest.result);
};
};
// معالجة الأخطاء
request.onerror = (event) => {
console.error('Database error:', event.target.error);
};
// فئة غلاف IndexedDB (مبسطة)
class IDBStore {
constructor(dbName, storeName) {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onerror = () => reject(request.error);
});
}
async add(data) {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(id) {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
مقدمة Service Workers
تمكن Service Workers من الوظائف غير المتصلة بالإنترنت والمعالجة في الخلفية:
// تسجيل service worker (في main.js)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker registered:', registration);
})
.catch(error => {
console.error('Service Worker registration failed:', error);
});
}
// ملف Service Worker (sw.js)
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles.css',
'/script.js',
'/logo.png'
];
// حدث التثبيت - تخزين الموارد مؤقتاً
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Caching files');
return cache.addAll(urlsToCache);
})
);
});
// حدث التنشيط - تنظيف الذاكرة المؤقتة القديمة
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
// حدث الجلب - الخدمة من الذاكرة المؤقتة أو الشبكة
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
// إرجاع النسخة المخزنة مؤقتاً أو الجلب من الشبكة
return response || fetch(event.request);
})
);
});
// تحديث service worker
navigator.serviceWorker.getRegistration().then(registration => {
registration.update();
});
// الاستماع للتحديثات
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.log('New service worker activated');
window.location.reload();
});
تمرين عملي:
إنشاء معرض صور بتحميل بطيء باستخدام IntersectionObserver:
// بنية HTML
// <div class="gallery">
// <img data-src="image1.jpg" alt="Image 1">
// <img data-src="image2.jpg" alt="Image 2">
// <!-- المزيد من الصور -->
// </div>
class LazyGallery {
constructor(selector) {
this.images = document.querySelectorAll(`${selector} img[data-src]`);
this.observer = null;
this.init();
}
init() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
}
});
},
{
rootMargin: '50px' // بدء التحميل قبل 50 بكسل من الرؤية
}
);
this.images.forEach(img => {
this.observer.observe(img);
});
}
loadImage(img) {
const src = img.dataset.src;
// إنشاء صورة مؤقتة للتحميل المسبق
const tempImg = new Image();
tempImg.onload = () => {
img.src = src;
img.classList.add('loaded');
this.observer.unobserve(img);
};
tempImg.onerror = () => {
img.classList.add('error');
this.observer.unobserve(img);
};
tempImg.src = src;
}
destroy() {
this.observer.disconnect();
}
}
// الاستخدام
const gallery = new LazyGallery('.gallery');
الملخص
في هذا الدرس، تعلمت:
- IntersectionObserver لاكتشاف الرؤية والتحميل البطيء
- MutationObserver لمراقبة تغييرات DOM
- ResizeObserver لتحجيم العناصر المستجيبة
- Web Workers للمعالجة المتوازية
- LocalStorage و SessionStorage لاستمرار البيانات البسيطة
- أساسيات IndexedDB لتخزين البيانات واسع النطاق
- Service Workers للقدرات غير المتصلة بالإنترنت
التالي: في الدرس الأخير، سنجمع كل شيء معاً مع مشروع شامل في العالم الحقيقي ونستكشف خطواتك التالية في تطوير JavaScript!