لنتحدّث عن... الهندسة المعمارية؟
سأتناول موضوعًا مهمًا، ولكن قد يساء فهمه، وهو بنية تطبيق الويب الذي تستخدمه، وتحديدًا، كيف تلعب قراراتك المتعلقة بالبنية دورًا عند إنشاء تطبيق ويب تقدّمي.
قد تبدو كلمة "بنية" غامضة، وقد لا يكون من الواضح على الفور سبب أهمية هذا المفهوم. حسنًا، إحدى الطرق التي يمكنك من خلالها التفكير في البنية هي طرح الأسئلة التالية على نفسك: عندما ينتقل مستخدم إلى صفحة على موقعي الإلكتروني، ما هو رمز HTML الذي يتم تحميله؟ وما الذي يتم تحميله عندما ينتقل المستخدم إلى صفحة أخرى؟
لا تكون الإجابات عن هذه الأسئلة واضحة دائمًا، وقد تصبح أكثر تعقيدًا عند البدء في التفكير في تطبيقات الويب التقدّمية. لذا، هدفي هو أن أشرح لك إحدى البُنى المحتملة التي وجدتها فعّالة. في هذه المقالة، سأشير إلى القرارات التي اتّخذتها على أنّها "أسلوبي" في إنشاء تطبيق ويب تقدّمي.
يمكنك استخدام طريقتي عند إنشاء تطبيق ويب تقدّمي خاص بك، ولكن في الوقت نفسه، تتوفّر دائمًا بدائل أخرى صالحة. آمل أن يلهمك فهم كيفية عمل كل هذه الأجزاء معًا، وأن تشعر بالقدرة على تخصيصها لتناسب احتياجاتك.
تطبيق Stack Overflow المتوافق مع الأجهزة الجوّالة
ولمرافقة هذه المقالة، أنشأتُ تطبيق ويب تقدّميًا على Stack Overflow. أقضي الكثير من الوقت في القراءة والمساهمة في Stack Overflow، وأردت إنشاء تطبيق ويب يسهّل تصفّح الأسئلة الشائعة حول موضوع معيّن. وهي تستند إلى واجهة برمجة التطبيقات العامة Stack Exchange API. وهو برنامج مفتوح المصدر، ويمكنك معرفة المزيد من المعلومات من خلال الانتقال إلى مشروع GitHub.
تطبيقات متعددة الصفحات (MPA)
قبل الانتقال إلى التفاصيل، لنعرّف بعض المصطلحات ونشرح بعض جوانب التكنولوجيا الأساسية. أولاً، سأشرح ما أسميه "تطبيقات متعددة الصفحات" أو "MPAs".
MPA هو اسم فاخر للهندسة المعمارية التقليدية المستخدَمة منذ بداية الويب. في كل مرة ينتقل فيها المستخدم إلى عنوان URL جديد، يعرض المتصفّح بشكل تدريجي رمز HTML خاصًا بتلك الصفحة. ولا تتم محاولة الحفاظ على حالة الصفحة أو المحتوى بين عمليات التنقّل. في كل مرة تنتقل فيها إلى صفحة جديدة، تبدأ من جديد.
يختلف ذلك عن نموذج التطبيق ذي الصفحة الواحدة (SPA) المخصّص لإنشاء تطبيقات الويب، حيث يشغّل المتصفّح رمز JavaScript لتعديل الصفحة الحالية عندما ينتقل المستخدم إلى قسم جديد. إنّ كلاً من التطبيقات ذات الصفحة الواحدة والتطبيقات المتعددة الصفحات هما نموذجان صالحان للاستخدام، ولكن في هذه المشاركة، أردت استكشاف مفاهيم تطبيقات الويب التقدّمية في سياق تطبيق متعدد الصفحات.
سرعة وموثوقية
لقد سمعتني (وغيري من الأشخاص) أستخدم عبارة "تطبيق ويب تقدّمي"، أو PWA. قد تكون على دراية ببعض المعلومات الأساسية في مكان آخر على هذا الموقع.
يمكنك اعتبار تطبيق الويب التقدّمي (PWA) تطبيق ويب يقدّم تجربة مستخدم ممتازة ويستحق أن يظهر على الشاشة الرئيسية للمستخدم. إنّ الاختصار FIRE، الذي يرمز إلى سريع ومتكامل وموثوق وجذّاب، يلخّص جميع السمات التي يجب التفكير فيها عند إنشاء تطبيق ويب تقدّمي.
في هذه المقالة، سأركّز على مجموعة فرعية من هذه السمات، وهي السرعة والموثوقية.
السرعة: مع أنّ مفهوم "السرعة" يختلف من سياق لآخر، سأشرح هنا مزايا السرعة التي يمكن تحقيقها من خلال تحميل أقل قدر ممكن من البيانات من الشبكة.
موثوقة: لا تكفي السرعة وحدها. لكي يبدو تطبيق الويب كتطبيق ويب تقدّمي، يجب أن يكون موثوقًا. يجب أن يكون التطبيق مرنًا بما يكفي لتحميل محتوى دائمًا، حتى لو كانت صفحة خطأ مخصّصة فقط، بغض النظر عن حالة الشبكة.
سرعة موثوقة: أخيرًا، سأعيد صياغة تعريف تطبيقات الويب التقدّمية قليلاً وأشرح ما يعنيه إنشاء تطبيق سريع وموثوق. لا يكفي أن تكون الشبكة سريعة وموثوقة فقط عندما تكون متصلاً بشبكة ذات وقت استجابة منخفض. أن يكون تطبيق الويب سريعًا بشكل موثوق يعني أن تكون سرعته ثابتة، بغض النظر عن ظروف الشبكة الأساسية.
التكنولوجيات المتاحة: عاملو الخدمة وواجهة برمجة التطبيقات Cache Storage API
تضع تطبيقات الويب التقدّمية معيارًا عاليًا للسرعة والمرونة. لحسن الحظ، توفّر منصة الويب بعض العناصر الأساسية التي تساعد في تحقيق هذا النوع من الأداء. أشير إلى برامج الخدمة وواجهة برمجة التطبيقات الخاصة بمساحة التخزين المؤقت.
يمكنك إنشاء عامل خدمة يستمع إلى الطلبات الواردة، وينقل بعضها إلى الشبكة، ويخزّن نسخة من الرد لاستخدامها في المستقبل، وذلك من خلال واجهة برمجة التطبيقات Cache Storage API.

