اضافه کردن PWA به وبسایت
اگر یک وب سایت دارید که میخواهید به صورت اپلیکیشن روی موبایل و دسکتاپ هم قابل استفاده باشد، میتونید از PWA یا Progressive Web App استفاده کنید. در اینجا یک نمونه کامل از فایلهای مورد نیاز برای اضافه کردن PWA به وبسایت قرار داده شده است که میتوانید از آن استفاده کنید.
همچنین در این کدها بخش نوتیفیکیشن و سینک در پسزمینه هم اضافه شده است.
اگر نسخه جدیدی از سرویسورکر منتشر شد، به کاربر اطلاع داده میشود و با کلیک روی دکمه بهروزرسانی، کشها پاک شده و نسخه جدید بارگذاری میشود. برای این کار کافی است در فایل sw ورژن خط اول را تغییر دهید
sw.js:
const CACHE_NAME = 'web-tools-v1.0.0';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/script.js',
'/favicon.png',
'/favicon.ico',
'/manifest.json'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// Send message to all clients when new version is ready
self.addEventListener('activate', (event) => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
}).then(() => {
// Notify all clients about the update
return self.clients.matchAll().then((clients) => {
clients.forEach(client => {
client.postMessage({
type: 'SW_UPDATED',
message: 'نسخه جدید در دسترس است'
});
});
});
})
);
return self.clients.claim();
});
// Listen for skip waiting message from page
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Fetch and cache strategy
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();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// Background sync event
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});
// Example function to sync data
async function syncData() {
console.log('Syncing data...');
}
// Push notification event
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'اعلان جدید',
icon: '/favicon.png',
badge: '/favicon.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
}
};
event.waitUntil(
self.registration.showNotification('ابزارهای وب', options)
);
});
// Notification click event
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});
script.js:
// PWA Install Prompt
let deferredPrompt;
const installPromptDismissed = localStorage.getItem('installPromptDismissed');
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
if (!installPromptDismissed) {
showInstallPrompt();
}
});
function showInstallPrompt() {
const prompt = document.createElement('div');
prompt.className = 'install-prompt';
prompt.innerHTML = `
<div class="install-prompt-text">
<div class="install-prompt-title">📱 نصب اپلیکیشن</div>
</div>
<button class="install-btn" id="installBtn">نصب</button>
<button class="close-install" id="closeInstall">✕</button>
`;
document.body.appendChild(prompt);
document.getElementById('installBtn').addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const {outcome} = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User accepted the install prompt');
}
deferredPrompt = null;
prompt.remove();
});
document.getElementById('closeInstall').addEventListener('click', () => {
localStorage.setItem('installPromptDismissed', 'true');
prompt.remove();
});
}
// Register Service Worker
if ('serviceWorker' in navigator) {
let newWorker;
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration);
// Check for updates periodically
setInterval(() => {
registration.update();
}, 60000); // Check every minute
// Listen for waiting worker
registration.addEventListener('updatefound', () => {
newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New service worker is ready
showUpdateNotification();
}
});
});
})
.catch(err => {
console.log('SW registration failed:', err);
});
// Listen for messages from service worker
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SW_UPDATED') {
showUpdateNotification();
}
});
// Handle controller change
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
});
// Show update notification
function showUpdateNotification() {
const notification = document.getElementById('updateNotification');
if (notification) {
notification.classList.remove('hidden');
notification.classList.add('show');
}
}
// Handle update button click
document.addEventListener('DOMContentLoaded', () => {
const updateButton = document.getElementById('updateButton');
const dismissButton = document.getElementById('dismissUpdate');
const notification = document.getElementById('updateNotification');
if (updateButton) {
updateButton.addEventListener('click', () => {
// Clear all caches and reload
if ('caches' in window) {
caches.keys().then(names => {
names.forEach(name => {
caches.delete(name);
});
}).then(() => {
// Tell the service worker to skip waiting
if (newWorker) {
newWorker.postMessage({type: 'SKIP_WAITING'});
} else {
window.location.reload();
}
});
} else {
window.location.reload();
}
});
}
if (dismissButton) {
dismissButton.addEventListener('click', () => {
notification.classList.remove('show');
notification.classList.add('hidden');
});
}
});
}
// Handle app installation
window.addEventListener('appinstalled', () => {
console.log('App installed successfully');
deferredPrompt = null;
});
manifest.json:
{
"id": "/",
"scope": "/",
"name": "Web Tools",
"short_name": "Web",
"description": "my description",
"start_url": "/",
"display": "fullscreen",
"background_color": "#0f1730",
"theme_color": "#7c5cff",
"orientation": "portrait-primary",
"lang": "fa",
"dir": "rtl",
"display_override": ["fullscreen", "standalone"],
"prefer_related_applications": false,
"related_applications": [],
"icons": [
{
"src": "/favicon.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "/favicon.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": [
"utilities",
"productivity",
"tools"
],
"screenshots": [
{
"src": "/screenshots/01.jpg",
"sizes": "1280x720",
"type": "image/jpeg",
"form_factor": "wide",
"label": "صفحه اصلی"
},
{
"src": "/screenshots/02.jpg",
"sizes": "540x720",
"type": "image/jpeg",
"form_factor": "narrow",
"label": "لیست ابزارها"
},
{
"src": "/screenshots/03.jpg",
"sizes": "540x720",
"type": "image/jpeg",
"form_factor": "narrow",
"label": "جزئیات ابزار"
}
],
"shortcuts": [
{
"name": "ابزارهای کاربردی",
"short_name": "ابزارها",
"description": "دسترسی سریع به ابزارهای کاربردی",
"url": "/",
"icons": [
{
"src": "/favicon.png",
"sizes": "192x192"
}
]
}
],
"share_target": {
"action": "/",
"method": "GET",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#7c5cff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Web Tools" />
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/favicon.ico" />
<link rel="manifest" href="/manifest.json" />
</head>
<body>
<!-- Update Notification -->
<div id="updateNotification" class="update-notification hidden">
<div class="update-content">
<span class="update-icon">🔄</span>
<div class="update-text">
<strong>نسخه جدید موجود است!</strong>
<p>برای استفاده از آخرین نسخه، صفحه را بهروزرسانی کنید.</p>
</div>
<button id="updateButton" class="update-button">بهروزرسانی</button>
<button id="dismissUpdate" class="dismiss-button" title="بستن">✕</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
.install-prompt {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: white;
padding: 14px 20px;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(124, 92, 255, .4);
display: flex;
align-items: center;
gap: 12px;
z-index: 1000;
animation: slideUp .3s ease;
max-width: calc(100% - 30px);
}
@keyframes slideUp {
from {
transform: translateX(-50%) translateY(100px);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
.install-prompt-text {
flex: 1;
min-width: 0;
}
.install-prompt-title {
font-weight: 600;
font-size: 15px;
margin-bottom: 2px;
}
.install-prompt-desc {
font-size: 13px;
opacity: .9;
}
.install-btn {
background: rgba(255, 255, 255, .25);
border: 1px solid rgba(255, 255, 255, .3);
color: white;
padding: 8px 16px;
border-radius: 10px;
cursor: pointer;
font-family: 'Vazirmatn', sans-serif;
font-weight: 600;
font-size: 14px;
transition: all .2s ease;
white-space: nowrap;
}
.install-btn:hover {
background: rgba(255, 255, 255, .35);
transform: scale(1.05);
}
.close-install {
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 20px;
padding: 4px;
line-height: 1;
opacity: .8;
transition: opacity .2s;
}
.close-install:hover {
opacity: 1;
}
.update-notification {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(150%);
z-index: 1000;
width: min(500px, calc(100% - 32px));
opacity: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.update-notification.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.update-notification.hidden {
display: block;
}
.update-content {
background: linear-gradient(135deg, var(--accent) 0%, #9b7fff 100%);
backdrop-filter: blur(20px);
border-radius: var(--radius);
padding: 20px 24px;
box-shadow: 0 12px 40px rgba(124, 92, 255, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 16px;
position: relative;
color: white;
}
.update-icon {
font-size: 32px;
flex-shrink: 0;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.update-text {
flex: 1;
}
.update-text strong {
display: block;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.update-text p {
margin: 0;
font-size: 14px;
opacity: 0.95;
line-height: 1.4;
}
.update-button {
background: white;
color: var(--accent);
border: none;
padding: 10px 20px;
border-radius: 12px;
font-family: 'Vazirmatn', sans-serif;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.update-button:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.3);
}
.update-button:active {
transform: scale(0.98);
}
.dismiss-button {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.2);
border: none;
width: 28px;
height: 28px;
border-radius: 8px;
color: white;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding: 0;
line-height: 1;
}
.dismiss-button:hover {
background: rgba(255, 255, 255, 0.3);
}
برای استفاده کافی است این کدها را به سایت فعلی خود اضافه کنید و نام اپ و آیکون را در فایل manifest تغییر دهید.
نمونه اپلیکیشن: