
تطبيق الويب التقدمي (PWA) هو تطبيق ويب يستخدم إمكانات المتصفح الحديثة لتقديم تجربة تشبه التطبيق. تتميز تطبيقات PWA بأنها موثوقة وسريعة وجذابة - وتجمع بين الوصول إلى الويب وإمكانيات تطبيقات الهاتف المحمول الأصلية.
تطبيق الويب التقدمي (PWA) هو تطبيق ويب يستخدم إمكانات المتصفح الحديثة لتقديم تجربة تشبه التطبيق. تتميز تطبيقات PWA بأنها موثوقة وسريعة وجذابة - وتجمع بين الوصول إلى الويب وإمكانيات تطبيقات الهاتف المحمول الأصلية.
تحل تطبيقات PWA توترًا أساسيًا: يريد المستخدمون التوفر الفوري للويب (بدون تثبيت، قابل للربط، محدث دائمًا) ولكنهم يتوقعون أداء وميزات التطبيقات الأصلية (الدعم دون اتصال بالإنترنت، ودفع الإشعارات، ووجود الشاشة الرئيسية). تقدم PWAs كليهما.
تمت صياغة مصطلح "تطبيق الويب التقدمي" بواسطة Alex Russell وFrances Berriman من Google في عام 2015. ويجب أن تستوفي PWA هذه الخصائص العشرة:
| # | المبدأ | لماذا يهم؟ |
|---|---|---|
| 1 | تقدمي — يناسب الجميع | تدهور رشيق على المتصفحات القديمة |
| 2 | ** سريع الاستجابة ** — يناسب أي شاشة | سطح المكتب، الجهاز اللوحي، الهاتف المحمول |
| 3 | اتصال مستقل — يعمل دون اتصال بالإنترنت | التخزين المؤقت لعامل الخدمة |
| 4 | يشبه التطبيق — إحساس التطبيق الأصلي | بنية شل، والتنقل السلس |
| 5 | جديد — محدث دائمًا | دورة حياة تحديث عامل الخدمة |
| 6 | آمن — يتم تقديمه عبر HTTPS | يمنع العبث، مطلوب لعمال الخدمة |
| 7 | قابل للاكتشاف — صديق لكبار المسئولين الاقتصاديين | تقوم محركات البحث بفهرسة PWAs |
| 8 | قابلة لإعادة المشاركة — إشعارات الدفع | إعادة جذب المستخدمين مثل التطبيقات الأصلية |
| 9 | قابل للتثبيت — أضف إلى الشاشة الرئيسية | بيان تطبيق الويب |
| 10 | قابل للربط — المشاركة عبر URL | لا يوجد متجر التطبيقات المطلوبة |
عامل الخدمة هو ملف JavaScript يتم تشغيله في الخلفية، بشكل منفصل عن صفحة الويب. وهو يعمل بمثابة وكيل شبكة قابل للبرمجة، حيث يعترض الطلبات ويمكّن الوظائف دون اتصال بالإنترنت، ودفع الإشعارات، ومزامنة الخلفية.
Installing → Installed → Activating → Activated → Idle → Terminated
│ │
└─── (new version detected) ──────────────┘
└─── Waiting (until all tabs close) ─────┘
// service-worker.js
const CACHE_NAME = 'my-pwa-v2';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html',
'/icons/icon-192.png'
];
// Install event — cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Caching app shell');
return cache.addAll(ASSETS_TO_CACHE);
})
);
// Force new service worker to activate immediately
self.skipWaiting();
});
// Activate event — clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
// Take control of all open pages immediately
clients.claim();
});
// Fetch event — intercept network requests
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// Return cached response if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(event.request).then(networkResponse => {
// Don't cache non-GET or error responses
if (!event.request.url.includes('/api/')) {
return networkResponse;
}
// Cache API responses for offline use
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return networkResponse;
}).catch(() => {
// Network failed — return offline fallback
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
return new Response('Offline', { status: 503 });
});
})
);
});
| استراتيجية | الوصف | حالة الاستخدام |
|---|---|---|
| ** ذاكرة التخزين المؤقت أولا ** | تحقق من ذاكرة التخزين المؤقت، وارجع إلى الشبكة | الأصول الثابتة (CSS، JS، الصور) |
| الشبكة أولا | حاول الشبكة، والعودة إلى ذاكرة التخزين المؤقت | مكالمات API، المحتوى الديناميكي |
| ** قديمة أثناء إعادة التحقق ** | إرجاع ذاكرة التخزين المؤقت والتحديث في الخلفية | خلاصات الأخبار، ومنشورات المدونة |
| الشبكة فقط | جلب دائما من الشبكة | البيانات الحساسة (المصرفية) |
| ** ذاكرة التخزين المؤقت فقط ** | لا يتم الجلب أبدًا من الشبكة | قذيفة التطبيق |
// Stale-while-revalidate strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
}).catch(() => cachedResponse);
return cachedResponse || fetchPromise;
})
);
});
البيان عبارة عن ملف JSON يتحكم في كيفية ظهور PWA عند تثبيته:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "An amazing PWA that works offline",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3367D6",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],
"lang": "en-US",
"dir": "ltr",
"prefer_related_applications": false
}
وأوضح الخصائص الظاهرة:
العرض: مستقل - يفتح بدون واجهة مستخدم المتصفح (شريط العناوين، علامات التبويب).العرض: ملء الشاشة - ملء الشاشة، بدون متصفح Chrome على الإطلاق.العرض: الحد الأدنى من واجهة المستخدم - الحد الأدنى من عناصر التحكم في المتصفح.النطاق: "/" — ما هي عناوين URL التي تعد جزءًا من PWA.start_url — الصفحة التي يتم فتحها عند تشغيل PWA.يحتاج عمال الخدمة إلى سياق آمن. HTTPS إلزامي من أجل:
# Redirect HTTP to HTTPS
# Nginx
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
غلاف التطبيق هو الحد الأدنى من HTML وCSS وJavaScript المطلوب لعرض واجهة المستخدم. يتم تخزينه مؤقتًا عند التحميل الأول ويعمل كأساس لـ PWA.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367D6">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/icons/icon-192.png" sizes="192x192">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<title>My PWA</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header id="app-header">My PWA</header>
<main id="app-content"></main>
<footer id="app-footer">© 2026</footer>
<!-- Register service worker -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW registration failed:', err));
});
}
</script>
<script src="/app.js"></script>
</body>
</html>
// app.js — Fetch content with offline support
const CACHE_KEY = 'content-cache';
async function loadContent(url) {
try {
const response = await fetch(url);
const data = await response.json();
// Cache for offline
const cache = await caches.open(CACHE_KEY);
cache.put(url, new Response(JSON.stringify(data)));
renderContent(data);
} catch (error) {
// Offline — load from cache
const cached = await caches.match(url);
if (cached) {
const data = await cached.json();
renderContent(data);
showOfflineIndicator();
} else {
showOfflineMessage();
}
}
}
// Request permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// Get push subscription
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
}
}
// service-worker.js — Handle push events
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
vibrate: [100, 50, 100],
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'open' || !event.action) {
clients.openWindow(event.notification.data.url);
}
});
| ميزة | PWA | التطبيق الأصلي |
|---|---|---|
| التوزيع | عنوان URL (لا يلزم وجود متجر) | متجر التطبيقات، جوجل بلاي |
| ** تثبيت الاحتكاك ** | نقرة واحدة (موجه المتصفح) | تنزيل وتثبيت والأذونات |
| حجم الملف | كيلو بايت إلى ميغابايت | من 10 ميجابايت إلى 500 ميجابايت+ |
| ** التحديثات ** | فوري (عامل الخدمة) | عبر مراجعة متجر التطبيقات |
| ** غير متصل ** | نعم (عامل الخدمة) | نعم (مدمج) |
| ** دفع الإخطارات ** | نعم | نعم |
| واجهات برمجة التطبيقات الخاصة بالجهاز | الكاميرا، GPS، التسارع، الدفع | الوصول الكامل إلى جميع واجهات برمجة التطبيقات الخاصة بالجهاز |
| بلوتوث | بلوتوث الويب (محدود) | واجهة برمجة تطبيقات بلوتوث كاملة |
| ** مهام الخلفية ** | مزامنة الخلفية (محدودة) | تنفيذ الخلفية الكاملة |
| الأداء | شبه أصلي (JavaScript/WebAssembly) | أصلي (الكود المترجم) |
| ** تحسين محركات البحث ** | قابلة للفهرسة بواسطة محركات البحث | غير قابلة للفهرسة |
| ** تكلفة التطوير ** | قاعدة تعليمات برمجية واحدة | iOS + Android (أو عبر الأنظمة الأساسية) |
| الاحتفاظ بالمستخدمين | أقل (أسهل للمغادرة) | أعلى (مثبت = ملتزم) |
| متري | قبل سلطة المياه الفلسطينية | بعد سلطة المياه الفلسطينية | تحسين |
|---|---|---|---|
| التحميل الأول | 5s | 1.2 ثانية | أسرع بنسبة 76% |
| الأحمال اللاحقة | 3ث | 0.3 ثانية | أسرع بنسبة 90% |
| ** معدل الارتداد ** | 45% | 20% | تخفيض 55% |
| معدل التحويل | 2% | 3.5% | زيادة 75% |
| الصفحات لكل جلسة | 3 | 5 | زيادة 66% |
نتائج العالم الحقيقي:
| الشركة | المقياس الرئيسي | تحسين |
|---|---|---|
| تويتر (X) لايت | التغريدات المرسلة لكل جلسة | زيادة 75% |
| بينتريست | الوقت الذي يقضيه | زيادة 40% |
| اوبر | إتمام الحجز | سلس حتى على 2G |
| ستاربكس | المستخدمين النشطين يوميا | زيادة 2x |
| علي بابا | معدل التحويل | زيادة 76% |
# Run Lighthouse from CLI
npx lighthouse https://example.com --view --preset=desktop
# PWA-specific checks:
# - ✅ Registers a service worker
# - ✅ Responds with 200 when offline
# - ✅ Web app manifest exists
# - ✅ Manifest has display: standalone
# - ✅ Served over HTTPS
# - ✅ Redirects HTTP to HTTPS
# - ✅ Configurable start URL
# - ✅ Icons provided (192px and 512px)
## PWA Pre-Flight Checklist
- [ ] HTTPS enabled
- [ ] Service worker registered and active
- [ ] Pages load offline (test with airplane mode)
- [ ] Manifest configured with correct icons
- [ ] "Add to Home Screen" prompt appears
- [ ] PWA opens in standalone mode with no address bar
- [ ] Splash screen displays correctly
- [ ] Push notifications work
- [ ] All pages are responsive (mobile, tablet, desktop)
- [ ] Performance budget met (Lighthouse score > 90)
- [ ] Cross-browser tested (Chrome, Firefox, Safari, Edge)
- [ ] Accessibility audit passes
// Register a sync event
async function scheduleSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-pending-data');
}
// Service worker handles sync
self.addEventListener('sync', event => {
if (event.tag === 'sync-pending-data') {
event.waitUntil(syncPendingData());
}
});
// Request periodic sync (requires user engagement)
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // Once per day
});
// Share content using native share sheet
async function shareContent(title, text, url) {
if (navigator.share) {
await navigator.share({ title, text, url });
} else {
// Fallback to custom share UI
copyToClipboard(url);
}
}
// Set app icon badge
if (navigator.setAppBadge) {
await navigator.setAppBadge(5); // Show "5" on icon
} else if (navigator.clearAppBadge) {
await navigator.clearAppBadge();
}
| مأزق | مشكلة | الحل |
|---|---|---|
| ** الإفراط في التخزين المؤقت ** | يقدم التطبيق محتوى قديمًا | ذاكرة التخزين المؤقت للإصدار، قم بتنفيذ ذاكرة التخزين المؤقت أولاً لـ Shell، والشبكة أولاً للمحتوى |
| ** لا يوجد احتياطي دون اتصال ** | شاشة بيضاء حاليا | قم دائمًا بتخزين /offline.html برسالة ذات معنى |
| ** فشل خرق ذاكرة التخزين المؤقت ** | عامل الخدمة القديم لا يزال قائما | استخدم SkipWaiting() وclients.claim() |
| ** مشاكل شاشة البداية ** | لون أو رمز المظهر خاطئ | قم بتكوين البيان بشكل صحيح، واستخدم الرموز القابلة للإخفاء |
| ** التوافق مع رحلات السفاري ** | بعض الميزات محدودة (الدفع، المزامنة الدورية) | التحسين التدريجي — التحلل برشاقة |
| حجم ذاكرة التخزين المؤقت كبير | تم تجاوز حصة التخزين (50 ميجابايت لكل أصل) | تقليم ذاكرة التخزين المؤقت القديمة، والحد من حجم ذاكرة التخزين المؤقت |
| start_url مفقود | تفتح الشاشة الرئيسية صفحة خاطئة | حدد دائمًا start_url في البيان |
تمثل تطبيقات الويب التقدمية أفضل ما في العالمين - الوصول إلى الويب وإمكانية الوصول إليه بالإضافة إلى الإمكانات وتجربة المستخدم للتطبيقات الأصلية.
لا تعد تطبيقات PWA بديلاً للتطبيقات الأصلية في كل السيناريوهات - ولكن بالنسبة لمعظم التطبيقات المستندة إلى المحتوى والتطبيقات المساعدة، فإنها توفر تجربة مستخدم فائقة مع جزء صغير من تكلفة التطوير.
لا توجد تعليقات معتمدة بعد. قد تنتظر الردود الجديدة المراجعة.