في المرة التالية التي يرسل فيها تطبيق الويب الطلب نفسه، يمكن لمشغّل الخدمات التحقّق من ذاكرات التخزين المؤقت وإرجاع الرد المخزّن مؤقتًا سابقًا.

ويُعدّ تجنُّب استخدام الشبكة كلما أمكن ذلك جزءًا مهمًا من تقديم أداء سريع وموثوق.
JavaScript "المتشابهة"
أريد أيضًا أن أتحدث عن مفهوم آخر يُشار إليه أحيانًا باسم JavaScript "متشابه" أو "شامل". ببساطة، هي فكرة مفادها أنّه يمكن مشاركة رمز JavaScript نفسه بين بيئات تشغيل مختلفة. عندما أنشأتُ تطبيق الويب التقدّمي، أردتُ مشاركة رمز JavaScript بين خادم الخلفية ومشغّل الخدمات.
تتوفّر العديد من الطرق الصالحة لمشاركة الرموز بهذه الطريقة، ولكن طريقتي كانت استخدام وحدات ES كمصدر نهائي للرمز. بعد ذلك، قمت بتحويل هذه الوحدات إلى تنسيق آخر وتجميعها للخادم وخدمة العامل باستخدام مزيج من Babel وRollup. في مشروعي،
الملفات التي تحمل امتداد الملف .mjs
هي رموز برمجية مضمّنة في وحدة ES.
الخادم
مع أخذ هذه المفاهيم والمصطلحات في الاعتبار، لننتقل إلى كيفية إنشاء تطبيق الويب التقدّمي الخاص بي على Stack Overflow. سأبدأ بتغطية خادم الخلفية لدينا، وسأشرح كيف يتناسب ذلك مع البنية الشاملة.
كنت أبحث عن مزيج من الخلفية الديناميكية والاستضافة الثابتة، وكان أسلوبي هو استخدام منصة Firebase.
ستنشئ Firebase Cloud Functions تلقائيًا بيئة مستندة إلى Node عند تلقّي طلب وارد، وستتكامل مع إطار عمل Express HTTP الشائع الذي كنت على دراية به. ويوفّر أيضًا استضافة جاهزة للاستخدام لجميع الموارد الثابتة في موقعي الإلكتروني. لنلقِ نظرة على كيفية معالجة الخادم للطلبات.
عندما يرسل المتصفّح طلب انتقال إلى خادم Google، يمرّ الطلب بالخطوات التالية:

يوجه الخادم الطلب استنادًا إلى عنوان URL، ويستخدم منطق إنشاء النماذج لإنشاء مستند HTML كامل. أستخدم مزيجًا من البيانات من واجهة برمجة التطبيقات Stack Exchange، بالإضافة إلى أجزاء HTML جزئية يخزّنها الخادم محليًا. بعد أن يعرف عامل الخدمة كيفية الرد، يمكنه البدء في بث HTML مرة أخرى إلى تطبيق الويب.
هناك جزآن من هذه الصورة يستحقان الاستكشاف بمزيد من التفصيل: التوجيه وإنشاء النماذج.
يتم الآن تخطيط المسار
في ما يتعلق بالتوجيه، كان أسلوبي هو استخدام بنية التوجيه الأصلية في إطار عمل Express. وهي مرنة بما يكفي لتطابق بادئات عناوين URL البسيطة، بالإضافة إلى عناوين URL التي تتضمّن مَعلمات كجزء من المسار. في هذا المثال، أنشئ عملية ربط بين أسماء المسارات ونمط Express الأساسي الذي سيتم مطابقته.
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index&
#39;, '/'],
]);
export default routes;
يمكنني بعد ذلك الرجوع إلى عملية الربط هذه مباشرةً من رمز الخادم. عندما يتطابق نمط Express معيّن، يستجيب المعالج المناسب بمنطق إنشاء نماذج خاص بالمسار المتطابق.
import routes from './lib/routes.mjs';
app.get(routes.get('index'), as>ync (req, res) = {
// Templa
ting 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، يستدعي دالة إنشاء نماذج مخصّصة لترجمة البيانات من واجهة برمجة التطبيقات إلى ترميز HTML المقابل.
لغة إنشاء النماذج
قد يكون موضوع إنشاء النماذج مثيرًا للجدل بشكل مفاجئ، والطريقة التي اخترتها هي مجرد إحدى الطرق العديدة المتاحة. عليك استبدال الحلّ الحالي بحلّ آخر، خاصةً إذا كان لديك روابط قديمة بإطار عمل حالي للنماذج.
في حالة الاستخدام الخاصة بي، كان من المنطقي الاعتماد على السلاسل الحرفية النموذجية في JavaScript فقط، مع تقسيم بعض المنطق إلى دوال مساعدة. من المزايا الرائعة لتصميم تطبيق 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;
}
إنّ وظائف النماذج هذه هي JavaScript خالصة، ومن المفيد تقسيم المنطق إلى وظائف مساعدة أصغر عند الاقتضاء. في هذا المثال، أمرّر كل عنصر من العناصر التي تم عرضها في الردّ من واجهة برمجة التطبيقات إلى إحدى هذه الدوال، ما يؤدي إلى إنشاء عنصر HTML عادي مع ضبط جميع السمات المناسبة.
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url=>"${<qu>estio
nUrl(id)}"${title}/a`;
}
تجدر الإشارة إلى سمة البيانات
التي أضيفها إلى كل رابط، data-cache-url
، والتي تم ضبطها على عنوان 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
();
});
هذه جولة سريعة في عملية إعداد الخادم. عندما يزور المستخدمون تطبيق الويب الخاص بي للمرة الأولى، سيتلقّون دائمًا ردًا من الخادم، ولكن عندما يعود الزائر إلى تطبيق الويب الخاص بي، سيبدأ برنامج عامل الخدمة في الرد. لنبدأ.
مشغّل الخدمات

