স্থাপত্য নিয়ে কথা বলা যাক?
আমি একটি গুরুত্বপূর্ণ, কিন্তু সম্ভাব্য ভুল বোঝাবুঝি বিষয় কভার করতে যাচ্ছি: আপনার ওয়েব অ্যাপের জন্য আপনি যে আর্কিটেকচার ব্যবহার করেন এবং বিশেষ করে, আপনি যখন একটি প্রগ্রেসিভ ওয়েব অ্যাপ তৈরি করছেন তখন আপনার আর্কিটেকচারাল সিদ্ধান্তগুলি কীভাবে কার্যকর হয়।
"আর্কিটেকচার" শব্দটা অস্পষ্ট মনে হতে পারে এবং এটি কেন গুরুত্বপূর্ণ তা অবিলম্বে স্পষ্ট নাও হতে পারে। আর্কিটেকচার সম্পর্কে চিন্তা করার একটি উপায় হল নিজেকে নিম্নলিখিত প্রশ্নগুলি জিজ্ঞাসা করা: যখন কোনও ব্যবহারকারী আমার সাইটের কোনও পৃষ্ঠা দেখেন, তখন কোন HTML লোড হয়? এবং তারপর, অন্য পৃষ্ঠায় গেলে কী লোড হয়?
এইসব প্রশ্নের উত্তর সবসময় সহজ হয় না এবং আপনি প্রোগ্রেসিভ ওয়েব অ্যাপ সম্পর্কে চিন্তাভাবনা শুরু করলে, সেগুলি আরও জটিল হয়ে যেতে পারে। তাই আমার লক্ষ্য হল, আমি যে আর্কিটেকচারটি কার্যকর বলে মনে করেছি, সেটি আপনাকে বুঝিয়ে দেওয়া। এই নিবন্ধ জুড়ে, আমি যে সিদ্ধান্তগুলি নিয়েছি সেগুলিকে প্রোগ্রেসিভ ওয়েব অ্যাপ তৈরির ক্ষেত্রে "আমার পদ্ধতি" হিসেবে লেবেল করব।
আপনার নিজস্ব PWA তৈরি করার সময় আপনি আমার পদ্ধতি ব্যবহার করতে পারেন, তবে একই সাথে, সবসময় অন্যান্য বৈধ বিকল্প আছে। আমি আশা করি যে, সব অংশ কীভাবে একসাথে ফিট হয় তা দেখে আপনি অনুপ্রাণিত হবেন এবং আপনার প্রয়োজন অনুযায়ী এটি কাস্টমাইজ করার জন্য নিজেকে ক্ষমতায়িত বোধ করবেন।
Stack Overflow PWA
এই নিবন্ধের সাথে আমি একটি Stack Overflow PWA তৈরি করেছি। আমি Stack Overflow-এ অনেক সময় ধরে পড়ি ও কন্ট্রিবিউট করি এবং আমি এমন একটি ওয়েব অ্যাপ তৈরি করতে চেয়েছিলাম যা কোনও নির্দিষ্ট বিষয়ের জন্য প্রায়শই জিজ্ঞাসিত প্রশ্ন ব্রাউজ করা সহজ করে তুলবে। এটি পাবলিক Stack Exchange API-এর উপরে তৈরি করা হয়েছে। এটি ওপেন সোর্স এবং আপনি GitHub প্রোজেক্টে গিয়ে আরও জানতে পারবেন।
মাল্টি-পেজ অ্যাপ (MPA)
নির্দিষ্ট বিষয়ে যাওয়ার আগে, কিছু শব্দ সংজ্ঞায়িত করা যাক এবং অন্তর্নিহিত প্রযুক্তির অংশগুলি ব্যাখ্যা করা যাক। প্রথমে, আমি "মাল্টি পেজ অ্যাপ" বা "MPA" সম্পর্কে আলোচনা করব।
MPA হল ওয়েব শুরু হওয়ার সময় থেকে ব্যবহার করা ঐতিহ্যগত আর্কিটেকচারের একটি ফ্যান্সি নাম। ব্যবহারকারী যতবার নতুন URL-এ নেভিগেট করেন, ততবার ব্রাউজার সেই পৃষ্ঠার জন্য নির্দিষ্ট HTML প্রোগ্রেসিভভাবে রেন্ডার করে। নেভিগেশনের মধ্যে পৃষ্ঠার স্ট্যাটাস বা কন্টেন্ট সংরক্ষণ করার কোনও চেষ্টা করা হয়নি। আপনি যতবার নতুন পৃষ্ঠা ভিজিট করেন, ততবার নতুন করে শুরু করেন।
এটি ওয়েব অ্যাপ তৈরি করার জন্য সিঙ্গেল-পেজ অ্যাপ (SPA) মডেলের থেকে আলাদা, যেখানে ব্যবহারকারী নতুন বিভাগে গেলে ব্রাউজার জাভাস্ক্রিপ্ট কোড রান করে বর্তমান পৃষ্ঠা আপডেট করে। SPA এবং MPA, দুটিই ব্যবহার করার জন্য সমানভাবে উপযুক্ত মডেল, তবে এই পোস্টে আমি মাল্টি-পেজ অ্যাপের প্রেক্ষিতে PWA-এর ধারণা এক্সপ্লোর করতে চেয়েছিলাম।
নির্ভরযোগ্যভাবে দ্রুত
আপনি আমাকে (এবং আরও অনেককে) "প্রোগ্রেসিভ ওয়েব অ্যাপ" বা PWA শব্দবন্ধটি ব্যবহার করতে শুনেছেন। আপনি হয়ত ইতিমধ্যেই কিছু ব্যাকগ্রাউন্ড মেটেরিয়ালের সাথে পরিচিত, এই সাইটের অন্য কোথাও।
আপনি PWA-কে এমন একটি ওয়েব অ্যাপ হিসেবে ভাবতে পারেন যা ব্যবহারকারীকে ফার্স্ট-ক্লাস অভিজ্ঞতা প্রদান করে, এবং যা ব্যবহারকারীর হোম স্ক্রিনে জায়গা করে নেয়। "FIRE" অ্যাক্রোনিমটি Fast, Integrated, Reliable এবং Engaging-এর সংক্ষিপ্ত রূপ, যা PWA তৈরি করার সময় যেসব অ্যাট্রিবিউট সম্পর্কে ভাবতে হবে তার সারসংক্ষেপ করে।
এই নিবন্ধে, আমি এই অ্যাট্রিবিউটের সাবসেটের উপর ফোকাস করব: দ্রুত এবং নির্ভরযোগ্য।
দ্রুত: "দ্রুত" বলতে বিভিন্ন প্রসঙ্গে বিভিন্ন জিনিস বোঝায়, আমি নেটওয়ার্ক থেকে যতটা সম্ভব কম লোড করার স্পিড সংক্রান্ত সুবিধা কভার করতে চলেছি।
নির্ভরযোগ্য: তবে শুধু স্পিড থাকলেই হবে না। PWA-এর মতো অনুভূতি পেতে, আপনার ওয়েব অ্যাপকে নির্ভরযোগ্য হতে হবে। নেটওয়ার্কের অবস্থা যাই হোক না কেন, এটি যাতে সবসময় কিছু না কিছু লোড করতে পারে, এমনকি সেটি যদি শুধু একটি কাস্টমাইজ করা সমস্যা সংক্রান্ত পেজও হয়, সেই ব্যাপারে এটিকে যথেষ্ট স্থিতিস্থাপক হতে হবে।
নির্ভরযোগ্যভাবে দ্রুত: এবং অবশেষে, আমি PWA-এর সংজ্ঞাটি একটু নতুন করে বলতে যাচ্ছি এবং এটি কী তা দেখুন এর অর্থ হল এমন কিছু তৈরি করা যা নির্ভরযোগ্যভাবে দ্রুত। শুধুমাত্র কম লেটেন্সি নেটওয়ার্কে থাকাকালীন দ্রুত ও নির্ভরযোগ্য হওয়া যথেষ্ট নয়। নির্ভরযোগ্যভাবে দ্রুত হওয়ার অর্থ হল, আপনার ওয়েব অ্যাপের স্পিড নেটওয়ার্কের অবস্থা যেমনই হোক না কেন, একই রকম থাকে।
প্রযুক্তি চালু করা: সার্ভিস ওয়ার্কার + ক্যাশে স্টোরেজ API
PWA স্পিড ও স্থিতিস্থাপকতার জন্য একটি উচ্চ মানদণ্ড প্রবর্তন করে। সৌভাগ্যবশত, ওয়েব প্ল্যাটফর্ম এই ধরনের পারফর্ম্যান্সকে বাস্তবে রূপ দেওয়ার জন্য কিছু বিল্ডিং ব্লক অফার করে। আমি সার্ভিস ওয়ার্কার এবং Cache Storage API-এর কথা বলছি।
আপনি এমন একটি সার্ভিস ওয়ার্কার তৈরি করতে পারেন যা ইনকামিং অনুরোধের জন্য অপেক্ষা করে, কিছু অনুরোধ নেটওয়ার্কে পাস করে এবং Cache Storage API-এর মাধ্যমে ভবিষ্যতের ব্যবহারের জন্য উত্তরের একটি কপি স্টোর করে।
পরের বার ওয়েব অ্যাপ একই অনুরোধ করলে, এর সার্ভিস ওয়ার্কার ক্যাশে চেক করে আগে ক্যাশে করা উত্তর ফিরিয়ে দিতে পারে।
যতটা সম্ভব নেটওয়ার্ক এড়িয়ে চলা হল নির্ভরযোগ্যভাবে দ্রুত পারফর্ম্যান্স অফার করার একটি গুরুত্বপূর্ণ অংশ।
"আইসোমরফিক" জাভাস্ক্রিপ্ট
আমি আরও একটি ধারণা কভার করতে চাই, যেটিকে কখনও কখনও "আইসোমরফিক" বা "ইউনিভার্সাল" জাভাস্ক্রিপ্ট বলা হয়। সহজ কথায়, এটি হল এমন একটি ধারণা যার মাধ্যমে একই JavaScript কোড বিভিন্ন রানটাইম এনভায়রনমেন্টের মধ্যে শেয়ার করা যায়। আমি যখন আমার 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-এর সাথে ম্যাচ করতে পারে। এখানে, আমি রুটের নাম এবং তার সাথে ম্যাচ করার জন্য অন্তর্নিহিত এক্সপ্রেস প্যাটার্নের মধ্যে একটি ম্যাপিং তৈরি করি।
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index', '/'],
]);
export default routes;
আমি তখন সরাসরি সার্ভারের কোড থেকে এই ম্যাপিং রেফারেন্স করতে পারি। প্রদত্ত এক্সপ্রেস প্যাটার্নের জন্য কোনও মিল থাকলে, উপযুক্ত হ্যান্ডলার মিলিত রুটের জন্য নির্দিষ্ট টেমপ্লেটিং লজিক সহ উত্তর দেয়।
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 বয়লারপ্লেট ফেরত পাঠায় এবং ব্রাউজার সেই আংশিক পৃষ্ঠাটি তখনই রেন্ডার করতে পারে। সার্ভার বাকি ডেটা সোর্স একত্রিত করার সাথে সাথে, ডকুমেন্ট সম্পূর্ণ না হওয়া পর্যন্ত এটি ব্রাউজারে স্ট্রিম করে।
আমি কী বলতে চাইছি তা বুঝতে, আমাদের একটি রুটের এক্সপ্রেস কোড দেখুন:
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();
});
response অবজেক্টের write() পদ্ধতি ব্যবহার করে,
এবং স্থানীয়ভাবে স্টোর করা আংশিক টেমপ্লেট রেফারেন্স করে, আমি কোনও এক্সটার্নাল ডেটা সোর্স ব্লক না করেই,
অবিলম্বে উত্তর স্ট্রিম শুরু করতে
পারি। ব্রাউজার এই প্রাথমিক 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, Stack Exchange API URL-এ সেট করা
যা সংশ্লিষ্ট প্রশ্ন দেখানোর জন্য আমার প্রয়োজন। সেটা মনে
রাখবেন। আমি পরে এটি আবার দেখব।
আমার রুট হ্যান্ডলার-এ ফিরে গিয়ে, টেমপ্লেট তৈরি করা সম্পূর্ণ হয়ে গেলে, আমি আমার পৃষ্ঠার 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 নামক হাই-লেভেল লাইব্রেরির সেটের উপরে আমার সার্ভিস ওয়ার্কার তৈরি করতে চলেছি। এটি যেকোনও সার্ভিস ওয়ার্কারের ক্যাশিং, রাউটিং ও রেসপন্স জেনারেশন লজিকের জন্য একটি মজবুত ভিত্তি প্রদান করে।
রাউটিং
আমার সার্ভার-সাইড কোডের মতোই, আমার সার্ভিস ওয়ার্কারকে জানতে হবে যে কীভাবে ইনকামিং অনুরোধের সাথে উপযুক্ত রেসপন্স লজিক ম্যাচ করতে হয়।
আমার পদ্ধতি ছিল
অনুবাদ
প্রতিটি এক্সপ্রেস রুটকে একটি সংশ্লিষ্ট রেগুলার এক্সপ্রেশনে,
একটি সহায়ক লাইব্রেরি ব্যবহার করে যার নাম
regexparam. অনুবাদ হয়ে গেলে, আমি রেগুলার এক্সপ্রেশন রাউটিং-এর জন্য Workbox-এর বিল্ট-ইন সহায়তা ব্যবহার করতে পারব।
রেগুলার এক্সপ্রেশন আছে এমন মডিউল ইমপোর্ট করার পরে, আমি প্রতিটি রেগুলার এক্সপ্রেশন Workbox-এর রাউটারের সাথে রেজিস্টার করি। প্রতিটি রুটের মধ্যে আমি উত্তর জেনারেট করার জন্য কাস্টম টেমপ্লেটিং লজিক প্রদান করতে পারি। সার্ভিস ওয়ার্কারে টেমপ্লেট তৈরি করা আমার ব্যাকএন্ড সার্ভারের তুলনায় একটু বেশি জটিল, তবে Workbox অনেক কঠিন কাজ সহজ করে দেয়।
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templating logic.
);
স্ট্যাটিক অ্যাসেট ক্যাশিং
টেমপ্লেট তৈরি করার ক্ষেত্রে একটি গুরুত্বপূর্ণ বিষয় হল, আমার আংশিক HTML টেমপ্লেট Cache Storage API-এর মাধ্যমে লোকাল স্টোরেজে উপলভ্য আছে কিনা এবং ওয়েব অ্যাপে পরিবর্তন করার সময় সেগুলি আপ-টু-ডেট আছে কিনা তা নিশ্চিত করা। ম্যানুয়ালি ক্যাশে ম্যানেজ করার সময় ভুল হওয়ার সম্ভাবনা থাকে, তাই আমি বিল্ড প্রসেসের অংশ হিসেবে প্রি-ক্যাশিং ম্যানেজ করার জন্য Workbox ব্যবহার করি।
আমি Workbox-কে বলি কোন URL-গুলি কনফিগারেশন ফাইল ব্যবহার করে প্রিক্যাশ করতে হবে, যা আমার সমস্ত লোকাল অ্যাসেট সহ ডিরেক্টরিকে নির্দেশ করে এবং সাথে ম্যাচ করার জন্য প্যাটার্নের একটি সেট থাকে। এই ফাইলটি Workbox-এর CLI অটোমেটিক পড়ে, যা আমি প্রতিবার সাইট রিবিল্ড করার সময় রান করে।
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Other options...
};
Workbox প্রতিটি ফাইলের কন্টেন্টের স্ন্যাপশট নেয় এবং অটোমেটিক সেই URL-এর তালিকা ও রিভিশন আমার ফাইনাল সার্ভিস ওয়ার্কার ফাইলে ইনজেক্ট করে। প্রি-ক্যাশে করা ফাইল সবসময় উপলভ্য রাখতে এবং আপ-টু-ডেট রাখতে যা যা প্রয়োজন, তার সবকিছুই এখন ওয়ার্কবক্সে আছে। ফলাফল হল একটি 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 আলাদা আলাদা সোর্সকে
সমন্বয় করে এবং সেগুলিকে একসাথে জুড়ে একটি,
স্ট্রিমিং রেসপন্স তৈরি করে।
এছাড়াও, Streams API কাজ করছে কিনা তা Workbox অটোমেটিক শনাক্ত করে এবং কাজ না করলে, এটি সমতুল্য, নন-স্ট্রিমিং রেসপন্স তৈরি করে। এর অর্থ হল, আপনাকে ফলব্যাক লেখার ব্যাপারে চিন্তা করতে হবে না, কারণ স্ট্রিম ১০০% ব্রাউজার সাপোর্টের কাছাকাছি পৌঁছে গেছে।
রানটাইম ক্যাশিং
Stack Exchange API থেকে আমার সার্ভিস ওয়ার্কার কীভাবে রানটাইম ডেটা ম্যানেজ করে তা দেখা যাক। আমি Workbox-এর বিল্ট-ইন সহায়তা ব্যবহার করছি stale-while-revalidate ক্যাশিং স্ট্র্যাটেজির জন্য, এর সাথে মেয়াদ শেষ হওয়ার তারিখও ব্যবহার করছি যাতে ওয়েব অ্যাপের স্টোরেজ অসীমভাবে না বাড়ে।
আমি 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 ক্যাশিং লজিক প্রয়োগ করে, এর সাথে ৫০টি এন্ট্রিতে পৌঁছে গেলে সবচেয়ে কম-সম্প্রতি-ব্যবহৃত ক্যাশের মেয়াদ শেষ হয়ে যায়।
এখন আমার কাছে সেইসব স্ট্র্যাটেজি আছে, শুধু 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-এ রেসপন্স প্রসেস করে।
stale-while-revalidate স্ট্র্যাটেজির অর্থ হল, এই API কলের জন্য আমার কাছে আগে থেকে ক্যাশে করা উত্তর থাকলে, আমি অবিলম্বে এটি পৃষ্ঠায় স্ট্রিম করতে পারব, পরের বার অনুরোধ করা হলে "ব্যাকগ্রাউন্ডে" ক্যাশে এন্ট্রি আপডেট করার সময়।
সবশেষে, আমি আমার ফুটারের একটি ক্যাশে করা কপি স্ট্রিম করি এবং উত্তরটি সম্পূর্ণ করতে, চূড়ান্ত HTML ট্যাগ বন্ধ করি।
কোড শেয়ার করলে সবকিছু সিঙ্ক করা থাকে
আপনি লক্ষ্য করবেন যে সার্ভিস ওয়ার্কার কোডের কিছু অংশ পরিচিত লাগছে। আমার সার্ভিস ওয়ার্কারের ব্যবহার করা আংশিক HTML ও টেমপ্লেটিং লজিক আমার সার্ভার-সাইড হ্যান্ডলারের ব্যবহার করা লজিকের সাথে অভিন্ন। এই কোড শেয়ারিং নিশ্চিত করে যে ব্যবহারকারীরা একটি সামঞ্জস্যপূর্ণ অভিজ্ঞতা পান, তারা প্রথমবার আমার ওয়েব অ্যাপে ভিজিট করছেন বা পরিষেবা কর্মীর দ্বারা রেন্ডার করা পৃষ্ঠায় ফিরে আসছেন। আইসোমরফিক জাভাস্ক্রিপ্টের এটাই হল সৌন্দর্য।
ডায়নামিক, প্রোগ্রেসিভ এনহ্যান্সমেন্ট
আমি আমার PWA-এর জন্য সার্ভার এবং সার্ভিস ওয়ার্কার উভয়ই ব্যবহার করেছি, কিন্তু একটি শেষ লজিক কভার করতে হবে: একটি অল্প পরিমাণ জাভাস্ক্রিপ্ট আছে যা আমার প্রতিটি পৃষ্ঠায় চলে, সেগুলি সম্পূর্ণরূপে স্ট্রিম করার পরে।
এই কোডটি ব্যবহারকারীর অভিজ্ঞতাকে প্রোগ্রেসিভ এনহ্যান্সমেন্ট করে, তবে এটি গুরুত্বপূর্ণ নয়—ওয়েব অ্যাপটি রান না করলেও কাজ করবে।
পৃষ্ঠার মেটাডেটা
আমার অ্যাপ API রেসপন্সের উপর ভিত্তি করে পৃষ্ঠার মেটাডেটা আপডেট করার জন্য ক্লায়েন্ট-সাইড JavaScript ব্যবহার করে। আমি প্রতিটি পৃষ্ঠার জন্য ক্যাশে করা HTML-এর একই প্রাথমিক বিট ব্যবহার করি, তাই ওয়েব অ্যাপ আমার ডকুমেন্টের হেডে জেনেরিক ট্যাগ যোগ করে। কিন্তু আমার টেমপ্লেট ও ক্লায়েন্ট-সাইড কোডের মধ্যে সমন্বয় করে, আমি পৃষ্ঠা-নির্দিষ্ট মেটাডেটা ব্যবহার করে উইন্ডোর শীর্ষক আপডেট করতে পারি।
টেমপ্লেটিং কোডের অংশ হিসেবে, আমার পদ্ধতি হল সঠিকভাবে এস্কেপ করা স্ট্রিং সহ একটি স্ক্রিপ্ট ট্যাগ অন্তর্ভুক্ত করা।
const metadataScript = `<script>
self._title = '${escape(item.title)}';
</script>`;
তারপর, আমার পৃষ্ঠা লোড হয়ে গেলে, আমি সেই স্ট্রিং পড়ে ডকুমেন্টের নাম আপডেট করি।
if (self._title) {
document.title = unescape(self._title);
}
আপনার নিজস্ব ওয়েব অ্যাপে পৃষ্ঠা-নির্দিষ্ট মেটাডেটার অন্যান্য অংশ আপডেট করতে চাইলে, আপনি একই পদ্ধতি অনুসরণ করতে পারেন।
অফলাইন UX
আমি যে অন্য প্রোগ্রেসিভ এনহ্যান্সমেন্ট যোগ করেছি তা আমাদের অফলাইন ক্ষমতার দিকে মনোযোগ আকর্ষণ করার জন্য ব্যবহার করা হয়। আমি একটি নির্ভরযোগ্য PWA তৈরি করেছি এবং আমি চাই ব্যবহারকারীরা জানুক যে তারা অফলাইনে থাকলেও, আগে ভিজিট করা পৃষ্ঠাগুলি এখনও লোড করতে পারবে।
প্রথমে, আমি Cache Storage API ব্যবহার করে আগে থেকে ক্যাশে করা সমস্ত API অনুরোধের একটি তালিকা পাই এবং আমি সেটিকে URL-এর একটি তালিকায় অনুবাদ করি।
মনে আছে, সেই বিশেষ ডেটা অ্যাট্রিবিউট যেগুলির ব্যাপারে আমি বলেছিলাম, যার প্রত্যেকটিতে প্রশ্ন দেখানোর জন্য প্রয়োজনীয় API অনুরোধের 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].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 রেসপন্স অবিলম্বে দেখানো হলে, তা প্রায়শই ব্যবহারকারীর অভিজ্ঞতাকে আরও ভালো করে তোলে।