تطبيقات الويب التقدمية

بنية قشرة تطبيق الويب

18 دقيقة الدرس 15 من 30

مقدمة إلى بنية قشرة التطبيق

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

المفهوم الأساسي: قشرة التطبيق هي الحد الأدنى من HTML و CSS و JavaScript المطلوب لتشغيل واجهة المستخدم. يتم تخزينها مؤقتاً دون اتصال بحيث في الزيارات المتكررة، يتم تحميل القشرة فوراً بينما يتم تحميل المحتوى الديناميكي تدريجياً.

ما هي قشرة التطبيق؟

تتكون قشرة التطبيق من:

  • الرأس/التنقل: الشريط العلوي، القائمة، العلامة التجارية
  • بنية التخطيط: منطقة المحتوى الرئيسية، الأشرطة الجانبية، التذييلات
  • حالات التحميل: شاشات الهيكل العظمي، الدوارات، العناصر النائبة
  • الأنماط الأساسية: CSS للقشرة (وليس أنماط خاصة بالمحتوى)
  • السكريبتات الأساسية: JavaScript للتنقل ومنطق التطبيق

ما ليس في قشرة التطبيق:

  • المحتوى الديناميكي (المقالات، المنشورات، المنتجات)
  • البيانات التي أنشأها المستخدم
  • الصور الخاصة بالمحتوى
  • استجابات API

فوائد بنية قشرة التطبيق

  1. التحميل الفوري: يتم تحميل القشرة من الذاكرة المؤقتة فوراً
  2. دعم دون اتصال: يمكن للمستخدمين التنقل حتى بدون شبكة
  3. أداء يشبه التطبيقات الأصلية: يبدو وكأنه تطبيق أصلي
  4. تقليل عرض النطاق الترددي: يتم تخزين القشرة مؤقتاً مرة واحدة، ويتم تحديث المحتوى بشكل منفصل
  5. تجربة مستخدم أفضل: يرى المستخدمون شيئاً فوراً، وليس شاشة فارغة

قشرة التطبيق مقابل البنية التقليدية

الصفحة التقليدية المعروضة من الخادم

1. المستخدم يطلب /article/123 2. الخادم يولد صفحة HTML كاملة 3. المتصفح يقوم بتنزيل كل شيء (الرأس، المحتوى، التذييل، الأنماط، السكريبتات) 4. يتم عرض الصفحة 5. الطلب التالي للصفحة يكرر العملية بأكملها

بنية قشرة التطبيق

1. المستخدم يزور لأول مرة - يقوم بتنزيل وتخزين قشرة التطبيق مؤقتاً - يجلب المحتوى للمسار الحالي - يعرض القشرة + المحتوى 2. المستخدم ينتقل إلى /article/123 - يتم تحميل القشرة فوراً من الذاكرة المؤقتة - يتم جلب المحتوى فقط من الشبكة - يتم تحديث القشرة بالمحتوى الجديد 3. المستخدم يتصل بدون اتصال - القشرة لا تزال تعمل - المحتوى المخزن مؤقتاً متاح - التدهور الأنيق للمحتوى غير المخزن مؤقتاً

تنفيذ قشرة التطبيق

1. بنية HTML