من المفترض أن يكون هذا المخطط مألوفًا، إذ يتضمّن العديد من العناصر نفسها التي سبق أن تناولتها، ولكن بترتيب مختلف قليلاً. لنستعرض معًا مسار الطلب مع أخذ عامل الخدمة في الاعتبار.
يتعامل عامل الخدمة مع طلب التنقّل الوارد لعنوان URL معيّن، وكما فعل الخادم، يستخدم مزيجًا من منطق التوجيه والقوالب لتحديد كيفية الاستجابة.
الطريقة هي نفسها كما في السابق، ولكن مع عناصر أساسية مختلفة منخفضة المستوى، مثل fetch()
وCache Storage API. أستخدم مصادر البيانات هذه لإنشاء استجابة HTML، والتي يعيدها عامل الخدمة إلى تطبيق الويب.
Workbox
بدلاً من البدء من الصفر باستخدام عناصر أساسية منخفضة المستوى، سأستخدم مجموعة من المكتبات العالية المستوى تسمى Workbox لإنشاء عامل الخدمة. ويوفّر أساسًا متينًا لأي منطق خاص بالتخزين المؤقت والتوجيه وإنشاء الردود في عامِل الخدمة.
يتم الآن تخطيط المسار
وكما هو الحال مع الرمز البرمجي من جهة الخادم، يجب أن يعرف عامل الخدمة كيفية مطابقة طلب وارد مع منطق الاستجابة المناسب.
كانت طريقتي هي
ترجمة
كل مسار Express إلى تعبير عادي مطابق،
باستخدام مكتبة مفيدة تُسمى
regexparam
. بعد إجراء هذه الترجمة، يمكنني الاستفادة من ميزة التوافق المضمّنة في Workbox مع توجيه التعبيرات العادية.
بعد استيراد الوحدة التي تتضمّن التعبيرات العادية، أسجّل كل تعبير عادي في موجّه Workbox. داخل كل مسار، يمكنني تقديم منطق نماذج مخصّص لإنشاء رد. تتطلّب عملية إنشاء النماذج في عامل الخدمة جهدًا أكبر مقارنةً بما كانت عليه في خادم الخلفية، ولكنّ Workbox يساعد في إنجاز الكثير من المهام الصعبة.
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templ
ating logic.
);
تخزين مواد العرض الثابتة في ذاكرة التخزين المؤقت
أحد الجوانب الرئيسية في عملية إنشاء النماذج هو التأكّد من أنّ نماذج HTML الجزئية متاحة محليًا من خلال واجهة برمجة التطبيقات Cache Storage API، ويتم تحديثها عند نشر تغييرات على تطبيق الويب. يمكن أن تكون عملية صيانة ذاكرة التخزين المؤقت عرضة للأخطاء عند إجرائها يدويًا، لذا أستخدم Workbox للتعامل مع التخزين المؤقت المسبق كجزء من عملية الإنشاء.
أحدّد لـ Workbox عناوين URL التي يجب تخزينها مؤقتًا مسبقًا باستخدام ملف إعداد يشير إلى الدليل الذي يحتوي على جميع مواد العرض المحلية بالإضافة إلى مجموعة من الأنماط التي يجب مطابقتها. يتم قراءة هذا الملف تلقائيًا من خلال واجهة سطر الأوامر في Workbox، التي يتم تشغيلها في كل مرة أعيد فيها إنشاء الموقع الإلكتروني.
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Othe
r options...
};
تأخذ Workbox لقطة من محتوى كل ملف، وتُدرج تلقائيًا قائمة عناوين URL والمراجعات في ملف عامل الخدمة النهائي. يتضمّن Workbox الآن كل ما يحتاجه لجعل الملفات المخزّنة مسبقًا متاحة دائمًا ومحدّثة. والنتيجة هي ملف service-worker.js
يحتوي على ما يشبه ما يلي:
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746
a9ecc6',
},
// etc.
]);
بالنسبة إلى المستخدمين الذين يستخدمون عملية إنشاء أكثر تعقيدًا، تتضمّن Workbox كلاً من المكوّن الإضافي webpack
ووحدة عقدة عامة، بالإضافة إلى واجهة سطر الأوامر.
البث
بعد ذلك، أريد أن يرسل مشغّل الخدمات جزء HTML المخزّن مؤقتًا إلى تطبيق الويب على الفور. وهذا جزء أساسي من تقديم تجربة "سريعة بشكل موثوق"، إذ أحصل دائمًا على محتوى مفيد على الشاشة على الفور. لحسن الحظ، يتيح استخدام Streams API ضمن عامل الخدمة إمكانية ذلك.
ربما سمعت عن Streams API من قبل. وقد أشاد بها زميلي "جيك أرشيبالد" لسنوات. وقد توقّع أن يكون عام 2016 هو عام البث المباشر على الويب. ولا تزال واجهة برمجة التطبيقات Streams API رائعة اليوم كما كانت قبل عامين، ولكن مع اختلاف أساسي.
في السابق، كان Chrome هو المتصفّح الوحيد الذي يتوافق مع Streams، ولكن أصبحت واجهة برمجة التطبيقات Streams متوافقة مع المزيد من المتصفّحات الآن. بشكل عام، تبدو الصورة إيجابية، ومع توفّر رمز احتياطي مناسب، لن يمنعك شيء من استخدام عمليات البث في مشغّل الخدمات اليوم.
حسنًا... قد يكون هناك شيء واحد يمنعك من ذلك، وهو عدم فهمك لطريقة عمل واجهة برمجة التطبيقات 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. أستفيد من ميزة التوافق المضمّنة في 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، بالإضافة إلى انتهاء صلاحية ذاكرة التخزين المؤقت الأقل استخدامًا عند الوصول إلى 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({reque
st: '/foot.html'}),
]);
أما المصدران الأول والثاني، فهما عبارة عن نماذج جزئية مخزَّنة مؤقتًا مسبقًا تتم قراءتها مباشرةً من واجهة برمجة التطبيقات Cache Storage، وبالتالي ستكون متاحة دائمًا على الفور. يضمن ذلك أنّ تنفيذ عامل الخدمة سيكون سريعًا بشكل موثوق في الاستجابة للطلبات، تمامًا مثل الرمز البرمجي من جهة الخادم.
تستردّ دالة المصدر التالية البيانات من واجهة برمجة التطبيقات Stack Exchange، وتعالج الردّ وتحوّله إلى HTML الذي يتوقّعه تطبيق الويب.
تعني استراتيجية "البيانات القديمة أثناء إعادة التحقّق " أنّه إذا كان لديّ ردّ مخزّن مؤقتًا من قبل لطلب البيانات من واجهة برمجة التطبيقات هذا، سأتمكّن من بثّه إلى الصفحة على الفور، مع تعديل إدخال ذاكرة التخزين المؤقت"في الخلفية" في المرة التالية التي يتم فيها طلب البيانات.
أخيرًا، أبث نسخة مخزّنة مؤقتًا من تذييلي وأغلق علامات HTML النهائية، لإكمال الرد.
يساعد رمز المشاركة في الحفاظ على مزامنة البيانات
ستلاحظ أنّ بعض أجزاء رمز عامل الخدمة تبدو مألوفة. إنّ ترميز HTML الجزئي ومنطق إنشاء النماذج اللذين يستخدمهما عامل الخدمة مطابقان لما يستخدمه معالج جهة الخادم. يضمن هذا الإجراء حصول المستخدمين على تجربة متّسقة، سواء كانوا يزورون تطبيق الويب الخاص بي للمرة الأولى أو يعودون إلى صفحة يعرضها عامل الخدمة. هذا هو جمال JavaScript المتشابهة.
تحسينات ديناميكية وتدريجية
لقد راجعتُ كلاً من الخادم وService Worker لتطبيق الويب التقدّمي، ولكن بقي جزء أخير من المنطق يجب تغطيته، وهو كمية صغيرة من JavaScript يتم تنفيذها على كل صفحة من صفحاتي بعد أن يتم بثها بالكامل.
يحسّن هذا الرمز تجربة المستخدم بشكل تدريجي، ولكنّه ليس ضروريًا، إذ سيظل تطبيق الويب يعمل حتى إذا لم يتم تنفيذه.
البيانات الوصفية للصفحة
يستخدم تطبيقي JavaScript من جهة العميل لتعديل البيانات الوصفية الخاصة بإحدى الصفحات استنادًا إلى استجابة واجهة برمجة التطبيقات. بما أنّني أستخدم الجزء الأوّلي نفسه من HTML المخزّن مؤقتًا لكل صفحة، ينتهي المطاف بتطبيق الويب بعلامات عامة في رأس المستند. ولكن من خلال التنسيق بين القوالب والرمز البرمجي من جهة العميل، يمكنني تعديل عنوان النافذة باستخدام البيانات الوصفية الخاصة بالصفحة.
كجزء من رمز إنشاء النماذج، أتبع أسلوبًا يتضمّن علامة نص برمجي تحتوي على السلسلة التي تمّت إزالة الأحرف الخاصة منها بشكل صحيح.
const metadataScript = `<script>
self._title = '${escape(item.title)<}';>
/s
cript`;
بعد تحميل صفحتي، أقرأ السلسلة وأعدّل عنوان المستند.
if (self._title) {
document.title = unescape(self._title);
}
إذا كانت هناك بيانات وصفية أخرى خاصة بالصفحة تريد تعديلها في تطبيق الويب الخاص بك، يمكنك اتّباع النهج نفسه.
تجربة المستخدم بلا إنترنت
التحسين التدريجي الآخر الذي أضفته يُستخدَم لجذب الانتباه إلى إمكاناتنا غير المتصلة بالإنترنت. لقد أنشأتُ تطبيق PWA موثوقًا، وأريد أن يعرف المستخدمون أنّه بإمكانهم تحميل الصفحات التي سبق لهم زيارتها حتى عندما يكونون غير متصلين بالإنترنت.
أولاً، أستخدم Cache Storage API للحصول على قائمة بجميع طلبات البيانات من واجهة برمجة التطبيقات التي تم تخزينها مؤقتًا، ثم أحوّل هذه القائمة إلى قائمة بعناوين URL.
تذكَّر سمات البيانات الخاصة التي تحدّثتُ عنها، والتي يحتوي كل منها على عنوان URL لطلب البيانات من واجهة برمجة التطبيقات اللازم لعرض سؤال. يمكنني الرجوع إلى سمات البيانات هذه ومقارنتها بقائمة عناوين 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.addEventListe
ner('offline', offlineHandler);
المشاكل الشائعة
لقد قدّمتُ الآن جولة حول طريقتي في إنشاء تطبيق ويب تقدّمي (PWA) متعدّد الصفحات. هناك العديد من العوامل التي يجب مراعاتها عند وضع منهجيتك الخاصة، وقد ينتهي بك الأمر إلى اتخاذ خيارات مختلفة عن تلك التي اتخذتها. وتُعد هذه المرونة إحدى الميزات الرائعة في تصميم تطبيقات الويب.
هناك بعض الأخطاء الشائعة التي قد تواجهها عند اتخاذ قراراتك المعمارية، وأريد أن أجنّبك بعض المشاكل.
عدم تخزين HTML الكامل مؤقتًا
أنصحك بعدم تخزين مستندات HTML كاملة في ذاكرة التخزين المؤقت. أولاً، إنّها مضيعة للمساحة. إذا كان تطبيق الويب يستخدِم بنية HTML أساسية مماثلة لكل صفحة من صفحاته، سينتهي بك الأمر بتخزين نُسخ من الترميز نفسه مرارًا وتكرارًا.
والأهم من ذلك، إذا نشرت تغييرًا على بنية HTML المشترَكة في موقعك الإلكتروني، ستظل كل صفحة من تلك الصفحات المخزّنة مؤقتًا سابقًا عالقة بالتصميم القديم. تخيَّل مدى الإحباط الذي سيشعر به الزائر المتكرّر عندما يرى مزيجًا من الصفحات القديمة والجديدة.
اختلاف بين الخادم وService Worker
هناك مشكلة أخرى يجب تجنُّبها وهي عدم مزامنة الخادم وService Worker. كانت طريقتي هي استخدام JavaScript المتماثل، حتى يتم تشغيل الرمز نفسه في كلا المكانين. قد لا يكون ذلك ممكنًا دائمًا، وذلك حسب بنية الخادم الحالية.
بغض النظر عن القرارات المتعلقة بالتصميم التي تتخذها، يجب أن تتوفّر لديك استراتيجية لتشغيل الرمز المكافئ للتوجيه وإنشاء النماذج في الخادم وفي مشغّل الخدمات.
سيناريوهات الحالة الأسوأ
التصميم أو التنسيق غير متسق
ماذا يحدث عند تجاهل هذه المشاكل؟ حسنًا، يمكن أن تحدث أنواع مختلفة من الأخطاء، ولكن أسوأ سيناريو هو أن يزور مستخدم متكرّر صفحة مخزّنة مؤقتًا بتصميم قديم جدًا، ربما صفحة تتضمّن نصًا قديمًا في العنوان أو تستخدم أسماء فئات CSS لم تعُد صالحة.
أسوأ سيناريو: تعذُّر التوجيه
بدلاً من ذلك، قد يعثر المستخدم على عنوان URL يتعامل معه الخادم، ولكن ليس عامل الخدمة. الموقع الإلكتروني المليء بالتصاميم غير النشطة والنهايات المسدودة ليس تطبيق ويب تقدّميًا موثوقًا.
نصائح لتحقيق النجاح
ولكنّك لست وحدك في هذا الأمر. يمكن أن تساعدك النصائح التالية في تجنُّب هذه المشاكل:
استخدام مكتبات النماذج والتوجيه التي تتضمّن عمليات تنفيذ بلغات متعددة
حاوِل استخدام مكتبات إنشاء النماذج والتوجيه التي تتضمّن عمليات تنفيذ JavaScript. أدرك أنّ بعض المطوّرين لا يمكنهم نقل بياناتهم من خادم الويب الحالي ولغة إنشاء النماذج.
ومع ذلك، يتوفّر عدد من أُطر العمل الشائعة الخاصة بالقوالب والتوجيه بلغات متعددة. إذا عثرت على أداة تعمل مع JavaScript بالإضافة إلى لغة الخادم الحالية، ستكون قد اقتربت خطوة واحدة من الحفاظ على مزامنة عامل الخدمة والخادم.
تفضيل النماذج المتسلسلة على النماذج المتداخلة
بعد ذلك، أنصحك باستخدام سلسلة من النماذج المتسلسلة التي يمكن بثها الواحدة تلو الأخرى. لا بأس إذا كانت الأجزاء اللاحقة من صفحتك تستخدم منطقًا أكثر تعقيدًا للنماذج، طالما يمكنك بث الجزء الأولي من HTML بأسرع ما يمكن.
تخزين المحتوى الثابت والديناميكي في ذاكرة التخزين المؤقت في عامل الخدمة
لتحقيق أفضل أداء، عليك التخزين المؤقت مسبقًا لجميع الموارد الثابتة المهمة في موقعك الإلكتروني. عليك أيضًا إعداد منطق التخزين المؤقت أثناء وقت التشغيل للتعامل مع المحتوى الديناميكي، مثل طلبات البيانات من واجهة برمجة التطبيقات. يعني استخدام Workbox أنّه يمكنك الاستفادة من استراتيجيات تم اختبارها جيدًا وجاهزة للاستخدام في مرحلة الإنتاج بدلاً من تنفيذها بالكامل من البداية.
الحظر على الشبكة فقط عند الضرورة القصوى
وفي ما يتعلّق بذلك، يجب الحظر على الشبكة فقط عندما يتعذّر بث رد من ذاكرة التخزين المؤقت. يمكن أن يؤدي عرض استجابة واجهة برمجة التطبيقات المخزّنة مؤقتًا على الفور إلى تحسين تجربة المستخدمين مقارنةً بانتظار بيانات جديدة.