فراتر از SPA - معماری های جایگزین برای PWA شما

بیایید در مورد ... معماری صحبت کنیم؟

من قصد دارم یک موضوع مهم، اما احتمالاً اشتباه فهمیده شده را پوشش دهم: معماری که برای برنامه وب خود استفاده می‌کنید، و به طور خاص، اینکه چگونه تصمیمات معماری شما هنگام ساخت یک برنامه وب پیش‌رونده به کار می‌آیند.

«معماری» می‌تواند مبهم به نظر برسد، و ممکن است فوراً مشخص نباشد که چرا این موضوع اهمیت دارد. خب، یک راه برای فکر کردن در مورد معماری این است که از خودتان سوالات زیر را بپرسید: وقتی کاربری از صفحه‌ای در سایت من بازدید می‌کند، چه HTML بارگذاری می‌شود؟ و سپس، وقتی از صفحه دیگری بازدید می‌کند، چه چیزی بارگذاری می‌شود؟

پاسخ به این سؤالات همیشه ساده نیست و وقتی شروع به فکر کردن در مورد برنامه‌های وب پیش‌رونده می‌کنید، می‌توانند پیچیده‌تر هم شوند. بنابراین هدف من این است که شما را با یک معماری ممکن که آن را مؤثر یافتم، آشنا کنم. در طول این مقاله، تصمیماتی را که گرفته‌ام به عنوان «رویکرد من» برای ساخت یک برنامه وب پیش‌رونده نامگذاری خواهم کرد.

شما می‌توانید هنگام ساخت PWA خودتان از رویکرد من استفاده کنید، اما در عین حال، همیشه جایگزین‌های معتبر دیگری نیز وجود دارد. امید من این است که دیدن چگونگی کنار هم قرار گرفتن همه قطعات، الهام‌بخش شما باشد و احساس کنید که می‌توانید این را متناسب با نیازهای خود سفارشی کنید.

PWA سرریز پشته

برای تکمیل این مقاله، یک PWA از Stack Overflow ساختم. من زمان زیادی را صرف خواندن و مشارکت در Stack Overflow می‌کنم و می‌خواستم یک برنامه وب بسازم که مرور سوالات متداول برای یک موضوع خاص را آسان کند. این برنامه بر اساس API عمومی Stack Exchange ساخته شده است. متن‌باز است و می‌توانید با مراجعه به پروژه GitHub اطلاعات بیشتری کسب کنید.

برنامه‌های چند صفحه‌ای (MPA)

قبل از اینکه به جزئیات بپردازم، بیایید برخی اصطلاحات را تعریف کنیم و بخش‌هایی از فناوری زیربنایی را توضیح دهیم. ابتدا، قصد دارم چیزی را که دوست دارم "برنامه‌های چند صفحه‌ای" یا "MPA" بنامم، پوشش دهم.

MPA نامی شیک برای معماری سنتی است که از ابتدای وب مورد استفاده قرار گرفته است. هر بار که کاربر به یک URL جدید هدایت می‌شود، مرورگر به تدریج HTML مخصوص آن صفحه را رندر می‌کند. هیچ تلاشی برای حفظ وضعیت صفحه یا محتوای آن بین پیمایش‌ها وجود ندارد. هر بار که از یک صفحه جدید بازدید می‌کنید، از نو شروع می‌کنید.

این در تضاد با مدل برنامه تک صفحه‌ای (SPA) برای ساخت برنامه‌های وب است که در آن مرورگر کد جاوا اسکریپت را برای به‌روزرسانی صفحه موجود هنگام بازدید کاربر از یک بخش جدید اجرا می‌کند. هم SPA و هم MPA مدل‌های معتبری برای استفاده هستند، اما برای این پست، می‌خواستم مفاهیم PWA را در چارچوب یک برنامه چند صفحه‌ای بررسی کنم.

قابل اعتماد و سریع

شما عبارت «وب اپلیکیشن پیش‌رونده» یا PWA را از من (و افراد بی‌شماری) شنیده‌اید. ممکن است از قبل با برخی از مطالب پیش‌زمینه در جاهای دیگر این سایت آشنا باشید.

می‌توانید یک PWA را به عنوان یک برنامه وب در نظر بگیرید که یک تجربه کاربری درجه یک ارائه می‌دهد و واقعاً جایی در صفحه اصلی کاربر برای خود دست و پا می‌کند. کلمه " FIRE " مخفف کلمات Fast ( سریع)، I ntegrated (یکپارچه)، R Trusted (قابل اعتماد) و E engaging (جذاب) است که تمام ویژگی‌هایی را که هنگام ساخت یک PWA باید در نظر بگیرید، خلاصه می‌کند.

