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

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

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

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

پاسخ به این سؤالات همیشه ساده نیست، و هنگامی که شروع به فکر کردن در مورد برنامه های وب مترقی کنید، ممکن است پیچیده تر شوند. بنابراین هدف من این است که شما را از طریق یک معماری ممکن که به نظرم موثر بوده است راهنمایی کنم. در طول این مقاله، من تصمیماتی را که گرفتم به عنوان "رویکرد من" برای ساختن یک برنامه وب مترقی عنوان می کنم.

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

پشته سرریز PWA

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

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

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

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

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

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

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

شما می توانید PWA را به عنوان یک برنامه وب در نظر بگیرید که تجربه کاربری درجه یک را ارائه می دهد و واقعاً در صفحه اصلی کاربر جایگاهی را به دست می آورد. مخفف " FIRE " که مخفف F ast، I ntegrated، R eliable و E ngaging است، تمام ویژگی هایی را که باید در هنگام ساختن PWA به آنها فکر کنید خلاصه می کند.

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

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

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

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

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

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

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

یک سرویس دهنده که از API حافظه کش برای ذخیره یک کپی از پاسخ شبکه استفاده می کند.

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

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

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

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

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

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

سرور

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

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

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

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

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

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

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

مسیریابی

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

قالب سمت سرور

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

سرور فوراً مقداری 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();
});

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

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

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

زبان قالب

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

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

بنابراین در اینجا یک مثال از نحوه قالب بندی بخش 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;
}

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

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(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() و Cache Storage API . من از این منابع داده برای ساختن پاسخ HTML استفاده می‌کنم که سرویس‌گر آن را به برنامه وب ارسال می‌کند.

جعبه کار

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

مسیریابی

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

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

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

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

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

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

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

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

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

Workbox یک عکس فوری از محتویات هر فایل می گیرد و به طور خودکار آن لیست URL ها و ویرایش ها را به فایل سرویس کارگر نهایی من تزریق می کند. 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 شنیده باشید. همکار من جیک آرچیبالد سال هاست که مداحی های آن را می خواند. او پیش‌بینی جسورانه‌ای کرد که سال 2016 سال جریان‌های وب خواهد بود. و Streams API امروز نیز مانند دو سال پیش عالی است، اما با یک تفاوت اساسی.

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

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

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 پشتیبانی می‌شود یا خیر، و وقتی پشتیبانی نمی‌شود، پاسخی معادل و غیر جریانی ایجاد می‌کند. این بدان معنی است که شما نیازی به نگرانی در مورد نوشتن نسخه های بازگشتی ندارید، زیرا استریم ها به 100٪ پشتیبانی مرورگر نزدیک تر می شوند.

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

بیایید بررسی کنیم که سرویس‌کار من چگونه با داده‌های زمان اجرا از 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 جزئی ما.

استراتژی دیگر منطق ذخیره سازی قدیمی-در حالی که اعتبار مجدد را نشان می دهد، همراه با انقضای حافظه پنهان که اخیراً کمتر استفاده شده است، پس از رسیدن به 50 ورودی، پیاده سازی می شود.

اکنون که آن استراتژی‌ها را در اختیار دارم، تنها چیزی که باقی می‌ماند این است که به 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 ذخیره سازی کش خوانده می شوند، بنابراین همیشه فوراً در دسترس خواهند بود. این تضمین می‌کند که پیاده‌سازی کارگر خدمات ما در پاسخگویی به درخواست‌ها، درست مانند کد سمت سرور من، به‌طور قابل اعتمادی سریع خواهد بود.

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

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

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

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

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

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

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

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

فراداده صفحه

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

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

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

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

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

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

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

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

ابتدا از Cache Storage 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].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);

دام های رایج

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

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

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

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

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

رانش كارگر سرور/خدمات

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

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

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

طرح / طراحی ناسازگار

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

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

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

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

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

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

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

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

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

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

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

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

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

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

منابع