<!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>تطبيق PWA الخاص بي</title> <!-- CSS الحرجة المضمنة --> <style> /* أنماط قشرة التطبيق */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: sans-serif; } .app-header { background: #2196F3; color: white; padding: 1rem; position: sticky; top: 0; z-index: 100; } .app-content { min-height: calc(100vh - 120px); padding: 1rem; } .app-footer { background: #f5f5f5; padding: 1rem; text-align: center; } /* شاشة الهيكل العظمي */ .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: loading 1.5s infinite; } @keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .skeleton-text { height: 1rem; margin-bottom: 0.5rem; border-radius: 4px; } .skeleton-title { height: 2rem; width: 60%; margin-bottom: 1rem; border-radius: 4px; } </style> <link rel="manifest" href="/manifest.json"> </head> <body> <!-- بنية قشرة التطبيق --> <header class="app-header"> <h1>تطبيق PWA الخاص بي</h1> <nav> <a href="/">الرئيسية</a> <a href="/about">حول</a> <a href="/articles">المقالات</a> </nav> </header> <main class="app-content" id="content"> <!-- يتم تحميل المحتوى الديناميكي هنا --> <!-- يتم عرض شاشة الهيكل العظمي أثناء التحميل --> <div class="skeleton skeleton-title"></div> <div class="skeleton skeleton-text"></div> <div class="skeleton skeleton-text"></div> <div class="skeleton skeleton-text"></div> </main> <footer class="app-footer"> <p>&copy; 2024 تطبيق PWA الخاص بي</p> </footer> <script src="/js/app.js"></script> </body> </html>

2. JavaScript قشرة التطبيق

// app.js - منطق التطبيق الأساسي class App { constructor() { this.contentElement = document.getElementById('content'); this.init(); } init() { // تسجيل Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } // التعامل مع التنقل this.setupNavigation(); // تحميل المحتوى للمسار الحالي this.loadContent(window.location.pathname); } setupNavigation() { // اعتراض نقرات الروابط للتوجيه من جانب العميل document.addEventListener('click', (e) => { if (e.target.tagName === 'A' && e.target.href.startsWith(window.location.origin)) { e.preventDefault(); const url = new URL(e.target.href); this.navigate(url.pathname); } }); // التعامل مع الرجوع/التقدم في المتصفح window.addEventListener('popstate', () => { this.loadContent(window.location.pathname); }); } navigate(path) { // تحديث URL بدون إعادة تحميل الصفحة history.pushState(null, '', path); // تحميل المحتوى للمسار الجديد this.loadContent(path); } async loadContent(path) { try { // إظهار الهيكل العظمي أثناء التحميل this.showSkeleton(); // جلب المحتوى (من الذاكرة المؤقتة أو الشبكة) const response = await fetch(`/api/content${path}`); const data = await response.json(); // عرض المحتوى this.renderContent(data); } catch (error) { console.error('خطأ في تحميل المحتوى:', error); this.showError(); } } showSkeleton() { this.contentElement.innerHTML = ` <div class="skeleton skeleton-title"></div> <div class="skeleton skeleton-text"></div> <div class="skeleton skeleton-text"></div> <div class="skeleton skeleton-text"></div> `; } renderContent(data) { this.contentElement.innerHTML = ` <article> <h1>${data.title}</h1> <div>${data.content}</div> </article> `; } showError() { this.contentElement.innerHTML = ` <div class="error"> <h2>غير قادر على تحميل المحتوى</h2> <p>يرجى التحقق من اتصالك والمحاولة مرة أخرى.</p> </div> `; } } // تهيئة التطبيق new App();

3. Service Worker لقشرة التطبيق

// sw.js - Service Worker مع تخزين قشرة التطبيق مؤقتاً const CACHE_NAME = 'app-shell-v1'; const APP_SHELL_FILES = [ '/', '/index.html', '/css/styles.css', '/js/app.js', '/images/logo.png', '/offline.html' ]; // حدث التثبيت - تخزين قشرة التطبيق مؤقتاً self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { console.log('تخزين قشرة التطبيق مؤقتاً'); return cache.addAll(APP_SHELL_FILES); }) ); self.skipWaiting(); }); // حدث التفعيل - تنظيف الذاكرات المؤقتة القديمة self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log('حذف الذاكرة المؤقتة القديمة:', cacheName); return caches.delete(cacheName); } }) ); }) ); self.clients.claim(); }); // حدث الجلب - التقديم من الذاكرة المؤقتة، الاحتياطي للشبكة self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((response) => { if (response) { // التقديم من الذاكرة المؤقتة return response; } // استنساخ الطلب للجلب const fetchRequest = event.request.clone(); return fetch(fetchRequest).then((response) => { // التحقق من استجابة صالحة if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // استنساخ الاستجابة للذاكرة المؤقتة const responseToCache = response.clone(); // تخزين استجابات API بشكل منفصل مؤقتاً if (event.request.url.includes('/api/')) { caches.open('api-cache-v1').then((cache) => { cache.put(event.request, responseToCache); }); } return response; }).catch(() => { // فشلت الشبكة، تقديم صفحة دون اتصال if (event.request.mode === 'navigate') { return caches.match('/offline.html'); } }); }) ); });

تخزين قشرة التطبيق مؤقتاً

استراتيجية التخزين المؤقت المسبق

// التخزين المؤقت المسبق لقشرة التطبيق أثناء تثبيت Service Worker const PRECACHE_URLS = [ // HTML الأساسي '/', '/index.html', // الأنماط '/css/app-shell.css', '/css/skeleton.css', // السكريبتات '/js/app.js', '/js/router.js', // الصور '/images/logo.svg', '/images/icons/menu.svg', // الخطوط '/fonts/roboto-regular.woff2', // احتياطي دون اتصال '/offline.html' ]; self.addEventListener('install', (event) => { event.waitUntil( caches.open('app-shell-v1').then((cache) => { return cache.addAll(PRECACHE_URLS); }) ); });

استراتيجية التحديث

// تحديث قشرة التطبيق عند تحديث Service Worker self.addEventListener('activate', (event) => { const currentCaches = ['app-shell-v2', 'api-cache-v1']; event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (!currentCaches.includes(cacheName)) { console.log('حذف الذاكرة المؤقتة:', cacheName); return caches.delete(cacheName); } }) ); }) ); }); // إخطار المستخدم بالتحديثات self.addEventListener('controllerchange', () => { // إظهار إشعار تحديث للمستخدم if (navigator.serviceWorker.controller) { console.log('تم تحديث قشرة التطبيق'); } });

تحميل المحتوى الديناميكي

تحديثات المحتوى المتدفق

// تحميل وعرض المحتوى تدريجياً async function loadArticle(articleId) { try { // إظهار القشرة فوراً showArticleShell(); // جلب بيانات المقال const response = await fetch(`/api/articles/${articleId}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let article = ''; // تدفق المحتوى عند وصوله while (true) { const { done, value } = await reader.read(); if (done) break; article += decoder.decode(value, { stream: true }); // تحديث واجهة المستخدم تدريجياً updateArticleContent(article); } } catch (error) { showError(error); } }

التحديثات المتفائلة

// إظهار ملاحظات فورية، المزامنة في الخلفية async function likeArticle(articleId) { // تحديث واجهة المستخدم فوراً (متفائل) updateLikeButton(articleId, true); try { // الإرسال إلى الخادم await fetch(`/api/articles/${articleId}/like`, { method: 'POST' }); } catch (error) { // التراجع عند الفشل updateLikeButton(articleId, false); showError('فشل الإعجاب بالمقال'); } }

شاشات الهيكل العظمي

توفر شاشات الهيكل العظمي عناصر نائبة مرئية أثناء تحميل المحتوى:

<!-- هيكل عظمي للمقال --> <div class="article-skeleton"> <div class="skeleton skeleton-title"></div> <div class="skeleton skeleton-meta"></div> <div class="skeleton skeleton-image"></div> <div class="skeleton skeleton-text"></div> <div class="skeleton skeleton-text"></div> <div class="skeleton skeleton-text short"></div> </div>
/* أنماط الهيكل العظمي */ .skeleton { background: linear-gradient( 90deg, #f0f0f0 0%, #e0e0e0 50%, #f0f0f0 100% ); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 4px; } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } .skeleton-title { height: 2.5rem; width: 80%; margin-bottom: 1rem; } .skeleton-meta { height: 1rem; width: 40%; margin-bottom: 1.5rem; } .skeleton-image { width: 100%; height: 300px; margin-bottom: 1.5rem; } .skeleton-text { height: 1rem; margin-bottom: 0.75rem; } .skeleton-text.short { width: 60%; }

التحميل التدريجي

تحميل المحتوى في مراحل للأداء المثالي المدرك:

// استراتيجية التحميل التدريجي async function loadPage() { // المرحلة 1: إظهار القشرة فوراً (من الذاكرة المؤقتة) showAppShell(); // المرحلة 2: تحميل المحتوى الحرج const criticalContent = await loadCriticalContent(); renderCriticalContent(criticalContent); // المرحلة 3: تحميل المحتوى الثانوي (كسول) requestIdleCallback(() => { loadSecondaryContent().then(renderSecondaryContent); }); // المرحلة 4: الجلب المسبق للصفحة التالية المحتملة requestIdleCallback(() => { prefetchNextPage(); }); } function loadCriticalContent() { return fetch('/api/content/critical').then(r => r.json()); } function loadSecondaryContent() { return fetch('/api/content/secondary').then(r => r.json()); } function prefetchNextPage() { // الجلب المسبق بناءً على سلوك المستخدم const nextPageUrl = predictNextPage(); fetch(nextPageUrl, { mode: 'no-cors' }); }
تمرين:
  1. أنشئ قشرة تطبيق مع رأس ومنطقة محتوى وتذييل
  2. نفذ شاشات هيكل عظمي لحالات التحميل
  3. أضف Service Worker لتخزين قشرة التطبيق مؤقتاً
  4. نفذ توجيهاً من جانب العميل يحمل المحتوى فقط (وليس القشرة)
  5. أضف تحميلاً تدريجياً للمحتوى الحرج والثانوي
  6. اختبر الوظائف دون اتصال - يجب أن تعمل القشرة بدون شبكة
  7. قس الأداء: يجب أن يتم تحميل قشرة التطبيق في <1 ثانية
نصيحة: حافظ على قشرة التطبيق الخاصة بك صغيرة قدر الإمكان. كلما كانت أصغر، كان تحميلها أسرع. انقل الأنماط والسكريبتات غير الحرجة خارج القشرة وقم بتحميلها تدريجياً.
تحذير: لا تخلط بين قشرة التطبيق وتطبيق الصفحة الواحدة (SPA). بينما يعملان معاً بشكل جيد، قشرة التطبيق هي على وجه التحديد حول بنية التخزين المؤقت. يمكنك تنفيذ قشرة التطبيق مع تطبيقات متعددة الصفحات التقليدية أيضاً.