در این مقاله، قصد دارم روی زیرمجموعه‌ای از این ویژگی‌ها تمرکز کنم: سریع و قابل اعتماد .

سریع: اگرچه «سریع» در زمینه‌های مختلف معانی متفاوتی دارد، من قصد دارم مزایای سرعت بارگذاری هرچه کمتر از شبکه را بررسی کنم.

قابل اعتماد: اما سرعت خام کافی نیست. برای اینکه حس یک PWA را داشته باشید، برنامه وب شما باید قابل اعتماد باشد. باید به اندازه کافی مقاوم باشد تا همیشه چیزی را بارگذاری کند، حتی اگر فقط یک صفحه خطای سفارشی باشد، صرف نظر از وضعیت شبکه.

سرعت قابل اعتماد: و در نهایت، تعریف PWA را کمی تغییر می‌دهم و به معنای ساخت چیزی با سرعت قابل اعتماد نگاه می‌کنم. اینکه فقط زمانی که در یک شبکه با تأخیر کم هستید، سریع و قابل اعتماد باشید، کافی نیست. سرعت قابل اعتماد به این معنی است که سرعت برنامه وب شما صرف نظر از شرایط شبکه، ثابت باشد.

فناوری‌های فعال‌کننده: سرویس ورکرها + رابط برنامه‌نویسی کاربردی ذخیره‌سازی کش

PWAها سطح بالایی از سرعت و انعطاف‌پذیری را ارائه می‌دهند. خوشبختانه، این پلتفرم وب برخی از بلوک‌های سازنده را برای تحقق این نوع عملکرد ارائه می‌دهد. من به service workerها و Cache Storage API اشاره می‌کنم.

شما می‌توانید یک سرویس ورکر بسازید که به درخواست‌های ورودی گوش می‌دهد، برخی را به شبکه منتقل می‌کند و یک کپی از پاسخ را برای استفاده‌های بعدی، از طریق API ذخیره‌سازی کش، ذخیره می‌کند.

یک سرویس ورکر که از رابط برنامه‌نویسی کاربردی ذخیره‌سازی کش برای ذخیره یک کپی از پاسخ شبکه استفاده می‌کند.

دفعه‌ی بعدی که برنامه‌ی وب درخواست مشابهی را ارسال می‌کند، سرویس ورکر آن می‌تواند کش‌های خود را بررسی کند و پاسخ کش‌شده‌ی قبلی را برگرداند.

یک سرویس ورکر که از رابط برنامه‌نویسی کاربردی ذخیره‌سازی کش برای پاسخگویی استفاده می‌کند و شبکه را دور می‌زند.

اجتناب از اتصال به شبکه در هر زمان ممکن، بخش مهمی از ارائه عملکرد سریع و قابل اعتماد است.

جاوا اسکریپت "ایزومورفیک"

مفهوم دیگری که می‌خواهم به آن بپردازم چیزی است که گاهی اوقات به آن جاوااسکریپت «ایزومورفیک» یا «جهانی» گفته می‌شود. به عبارت ساده، این ایده است که یک کد جاوااسکریپت می‌تواند بین محیط‌های اجرایی مختلف به اشتراک گذاشته شود. وقتی PWA خود را ساختم، می‌خواستم کد جاوااسکریپت را بین سرور back-end و service worker به اشتراک بگذارم.

رویکردهای معتبر زیادی برای اشتراک‌گذاری کد به این روش وجود دارد، اما رویکرد من استفاده از ماژول‌های ES به عنوان کد منبع قطعی بود. سپس با استفاده از ترکیبی از Babel و Rollup ، آن ماژول‌ها را برای سرور و service worker ترنسپایل و باندل کردم. در پروژه من، فایل‌هایی با پسوند .mjs کدی هستند که در یک ماژول ES قرار دارند.

سرور

با در نظر داشتن این مفاهیم و اصطلاحات، بیایید به نحوه ساخت PWA Stack Overflow خود بپردازیم. من قصد دارم با پوشش سرور backend خود شروع کنم و توضیح دهم که چگونه آن در معماری کلی جای می‌گیرد.

من به دنبال ترکیبی از یک بک‌اند پویا به همراه هاستینگ استاتیک بودم و رویکرد من استفاده از پلتفرم Firebase بود.

