طراحی تجربیات آفلاین قابل اعتماد برای برنامه های کاربردی وب مدرن

برنامه های کاربردی وب مدرن اغلب با این فرض طراحی می شوند که کاربران همیشه یک اتصال اینترنتی پایدار خواهند داشت. در واقعیت، قطع شدن شبکه امری رایج است. کاربر ممکن است در حین سفر ارتباط خود را قطع کند، سیگنال تلفن همراه ضعیفی داشته باشد یا برنامه ای را در مکانی با دسترسی محدود به اینترنت باز کند.
هنگامی که یک برنامه به طور کامل به درخواست های فعال شبکه بستگی دارد، حتی یک شکست موقت اتصال می تواند مانع از دسترسی کاربران به اطلاعات مهم یا تکمیل یک کار شود.
فناوری های برنامه کاربردی وب پیشرو (PWA) ابزارهایی را برای مقابله با این چالش ها فراهم می کند. با استفاده از سرویسدهندگان، APIهای کش و فضای ذخیرهسازی مرورگر، توسعهدهندگان میتوانند برنامههایی را طراحی کنند که در مواقعی که اتصال محدود یا موقتاً در دسترس نیست، مفید باقی میمانند.
ایجاد یک تجربه آفلاین به این معنی نیست که هر ویژگی باید بدون سرور اجرا شود. در عوض، هدف این است که تعیین کنیم کدام منابع و وظایف میتوانند بهصورت آفلاین در دسترس باقی بمانند، محدودیتها به وضوح ارتباط برقرار کنند، و هنگام بازیابی اتصال، اطلاعات را با خیال راحت همگامسازی کنند.
این آموزش به بررسی تکنیک های عملی برای ایجاد برنامه های کاربردی وب انعطاف پذیرتر با استفاده از فناوری های استاندارد مرورگر می پردازد.
1. درک کنید که پشتیبانی آفلاین واقعاً به چه معناست
پشتیبانی آفلاین یک ویژگی جداگانه مرورگر نیست. این ترکیبی از کش منابع، مدیریت داده، طراحی رابط و استراتژی های همگام سازی است.
به عنوان مثال، یک برنامه مدیریت وظایف را در نظر بگیرید. رابط کاربری، شیت ها و نمادهای آن را می توان به صورت محلی ذخیره کرد تا برنامه بدون اتصال به اینترنت باز شود. تکالیف دانلود شده قبلی نیز ممکن است برای خواندن در دسترس باقی بمانند.
با این حال، ایجاد یک کار جدید و همگام سازی آن با یک حساب راه دور نیاز به منطق بیشتری دارد. برنامه باید تصمیم بگیرد که چگونه تغییرات را به طور موقت ذخیره کند و در صورت عدم موفقیت همگام سازی چه کاری انجام دهد.
یک نقطه شروع مفید این است که عملکرد برنامه را به سه دسته تقسیم کنید:
-
منابع آماده آفلاین: فایل های ثابت مانند HTML، CSS، جاوا اسکریپت، فونت ها و آیکون ها.
-
داده های موجود در سایت: اطلاعات دانلود شده قبلی که کاربران آفلاین ممکن است به آن نیاز داشته باشند.
-
عملیات وابسته به شبکه: اقداماتی که به خدمات از راه دور نیاز دارند، مانند بازیابی قیمتهای واقعی یا پردازش پرداخت.
این تمایز به توسعه دهندگان کمک می کند تا انتظارات واقع بینانه ای داشته باشند. رابط ذخیره شده به طور خودکار همه ویژگی ها را به صورت آفلاین در دسترس قرار نمی دهد و داده های ذخیره شده محلی نباید به عنوان جایگزینی برای سوابق سرور قابل اعتماد در نظر گرفته شوند.
2. از Service Worker برای ذخیره منابع اصلی استفاده کنید
Service Worker یک فایل جاوا اسکریپت است که به طور جداگانه از صفحه اصلی اجرا می شود و می تواند درخواست های شبکه واجد شرایط را رهگیری کند. این پایه و اساس بسیاری از تجربیات آفلاین وب را فراهم می کند.
کارکنان خدمات معمولاً به یک زمینه ایمن مانند HTTPS نیاز دارند، اگرچه مرورگرها با localhost به عنوان یک زمینه توسعه امن برخورد می کنند.
مثال زیر یک پوسته برنامه کوچک را در حین نصب کش می کند.
یک فایل به نام ایجاد کنید sw.js:
const CACHE_NAME = "task-app-v1";
const APP_SHELL = [
"/",
"/index.html",
"/styles.css",
"/app.js",
"/offline.html"
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(APP_SHELL);
})
);
});
هنگامی که Service Worker نصب می شود، یک کش با نام را باز می کند و منابع لیست شده را ذخیره می کند.
را cache.addAll() اگر منبع مورد نیاز با موفقیت ذخیره نشود، روش را رد می کند. این رفتار به جلوگیری از موفقیت بیصدا نصب در زمانی که یک فایل مهم گم شده است کمک میکند.
مثال فرض می کند که هر منبع فهرست شده در مسیر مشخص شده وجود دارد. صفحه آفلاین باید یک فایل HTML واقعی باشد و پوشش برنامه باید حاوی منابع مورد نیاز برای ارائه رابط اصلی باشد.
سپس، سرویس کارگر را از فایل اصلی جاوا اسکریپت برنامه ثبت کنید:
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
await navigator.serviceWorker.register("/sw.js");
console.log("Service worker registered.");
} catch (error) {
console.error(
"Service worker registration failed:",
error
);
}
});
}
ثبت کارگر پس از بارگیری صفحه از تبدیل شدن تنظیمات سرویس کارگر به مسیر ارائه فوری صفحه جلوگیری می کند.
ثبت نام موفقیت آمیز لزوماً به این معنی نیست که کارگر قبلاً صفحه فعلی را کنترل می کند. نصب، فعال سازی و کنترل مراحل جداگانه ای در چرخه زندگی یک کارگر خدماتی است.
برای برنامه های تولید، توسعه دهندگان باید نسخه کش و پاکسازی را نیز برنامه ریزی کنند تا منابع قدیمی به طور نامحدود باقی نمانند.
3. زمانی که شبکه در دسترس نیست، منابع ذخیره شده را ارائه دهید
ذخیره فایل ها در حین نصب تنها بخشی از فرآیند است. برنامه همچنین باید تصمیم بگیرد که چگونه به درخواست ها رسیدگی کند.
استراتژی cache-first قبل از اقدام به درخواست شبکه، کش محلی را بررسی می کند. اغلب برای منابع ایستا نسخه شده مناسب است که در هر بازدید نیازی به رفرش ندارند.
کنترل کننده رویداد زیر را به sw.js:
self.addEventListener("fetch", (event) => {
const request = event.request;
const url = new URL(request.url);
if (
request.method !== "GET" ||
url.origin !== self.location.origin
) {
return;
}
if (url.pathname.startsWith("/api/")) {
return;
}
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request);
})
);
});
این کنترل کننده از پاسخ های ذخیره شده در حافظه پنهان برای مطابقت با درخواست های همان مبدا استفاده می کند. اگر منبع منطبق در کش نباشد، سعی می کند آن را از شبکه بازیابی کند.
درخواستهای API از این استراتژی خاص حذف میشوند، زیرا دادههای برنامه اغلب به قوانین متفاوتی برای تازگی و همگامسازی نیاز دارند.
یک محدودیت مهم وجود دارد: اگر منبعی در کش نباشد و شبکه در دسترس نباشد، درخواست شبکه ممکن است همچنان با شکست مواجه شود. یک پیاده سازی کامل تر ممکن است یک بازگشت آفلاین را برای درخواست های ناوبری مناسب برگرداند.
به عنوان مثال، ناوبری بازگشتی را می توان به کنترل کننده واکشی اضافه کرد:
event.respondWith(
caches.match(request).then(async (cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
try {
return await fetch(request);
} catch {
if (request.mode === "navigate") {
return (
await caches.match("/offline.html")
) ?? Response.error();
}
return Response.error();
}
})
);
از این بازگشت به جای قبلی استفاده کنید event.respondWith() مسدود کردن به جای یک کنترل کننده دوم برای همان درخواست.
بازگشت برای درخواست های ناوبری است. نباید یک صفحه HTML برای یک فایل جاوا اسکریپت، تصویر یا پاسخ API برگرداند، زیرا این منابع محتوای متفاوتی را انتظار دارند.
4. استراتژی ذخیره سازی متناسب با داده ها را انتخاب کنید
ابتدا ذخیره کردن در حافظه پنهان بهترین روش برای هر درخواستی نیست.
به عنوان مثال، یک وب سایت خبری ممکن است بخواهد آخرین مقالات ذخیره شده در حافظه پنهان را در حالی که سعی می کند محتوای به روز شده را از سرور بازیابی کند، نمایش دهد. برنامهای که مرتباً موجودی در حال تغییر را نمایش میدهد، ممکن است نیاز داشته باشد از ارائه دادههای قدیمی به عنوان فعلی اجتناب کند.
خط مشی شبکه تلاش می کند تا یک پاسخ جدید را بازیابی کند و زمانی که درخواست شبکه با شکست مواجه شد به یک کپی ذخیره شده برمی گردد.
یک پیاده سازی ساده شده به این صورت است:
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open("data-v1");
await cache.put(request, response.clone());
}
return response;
} catch (error) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
این کمک کننده می تواند توسط یک سرویس دهنده برای درخواست های GET مناسب استفاده شود.
پاسخهای موفق HTTP را در حافظه پنهان ذخیره میکند و در صورت رد درخواست شبکه، از یک پاسخ ذخیرهشده قبلی استفاده میکند. را response.clone() فراخوانی ضروری است زیرا بدنه های پاسخ دهنده جریان هایی هستند که معمولاً نمی توانند بیش از یک بار مصرف شوند.
با این حال، این استراتژی ساده شده انقضا، محدودیتهای اندازه حافظه پنهان یا تازه بودن ابرداده را اعمال نمیکند. پاسخ ذخیره شده در حافظه پنهان ممکن است حاوی اطلاعاتی باشد که دیگر معتبر نیستند.
بنابراین، توسعهدهندگان باید تصمیم بگیرند که چه مدت انواع مختلف دادهها میتوانند در حافظه پنهان باقی بمانند و آیا کاربران به نشانهای مبنی بر قدیمی بودن اطلاعات نیاز دارند یا خیر.
برای برنامه های احراز هویت شده، پاسخ های شخصی نیاز به توجه ویژه ای دارد. هرگز فرض نکنید که تمام پاسخ های API برای ذخیره در یک حافظه پنهان مشترک یا پایدار امن هستند.
5. اتصال را بدون وابستگی به آن کشف کنید
مرورگر فراهم می کند navigator.onLine مالکیت و رویدادهای آنلاین/آفلاین.
آنها می توانند به برنامه کمک کنند تا زمانی که مرورگر تغییر در اتصال را گزارش می دهد، رابط خود را به روز کند.
به عنوان مثال:
const connectionStatus = document.querySelector(
"#connection-status"
);
function updateConnectionStatus() {
connectionStatus.textContent = navigator.onLine
? "Connection available"
: "You may be offline";
}
window.addEventListener(
"online",
updateConnectionStatus
);
window.addEventListener(
"offline",
updateConnectionStatus
);
updateConnectionStatus();
در مثال فرض می شود که صفحه حاوی یک عنصر با شناسه است connection-status.
تفاوت بین اتصال مرورگر و در دسترس بودن خدمات واقعی مهم است. navigator.onLine فقط یک نکته مفید است ممکن است یک دستگاه به Wi-Fi متصل شود در حالی که روتر دسترسی به اینترنت ندارد، یا ممکن است یک API خاص حتی اگر وبسایتهای دیگر کار میکنند، در دسترس نباشد.
به همین دلیل، برنامه ها همچنان باید درخواست های شبکه ناموفق را مستقیماً رسیدگی کنند.
یک رابط کاربری مفید می تواند یک اعلان آفلاین کوچک را نمایش دهد و در عین حال به کاربران اجازه دهد به خواندن اطلاعات ذخیره شده در حافظه پنهان ادامه دهند. باید از ادعای موفقیت آمیز بودن عملیات صرفاً به این دلیل که مرورگر گزارش می دهد که اتصال بازیابی شده است خودداری کند.
6. تغییرات آفلاین را با خیال راحت ذخیره کنید
برخی از برنامهها باید در زمانی که کاربران آفلاین هستند از اقدامات پشتیبانی کنند.
تصور کنید یک کارگر میدانی از یک برنامه مدیریت وظیفه در منطقه ای با اتصال ضعیف استفاده می کند. یک کارگر باید بتواند بهروزرسانی را در یک کار بدون از دست دادن آن هنگام قطع شدن اتصال ذخیره کند.
برای داده های ساخت یافته، IndexedDB به طور کلی مناسب تر از اتکا به آن است localStorage به تنهایی IndexedDB از عملیات ناهمزمان پشتیبانی می کند و سوابق ساختاریافته را ذخیره می کند و آن را برای مجموعه های بزرگتر از داده های آفلاین مفید می کند.
یک مثال ساده از باز کردن پایگاه داده به شرح زیر است:
function openTaskDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("task-app", 1);
request.onupgradeneeded = () => {
const database = request.result;
if (!database.objectStoreNames.contains("pending")) {
database.createObjectStore("pending", {
keyPath: "id"
});
}
};
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
این کد یک فروشگاه شی با نام ایجاد می کند pending اولین باری که پایگاه داده مقداردهی اولیه می شود. را id ویژگی به عنوان یک کلید برای هر رکورد عمل می کند.
یک به روز رسانی آفلاین می تواند به عنوان یک شی حاوی یک شناسه منحصر به فرد، عملیات مورد نظر و اطلاعات مورد نیاز برای همگام سازی نمایش داده شود.
گام بعدی ذخیره این شی در پایگاه داده و همگام سازی مجدد در صورت لزوم خواهد بود.
این به چیزی بیش از کشف نیاز دارد online یک رویداد برنامه باید عملیات تکراری، احراز هویت منقضی شده، اعتبارسنجی سرور ناموفق و تغییرات متناقض را مدیریت کند.
به عنوان مثال، اگر زمانی که شما آفلاین هستید، یک کار در دو دستگاه مختلف ویرایش شود، برنامه به یک خط مشی حل تداخل نیاز دارد. بسته به محصول، این ممکن است شامل بررسی نسخه سمت سرور، ادغام فیلدهای مستقل یا درخواست از کاربر برای حل تغییرات متناقض باشد.
7. همگام سازی پس زمینه را به عنوان یک پیشرفت در نظر بگیرید
برخی از مرورگرها از همگامسازی پسزمینه پشتیبانی میکنند، که میتواند به کارمند سرویس اجازه دهد پس از بازیابی اتصال دوباره تلاش کند.
با این حال، پشتیبانی در مرورگرها و پلتفرم ها متفاوت است. این نباید تنها مکانیزمی باشد که مسئول ارائه داده های مهم کاربر است.
یک برنامه دائمی میتواند صفی از عملیات معلق را در IndexedDB نگه دارد و هنگام شروع برنامه یا زمانی که صفحه مربوطه فعال میشود، سعی در همگامسازی کند.
فرآیند همگام سازی باید:
عملیات در انتظار را از حافظه محلی واکشی کنید.
هر عملیات را به سرور ارسال کنید.
تنها پس از تأیید موفقیت سرور، عملیات را حذف کنید.
تلاش های ناموفق را برای امتحان مجدد بعدی ذخیره کنید.
خرابی های دائمی که نیاز به مداخله کاربر دارند را پوشش دهید.
برای عملیاتی که وضعیت سرور را تغییر می دهد، باطن باید از مکانیزم ضد تکراری مناسب نیز پشتیبانی کند. یک شناسه عملیات منحصر به فرد یا کلید عدم توانائی می تواند به جلوگیری از دوبار اعمال یک عمل بعد از یک نتیجه شبکه نامشخص کمک کند.
کلاینت هرگز نباید فرض کند که یک درخواست انتظار روی سرور شکست خورده است. سرور ممکن است عملیات را پردازش کرده باشد حتی اگر پاسخ به مرورگر نرسد.
8. تست رفتار آفلاین قبل از انتشار
اگر آزمایش فقط روی یک اتصال توسعه پایدار انجام شود، پشتیبانی آفلاین به راحتی قابل دست کم گرفتن است.
یک طرح آزمون عملی باید شامل حالات زیر باشد:
-
برنامه را به صورت آنلاین باز کنید سپس آن را با غیرفعال شدن شبکه بارگیری مجدد کنید.
-
از مسیری بازدید کنید که کش نیست.
-
درخواست داده هایی که هرگز به صورت محلی ذخیره نشده اند.
-
یک بهروزرسانی آفلاین ارسال کنید و دوباره وصل شوید.
-
شبیه سازی خرابی سرور، نه قطع کامل شبکه.
-
محتوای حافظه پنهان قدیمی و نسخه های به روز برنامه را آزمایش کنید.
-
بررسی کنید که همگامسازی ناموفق تغییرات معلق را حذف نکند.
-
رفتار مرورگرها و دستگاه های پشتیبانی شده توسط برنامه را بررسی کنید.
ابزارهای توسعهدهنده مرورگر میتوانند به شبیهسازی شرایط آفلاین و بررسی ثبتنامها و حافظه پنهان خدمات کاری کمک کنند.
توسعه دهندگان همچنین باید به خاطر داشته باشند که کش مرورگر را می توان پاک یا خارج کرد. نگهداری آفلاین جایگزین پشتیبان گیری سرور نیست و اطلاعات مهم نباید فقط در حافظه پنهان مرورگر وجود داشته باشد.
نتیجه گیری
تجارب آفلاین قابل اعتماد نیاز به تصمیم گیری های متفکرانه ای در مورد اینکه کدام منابع ذخیره شوند، کدام داده ها می توانند به صورت محلی ذخیره شوند و چگونه تغییرات باید همگام شوند، دارند.
کارکنان سرویس امکان رهگیری درخواست ها و ارائه منابع ذخیره شده را فراهم می کنند. API کش راهی برای ذخیره پاسخ ها فراهم می کند، در حالی که IndexedDB داده های آفلاین ساختار یافته را حفظ می کند. رویدادهای اتصال میتوانند بازخورد رابط را بهبود بخشند، اما درخواستهای واقعی همچنان باید بهعنوان عملیات بالقوه غیرقابل اعتماد تلقی شوند.
موثرترین استراتژی آفلاین به هدف برنامه بستگی دارد. یک وبسایت محتوا ممکن است خواندنهای ذخیرهشده را در اولویت قرار دهد، در حالی که یک برنامه بهرهوری ممکن است به یک صف دائمی از تغییرات معلق و یک فرآیند حل تعارض واضح نیاز داشته باشد.
با طراحی برای خرابی های شبکه از ابتدا، توسعه دهندگان می توانند برنامه های کاربردی وب بسازند که در موقعیت های بیشتری بدون گمراه کردن کاربران در مورد حفظ و نگهداری آن ها مفید باقی بمانند.

