Beyond SPAs - alternative architectures for your PWA

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

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

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

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

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

Stack Overflow PWA

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

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

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

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

این برخلاف مدل برنامه تک‌صفحه‌ای (SPA) برای ساختن برنامه‌های وب است که در آن مرورگر کد جاوا اسکریپت را اجرا می‌کند تا وقتی کاربر از بخش جدیدی بازدید می‌کند، صفحه موجود را به‌روز کند. Both SPAs and MPAs are equally valid models to use, but for this post, I wanted to explore PWA concepts within the context of a multi-page app.

سرعت قابل‌اعتماد

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

می‌توانید PWA را به‌عنوان یک برنامه وب درنظر بگیرید که تجربه کاربری درجه یکی را ارائه می‌دهد، و واقعاً جایگاهی در صفحه اصلی کاربر به‌دست می‌آورد. سرواژه «FIRE» که مخفف Fast (سریع)، Integrated (یکپارچه)، Reliable (قابل‌اعتماد)، و Engaging (جذاب) است، همه ویژگی‌هایی را که باید هنگام ساختن PWA درنظر بگیرید خلاصه می‌کند.

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

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

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

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

فناوری‌های فعال‌سازی: Service Workers + Cache Storage API

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

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

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

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

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

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

جاوا اسکریپت «هم‌ریخت»

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

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

سرور

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

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

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

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

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

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

دو بخش از این تصویر ارزش بررسی با جزئیات بیشتر را دارد: مسیریابی، و الگوبرداری.

مسیریابی

وقتی نوبت به مسیریابی می‌رسد، رویکرد من استفاده از نحو مسیریابی بومی چارچوب Express بود. این روش به‌اندازه کافی انعطاف‌پذیر است تا با پیشوندهای ساده نشانی وب و همچنین نشانی‌های وبی که پارامترها را به‌عنوان بخشی از مسیر شامل می‌شوند، مطابقت داشته باشد. در اینجا، من یک نگاشت ایجاد می‌کنم بین نام‌های مسیر و الگوی 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'), async (req, res) => {
  // Templating logic.
});

قالب‌سازی سمت سرور

And what does that templating logic look like? خب، من از رویکردی استفاده کردم که قطعات ناقص 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 مربوطه ترجمه کند.

زبان الگو

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

چیزی که برای مورد استفاده من منطقی بود این بود که فقط به الگوهای تحت‌اللفظی جاوا اسکریپت تکیه کنم، با برخی منطق‌ها که به توابع کمکی تقسیم شده‌اند. One of the nice things about building an MPA is that you don't have to keep track of state updates and re-render your HTML, so a basic approach that produced static HTML worked for me.

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

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

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

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

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

با برگشتن به گرداننده مسیر، وقتی الگو کامل شد، بخش نهایی 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();
});

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

عامل خدماتی

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

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

Our service worker handles an incoming navigation request for a given URL, and just like my server did, it uses a combination of routing and templating logic to figure out how to respond.

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

Workbox

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

مسیریابی

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

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

After importing the module that has the regular expressions, I register each regular expression with Workbox's router. در هر مسیر می‌توانم منطق الگوهای سفارشی را برای تولید پاسخ ارائه دهم. Templating in the service worker is a bit more involved than it was in my backend server, but Workbox helps with a lot of the heavy lifting.

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

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

ذخیره موقت دارایی ایستا

One key part of the templating story is making sure that my partial HTML templates are locally available via the Cache Storage API, and are kept up to date when I deploy changes to the web app. Cache maintenance can be error-prone when done by hand, so I turn to Workbox to handle precaching as part of my build process.

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

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

Workbox از محتوای هر فایل عکس آنی می‌گیرد و به‌طور خودکار آن فهرست نشانی‌های وب و بازبینی‌ها را به فایل نهایی کارگر سرویس من تزریق می‌کند. Workbox now has everything it needs to make the precached files always available, and kept up to date. نتیجه یک فایل 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 امروز هم به همان اندازه دو سال پیش عالی است، اما با یک تفاوت مهم.

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

خب... شاید یک چیز مانع شما شود و آن هم درک نحوه عملکرد واقعی Streams 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 takes care of coordinating the individual sources and stitching them together into a single, streaming response.

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

ذخیره در حافظه نهان زمان اجرا

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

دو استراتژی در 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 را پیاده‌سازی می‌کند، همراه با انقضای حافظه پنهان least-recently-used وقتی به ۵۰ ورودی می‌رسیم.

اکنون که این استراتژی‌ها را در جای خود دارم، تنها چیزی که باقی مانده این است که به 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'}),
]);

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

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

The stale-while-revalidate strategy means that if I have a previously cached response for this API call, I'll be able to stream it to the page immediately, while updating the cache entry "in the background" for the next time it's requested.

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

هم‌رسانی کد باعث می‌شود همه چیز همگام بماند

متوجه خواهید شد که بخش‌های خاصی از کد کارگر سرویس آشنا به‌نظر می‌رسند. The partial HTML and templating logic used by my service worker is identical to what my server-side handler uses. این هم‌رسانی کد تضمین می‌کند که کاربران تجربه ثابتی داشته باشند، چه برای اولین‌بار از برنامه وب من بازدید کنند یا به صفحه‌ای که توسط کارگر سرویس ارائه شده است بازگردند. این زیبایی جاوا اسکریپت ایزومورفیک است.

بهبودهای پویا و پیش‌رونده

I've walked through both the server and service worker for my PWA, but there's one last bit of logic to cover: there's a small amount of JavaScript that runs on each of my pages, after they're fully streamed in.

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

فراداده صفحه

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

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

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

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

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

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

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

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

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

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

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

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].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

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

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

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

مشکلات رایج

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

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

HTML کامل را در حافظه پنهان ذخیره نکنید

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

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

انحراف سرور / کارگر سرویس

The other pitfall to avoid involves your server and service worker getting out of sync. رویکرد من استفاده از جاوا اسکریپت ایزومورفیک بود تا همان کد در هر دو مکان اجرا شود. بسته به معماری سرور موجود شما، این کار همیشه امکان‌پذیر نیست.

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

سناریوهای بدترین حالت

چیدمان / طراحی ناسازگار

اگر این تله‌ها را نادیده بگیرید چه اتفاقی می‌افتد؟ Well, all sorts of failures are possible, but the worst case scenario is that a returning user visits a cached page with a very stale layout—perhaps one with out of date header text, or that uses CSS class names that are no longer valid.

بدترین سناریو: مسیریابی خراب

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

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

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

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

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

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

الگوهای متوالی را به الگوهای تودرتو ترجیح دهید

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

ذخیره کردن محتوای ایستا و پویا در حافظه نهان در سرویس‌کار

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

فقط درصورت لزوم، در شبکه مسدود کنید

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

منابع