توابع ابری فایربیس (Firebase Cloud Functions) به طور خودکار یک محیط مبتنی بر گره (Node-based) را در هنگام دریافت درخواست ایجاد می‌کنند و با چارچوب محبوب Express HTTP که من از قبل با آن آشنا بودم، ادغام می‌شوند. همچنین میزبانی آماده برای همه منابع استاتیک سایت من را ارائه می‌دهد. بیایید نگاهی به نحوه مدیریت درخواست‌ها توسط سرور بیندازیم.

وقتی یک مرورگر درخواست ناوبری به سرور ما ارسال می‌کند، جریان زیر را طی می‌کند:

مروری بر تولید پاسخ ناوبری، سمت سرور.

سرور درخواست را بر اساس URL مسیریابی می‌کند و از منطق قالب‌بندی برای ایجاد یک سند HTML کامل استفاده می‌کند. من از ترکیبی از داده‌های API Stack Exchange و همچنین قطعات HTML جزئی که سرور به صورت محلی ذخیره می‌کند، استفاده می‌کنم. به محض اینکه سرویس ورکر ما بداند چگونه پاسخ دهد، می‌تواند شروع به ارسال HTML به برنامه وب ما کند.

دو بخش از این تصویر ارزش بررسی دقیق‌تر را دارند: مسیریابی و قالب‌بندی.

مسیریابی

وقتی صحبت از مسیریابی می‌شود، رویکرد من استفاده از سینتکس مسیریابی بومی فریم‌ورک اکسپرس بود. این سینتکس به اندازه کافی انعطاف‌پذیر است تا پیشوندهای ساده URL و همچنین URLهایی که شامل پارامترها به عنوان بخشی از مسیر هستند را مطابقت دهد. در اینجا، من یک نگاشت بین نام‌های مسیر ایجاد می‌کنم که الگوی Express زیرین را برای مطابقت با آن در نظر می‌گیرد.

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

سپس می‌توانم مستقیماً از کد سرور به این نگاشت ارجاع دهم. وقتی الگویی برای یک الگوی Express داده شده وجود داشته باشد، کنترل‌کننده‌ی مناسب با منطق قالب‌بندی مختص مسیر منطبق پاسخ می‌دهد.

import routes from './lib/routes.mjs';
app.get(routes.get('index'), as>ync (req, res) = {
  // Templating logic.
});

قالب‌بندی سمت سرور

و منطق قالب‌بندی چگونه است؟ خب، من رویکردی را در پیش گرفتم که قطعات HTML ناقص را به ترتیب و یکی پس از دیگری کنار هم قرار می‌دهد. این مدل به خوبی با جریان‌سازی سازگار است.

سرور بلافاصله مقداری از کدهای HTML اولیه را ارسال می‌کند و مرورگر می‌تواند آن صفحه ناقص را فوراً رندر کند. همزمان با اینکه سرور بقیه منابع داده را کنار هم قرار می‌دهد، آنها را به مرورگر ارسال می‌کند تا سند کامل شود.

برای اینکه منظورم را بفهمید، به کد Express برای یکی از مسیرهای ما نگاهی بیندازید:

app.get(routes.get('index'), async (req>, res) = {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

با استفاده از متد write() شیء response و ارجاع به قالب‌های جزئی ذخیره‌شده محلی، می‌توانم جریان پاسخ را فوراً و بدون مسدود کردن هیچ منبع داده خارجی شروع کنم. مرورگر این HTML اولیه را دریافت می‌کند و بلافاصله یک رابط معنادار و پیام بارگذاری را رندر می‌کند.

بخش بعدی صفحه ما از داده‌های Stack Exchange API استفاده می‌کند. دریافت این داده‌ها به این معنی است که سرور ما باید یک درخواست شبکه ارسال کند. برنامه وب نمی‌تواند چیز دیگری را رندر کند تا زمانی که پاسخی دریافت و پردازش کند، اما حداقل کاربران در حین انتظار به یک صفحه خالی خیره نمی‌شوند.

زمانی که برنامه وب پاسخ را از Stack Exchange API دریافت کرد، یک تابع قالب‌بندی سفارشی را فراخوانی می‌کند تا داده‌ها را از API به HTML مربوطه ترجمه کند.

زبان قالب‌بندی

قالب‌بندی می‌تواند موضوعی شگفت‌آور و بحث‌برانگیز باشد، و آنچه من انتخاب کردم تنها یکی از رویکردهای موجود است. شما باید راه‌حل خودتان را جایگزین کنید، به‌خصوص اگر پیوندهای قدیمی با یک چارچوب قالب‌بندی موجود دارید.

چیزی که برای مورد استفاده من منطقی بود، تکیه بر template literals جاوا اسکریپت بود، به همراه مقداری منطق که به توابع کمکی تقسیم شده بود. یکی از نکات خوب در مورد ساخت MPA این است که لازم نیست به‌روزرسانی‌های حالت را پیگیری کنید و HTML خود را دوباره رندر کنید، بنابراین یک رویکرد اساسی که HTML استاتیک تولید می‌کرد، برای من کارساز بود.

بنابراین در اینجا مثالی از نحوه قالب‌بندی بخش HTML پویای فهرست برنامه وب من آورده شده است. همانند مسیرهای من، منطق قالب‌بندی در یک ماژول ES ذخیره می‌شود که می‌تواند هم به سرور و هم به سرویس ورکر وارد شود.

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}"< Qu>estions/h3`;
  cons<t form = `form me>tho<d=&qu>ot;GET".../form`;
  const questionCards = i>tems
    .map(item =
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('&<#39;);
  const que>stions = `div id<=&qu>ot;questions"${questionCards}/div`;
  return title + form + questions;
}

این توابع قالب، جاوا اسکریپت خالص هستند و در صورت لزوم، تقسیم منطق به توابع کمکی کوچک‌تر مفید است. در اینجا، من هر یک از آیتم‌های برگردانده شده در پاسخ API را به یکی از این توابع ارسال می‌کنم که یک عنصر HTML استاندارد با تمام ویژگی‌های مناسب ایجاد می‌کند.

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url=>"${<qu>estionUrl(id)}"${title}/a`;
}

نکته‌ی قابل توجه ، یک ویژگی داده‌ای است که به هر لینک اضافه می‌کنم، data-cache-url ، که روی URL API Stack Exchange تنظیم شده است تا سوال مربوطه را نمایش دهد. این را در نظر داشته باشید. بعداً دوباره به آن خواهم پرداخت.

برگردیم به کنترل‌کننده‌ی مسیر ، وقتی قالب‌بندی کامل شد، بخش پایانی HTML صفحه‌ام را به مرورگر ارسال می‌کنم و به این ارسال پایان می‌دهم. این نشانه برای مرورگر است که رندرینگ پیش‌رونده کامل شده است.

app.get(routes.get('index'), async (req>, res) = {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

خب، این خلاصه‌ای از تنظیمات سرور من بود. کاربرانی که برای اولین بار از برنامه وب من بازدید می‌کنند، همیشه از سرور پاسخی دریافت می‌کنند، اما وقتی بازدیدکننده دوباره به برنامه وب من مراجعه می‌کند، سرویس ورکر من شروع به پاسخگویی می‌کند. بیایید به جزئیات بیشتر بپردازیم.

کارگر خدماتی

مروری بر تولید پاسخ ناوبری، در سرویس ورکر.

این نمودار باید آشنا به نظر برسد - بسیاری از همان بخش‌هایی که قبلاً پوشش داده‌ام، اینجا با چیدمانی کمی متفاوت آمده‌اند. بیایید با در نظر گرفتن سرویس ورکر، جریان درخواست را بررسی کنیم.

سرویس ورکر ما یک درخواست ناوبری ورودی برای یک URL مشخص را مدیریت می‌کند و درست مانند سرور من، از ترکیبی از منطق مسیریابی و قالب‌بندی برای تشخیص نحوه پاسخ‌دهی استفاده می‌کند.

رویکرد مانند قبل است، اما با مقادیر اولیه سطح پایین متفاوت، مانند fetch() و API ذخیره‌سازی کش . من از این منابع داده برای ساخت پاسخ HTML استفاده می‌کنم که سرویس ورکر آن را به برنامه وب ارسال می‌کند.

جعبه کار

به جای شروع از ابتدا با مقادیر اولیه سطح پایین، قصد دارم سرویس ورکر خود را بر روی مجموعه‌ای از کتابخانه‌های سطح بالا به نام Workbox بسازم. این کتابخانه پایه محکمی برای منطق ذخیره‌سازی، مسیریابی و تولید پاسخ هر سرویس ورکر فراهم می‌کند.

مسیریابی

درست مانند کد سمت سرور، سرویس ورکر من باید بداند چگونه یک درخواست ورودی را با منطق پاسخ مناسب مطابقت دهد.

رویکرد من این بود که هر مسیر Express را با استفاده از یک کتابخانه مفید به نام regexparam به یک عبارت منظم مربوطه ترجمه کنم . پس از انجام این ترجمه، می‌توانم از پشتیبانی داخلی Workbox برای مسیریابی عبارات منظم بهره ببرم.

بعد از وارد کردن ماژولی که عبارات منظم را دارد، هر عبارت منظم را در روتر Workbox ثبت می‌کنم. در داخل هر مسیر می‌توانم منطق قالب‌بندی سفارشی را برای تولید پاسخ ارائه دهم. قالب‌بندی در service worker کمی پیچیده‌تر از سرور backend من است، اما Workbox در بسیاری از کارهای سنگین کمک می‌کند.

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

ذخیره سازی استاتیک دارایی ها

یکی از بخش‌های کلیدی داستان قالب‌بندی، اطمینان از این است که قالب‌های HTML جزئی من از طریق API ذخیره‌سازی Cache به صورت محلی در دسترس هستند و هنگام اعمال تغییرات در برنامه وب، به‌روز نگه داشته می‌شوند. نگهداری حافظه پنهان (cache) در صورت انجام دستی می‌تواند مستعد خطا باشد، بنابراین من به Workbox مراجعه می‌کنم تا پیش‌ذخیره‌سازی (precaching) را به عنوان بخشی از فرآیند ساخت خود مدیریت کنم.

من با استفاده از یک فایل پیکربندی به Workbox می‌گویم که کدام URLها را از قبل ذخیره کند (precache) که به دایرکتوری حاوی تمام فایل‌های محلی من به همراه مجموعه‌ای از الگوها برای مطابقت اشاره می‌کند. این فایل به طور خودکار توسط CLI Workbox خوانده می‌شود، که هر بار که سایت را بازسازی می‌کنم، اجرا می‌شود.

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox از محتوای هر فایل یک snapshot می‌گیرد و به طور خودکار آن لیست URLها و اصلاحات را به فایل نهایی service worker من تزریق می‌کند. Workbox اکنون هر آنچه را که برای در دسترس بودن و به‌روزرسانی همیشگی فایل‌های از پیش ذخیره شده نیاز دارد، در اختیار دارد. نتیجه، یک فایل service-worker.js است که حاوی چیزی شبیه به موارد زیر است:

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

برای افرادی که از فرآیند ساخت پیچیده‌تری استفاده می‌کنند، Workbox علاوه بر رابط خط فرمان، هم افزونه‌ی webpack و هم یک ماژول عمومی گره دارد.

پخش جریانی

در مرحله بعد، می‌خواهم سرویس ورکر آن HTML جزئی از پیش ذخیره شده را فوراً به برنامه وب برگرداند. این بخش مهمی از «سرعت قابل اعتماد» است - من همیشه بلافاصله چیزی معنی‌دار روی صفحه می‌بینم. خوشبختانه، استفاده از Streams API در سرویس ورکر ما این امکان را فراهم می‌کند.

شاید قبلاً در مورد Streams API شنیده باشید. همکار من، جیک آرچیبالد، سال‌هاست که از آن تعریف می‌کند. او پیش‌بینی جسورانه‌ای کرد که سال ۲۰۱۶ سال پخش آنلاین خواهد بود. و Streams API امروز به همان اندازه دو سال پیش عالی است، اما با یک تفاوت اساسی.

در حالی که در آن زمان فقط کروم از Streams پشتیبانی می‌کرد، API Streams اکنون به طور گسترده‌تری پشتیبانی می‌شود . داستان کلی مثبت است و با کد جایگزین مناسب، هیچ چیز مانع استفاده از Streams در Service Worker شما در حال حاضر نمی‌شود.

خب... ممکن است یک چیز مانع شما شود، و آن این است که سرتان در مورد نحوه‌ی کار API استریمز (Streams API) گیج شده باشد. این API مجموعه‌ای بسیار قدرتمند از مقادیر اولیه را در اختیار شما قرار می‌دهد و توسعه‌دهندگانی که در استفاده از آن راحت هستند می‌توانند جریان‌های داده‌ی پیچیده‌ای مانند موارد زیر ایجاد کنند:

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

اما درک کامل مفاهیم این کد ممکن است برای همه ممکن نباشد. به جای تجزیه و تحلیل این منطق، بیایید در مورد رویکرد من در مورد جریان‌سازی کارگران سرویس صحبت کنیم.

من از یک بسته‌بندی سطح بالا و کاملاً جدید به workbox-streams استفاده می‌کنم. با استفاده از آن، می‌توانم آن را در ترکیبی از منابع استریمینگ، چه از حافظه‌های پنهان و چه از داده‌های زمان اجرا که ممکن است از شبکه بیایند، ارسال کنم. Workbox وظیفه هماهنگ کردن منابع منفرد و ترکیب آنها در یک پاسخ استریمینگ واحد را بر عهده دارد.

علاوه بر این، Workbox به طور خودکار تشخیص می‌دهد که آیا Streams API پشتیبانی می‌شود یا خیر، و وقتی پشتیبانی نمی‌شود، یک پاسخ معادل و غیر استریمینگ ایجاد می‌کند. این بدان معناست که لازم نیست نگران نوشتن fallback باشید، زیرا استریم‌ها به پشتیبانی ۱۰۰٪ مرورگر نزدیک‌تر شده‌اند.

ذخیره سازی در زمان اجرا

بیایید بررسی کنیم که سرویس ورکر من چگونه با داده‌های زمان اجرا، از API Stack Exchange، برخورد می‌کند. من از پشتیبانی داخلی Workbox برای استراتژی ذخیره‌سازی stale-while-revalidate به همراه expire استفاده می‌کنم تا مطمئن شوم که فضای ذخیره‌سازی برنامه وب به طور نامحدود رشد نمی‌کند.

من دو استراتژی در Workbox تنظیم کردم تا منابع مختلفی را که پاسخ جریان را تشکیل می‌دهند، مدیریت کنند. Workbox با چند فراخوانی تابع و پیکربندی به ما امکان می‌دهد کاری را انجام دهیم که در حالت عادی به صدها خط کد دست‌نویس نیاز دارد.

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

اولین استراتژی، داده‌هایی را می‌خواند که از قبل ذخیره شده‌اند، مانند قالب‌های HTML ناقص ما.

استراتژی دیگر، منطق ذخیره‌سازی stale-while-revalidate را به همراه انقضای حافظه پنهان کم‌استفاده‌شده (last-recently-used cache expire) پس از رسیدن به ۵۰ ورودی پیاده‌سازی می‌کند.

حالا که این استراتژی‌ها را در جای خود قرار داده‌ام، تنها کاری که باقی مانده این است که به Workbox بگویم چگونه از آنها برای ساخت یک پاسخ کامل و جریانی استفاده کند. من آرایه‌ای از منابع را به عنوان توابع ارسال می‌کنم و هر یک از این توابع بلافاصله اجرا می‌شوند. Workbox نتیجه را از هر منبع می‌گیرد و آن را به ترتیب به برنامه وب جریان می‌دهد و فقط در صورتی که تابع بعدی در آرایه هنوز کامل نشده باشد، تأخیر ایجاد می‌کند.

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'})>,
  () = cacheStrategy.makeRequest({request: '/navbar.html'}),
  async >({event, url}) = {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, >data.items);
  },
  () = cacheStrategy.makeRequest({request: '/foot.html'}),
]);

دو منبع اول، قالب‌های جزئی از پیش ذخیره‌شده هستند که مستقیماً از API ذخیره‌سازی Cache خوانده می‌شوند، بنابراین همیشه بلافاصله در دسترس خواهند بود. این تضمین می‌کند که پیاده‌سازی service worker ما در پاسخ به درخواست‌ها، درست مانند کد سمت سرور من، به طور قابل اعتمادی سریع خواهد بود.

تابع منبع بعدی ما داده‌ها را از API Stack Exchange دریافت می‌کند و پاسخ را به HTML مورد انتظار برنامه وب پردازش می‌کند.

استراتژی stale-while-revalidate به این معنی است که اگر من قبلاً پاسخی را برای این فراخوانی API در حافظه پنهان داشته باشم، می‌توانم آن را فوراً به صفحه منتقل کنم، در حالی که ورودی حافظه پنهان را "در پس‌زمینه" برای دفعه بعدی که درخواست می‌شود، به‌روزرسانی می‌کنم.

در نهایت، یک کپی ذخیره‌شده از پاورقی‌ام را پخش می‌کنم و آخرین تگ‌های HTML را می‌بندم تا پاسخ کامل شود.

اشتراک‌گذاری کد، همه چیز را هماهنگ نگه می‌دارد

متوجه خواهید شد که بخش‌های خاصی از کد سرویس ورکر آشنا به نظر می‌رسند. HTML جزئی و منطق قالب‌بندی مورد استفاده توسط سرویس ورکر من با آنچه که کنترل‌کننده سمت سرور من استفاده می‌کند، یکسان است. این اشتراک‌گذاری کد تضمین می‌کند که کاربران، چه برای اولین بار از برنامه وب من بازدید کنند و چه به صفحه‌ای که توسط سرویس ورکر رندر شده است، بازگردند، تجربه‌ای یکسان و ثابت داشته باشند. این زیبایی جاوا اسکریپت ایزومورفیک است.

پیشرفت‌های پویا و پیش‌رونده

من هم سرور و هم سرویس ورکر (service worker) مربوط به PWA خودم را بررسی کرده‌ام، اما یک نکته‌ی منطقی دیگر هم هست که باید به آن بپردازم: مقدار کمی جاوا اسکریپت روی هر یک از صفحات من، پس از اینکه به طور کامل بارگذاری شدند، اجرا می‌شود.

این کد به تدریج تجربه کاربری را بهبود می‌بخشد، اما حیاتی نیست - اگر برنامه وب اجرا نشود، همچنان کار خواهد کرد.

فراداده صفحه

برنامه من از JavaScipt سمت کلاینت برای به‌روزرسانی متادیتای یک صفحه بر اساس پاسخ API استفاده می‌کند. از آنجا که من از همان بیت اولیه HTML ذخیره شده برای هر صفحه استفاده می‌کنم، برنامه وب در نهایت با برچسب‌های عمومی در head سند من مواجه می‌شود. اما از طریق هماهنگی بین کد قالب‌بندی و سمت کلاینت، می‌توانم عنوان پنجره را با استفاده از متادیتای خاص صفحه به‌روزرسانی کنم.

به عنوان بخشی از کد قالب‌بندی ، رویکرد من این است که یک تگ اسکریپت حاوی رشته escape شده به درستی اضافه کنم.

const metadataScript = `<script>
  self._title = '${escape(item.title)<}';>
/script`;

سپس، به محض اینکه صفحه من بارگذاری شد ، آن رشته را می‌خوانم و عنوان سند را به‌روزرسانی می‌کنم.

if (self._title) {
  document.title = unescape(self._title);
}

اگر می‌خواهید بخش‌های دیگری از فراداده‌های مختص صفحه را در برنامه وب خود به‌روزرسانی کنید، می‌توانید همین رویکرد را دنبال کنید.

تجربه کاربری آفلاین

یکی دیگر از بهبودهای تدریجی که اضافه کرده‌ام، برای جلب توجه به قابلیت‌های آفلاین ما استفاده می‌شود. من یک PWA قابل اعتماد ساخته‌ام و می‌خواهم کاربران بدانند که وقتی آفلاین هستند، همچنان می‌توانند صفحات قبلاً بازدید شده را بارگیری کنند.

ابتدا، من از API ذخیره‌سازی کش برای دریافت لیستی از تمام درخواست‌های API که قبلاً کش شده‌اند استفاده می‌کنم و آن را به لیستی از URLها ترجمه می‌کنم.

آن ویژگی‌های داده‌ای ویژه‌ای که در موردشان صحبت کردم را به خاطر دارید، که هر کدام شامل URL مربوط به درخواست API مورد نیاز برای نمایش یک سوال بودند؟ می‌توانم آن ویژگی‌های داده‌ای را با لیست URLهای ذخیره شده مقایسه کنم و آرایه‌ای از تمام لینک‌های سوالی که مطابقت ندارند، ایجاد کنم.

وقتی مرورگر وارد حالت آفلاین می‌شود، من فهرست لینک‌های ذخیره نشده را مرور می‌کنم و آن‌هایی را که کار نمی‌کنند، کم‌رنگ می‌کنم. به خاطر داشته باشید که این فقط یک راهنمایی بصری برای کاربر است که بداند از آن صفحات چه انتظاری باید داشته باشد - من در واقع لینک‌ها را غیرفعال نمی‌کنم یا مانع پیمایش کاربر نمی‌شوم.

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filte>r(card = {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandle>r = () = {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onli>neHandler = () = {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

مشکلات رایج

حالا نگاهی به رویکرد خودم برای ساخت یک PWA چندصفحه‌ای انداخته‌ام. عوامل زیادی وجود دارد که شما باید هنگام انتخاب رویکرد خودتان در نظر بگیرید و ممکن است در نهایت انتخاب‌های متفاوتی نسبت به من داشته باشید. این انعطاف‌پذیری یکی از نکات عالی در مورد ساخت برای وب است.

چند مشکل رایج وجود دارد که ممکن است هنگام تصمیم‌گیری‌های معماری خود با آنها مواجه شوید، و من می‌خواهم شما را از دردسر نجات دهم.

کل HTML را کش نکنید

من توصیه می‌کنم که کل اسناد HTML را در حافظه پنهان خود ذخیره نکنید. اولاً، این کار اتلاف فضا است. اگر برنامه وب شما از ساختار HTML پایه یکسانی برای هر یک از صفحات خود استفاده کند، در نهایت بارها و بارها کپی‌هایی از همان نشانه‌گذاری را ذخیره خواهید کرد.

مهم‌تر از آن، اگر تغییری در ساختار HTML مشترک سایت خود اعمال کنید، هر یک از آن صفحات ذخیره‌شده قبلی هنوز با طرح‌بندی قدیمی شما باقی می‌مانند. تصور کنید که یک بازدیدکننده‌ی برگشتی با دیدن ترکیبی از صفحات قدیمی و جدید چقدر ناامید می‌شود.

رانش سرور/کارگر سرویس

مشکل دیگری که باید از آن اجتناب کرد، عدم همگام‌سازی سرور و سرویس ورکر شماست. رویکرد من استفاده از جاوا اسکریپت ایزومورفیک بود، به طوری که کد یکسانی در هر دو مکان اجرا شود. بسته به معماری سرور فعلی شما، این همیشه امکان‌پذیر نیست.

صرف نظر از تصمیمات معماری که می‌گیرید، باید استراتژی‌ای برای اجرای کد مسیریابی و قالب‌بندی معادل در سرور و سرویس ورکر خود داشته باشید.

بدترین سناریوهای ممکن

طرح/طراحی ناهماهنگ

چه اتفاقی می‌افتد وقتی این مشکلات را نادیده می‌گیرید؟ خب، انواع و اقسام شکست‌ها ممکن است، اما بدترین حالت این است که یک کاربر دوباره از یک صفحه کش شده با طرح‌بندی بسیار قدیمی بازدید کند - شاید صفحه‌ای با متن هدر قدیمی یا صفحه‌ای که از نام‌های کلاس CSS استفاده می‌کند که دیگر معتبر نیستند.

بدترین حالت: مسیریابی خراب

از طرف دیگر، ممکن است کاربر به URL ای برخورد کند که توسط سرور شما مدیریت می‌شود، اما توسط سرویس ورکر شما مدیریت نمی‌شود. سایتی پر از طرح‌بندی‌های زامبی و بن‌بست، یک PWA قابل اعتماد نیست.

نکاتی برای موفقیت

اما شما تنها نیستید! نکات زیر می‌تواند به شما در جلوگیری از این مشکلات کمک کند:

از کتابخانه‌های قالب‌بندی و مسیریابی که پیاده‌سازی چندزبانه دارند استفاده کنید

سعی کنید از کتابخانه‌های قالب‌سازی و مسیریابی که پیاده‌سازی‌های جاوا اسکریپت دارند استفاده کنید. می‌دانم که هر توسعه‌دهنده‌ای این امکان را ندارد که از وب سرور و زبان قالب‌سازی فعلی خود مهاجرت کند.

اما تعدادی از چارچوب‌های قالب‌بندی و مسیریابی محبوب، پیاده‌سازی‌هایی به چندین زبان دارند. اگر بتوانید چارچوبی پیدا کنید که هم با جاوا اسکریپت و هم با زبان سرور فعلی شما کار کند، یک قدم به همگام‌سازی سرویس ورکر و سرور خود نزدیک‌تر شده‌اید.

قالب‌های ترتیبی را به قالب‌های تو در تو ترجیح دهید

در مرحله بعد، توصیه می‌کنم از مجموعه‌ای از قالب‌های متوالی استفاده کنید که می‌توانند یکی پس از دیگری به صورت جریانی (streamed) اجرا شوند. اشکالی ندارد اگر بخش‌های بعدی صفحه شما از منطق قالب‌بندی پیچیده‌تری استفاده کنند، تا زمانی که بتوانید بخش اولیه HTML خود را در اسرع وقت به صورت جریانی (streamed) اجرا کنید.

محتوای استاتیک و دینامیک را در سرویس ورکر خود کش کنید

برای بهترین عملکرد، باید تمام منابع استاتیک حیاتی سایت خود را از قبل ذخیره (precache) کنید. همچنین باید منطق ذخیره سازی زمان اجرا (runtime caching logic) را برای مدیریت محتوای پویا، مانند درخواست‌های API، تنظیم کنید. استفاده از Workbox به این معنی است که می‌توانید به جای پیاده‌سازی همه چیز از ابتدا، بر اساس استراتژی‌های آزمایش شده و آماده برای تولید، برنامه خود را بسازید.

فقط در مواقع ضروری، شبکه را مسدود کنید

و در همین رابطه، شما فقط باید زمانی که امکان پخش جریانی پاسخ از حافظه پنهان وجود ندارد، در شبکه بلاک کنید. نمایش فوری پاسخ API ذخیره شده اغلب می‌تواند منجر به تجربه کاربری بهتری نسبت به انتظار برای داده‌های جدید شود.

منابع