প্রায় 6 বছর আগে যখন আমি প্রতিক্রিয়া শিখছিলাম, তখন প্রতিক্রিয়া-রাউটার ছিল প্রথম তৃতীয় পক্ষের লাইব্রেরিগুলির মধ্যে একটি যা আমি তুলেছিলাম। আমি বলতে চাচ্ছি, এটা বোধগম্য: আধুনিক একক পৃষ্ঠা অ্যাপ্লিকেশনের মূল দিকগুলির মধ্যে একটি হল রাউটিং। আমি এটা মঞ্জুর জন্য গ্রহণ.
আমার জন্য, রিঅ্যাক্ট-রাউটার একটি ম্যাজিক বাক্স ছিল, এটি অভ্যন্তরীণভাবে কীভাবে কাজ করে তা আমার কোন ধারণা ছিল না। সুতরাং, যখন কিছু সময় পরে আমি স্বাভাবিকভাবে বুঝতে পারি যে কীভাবে রাউটিং সাধারণভাবে কাজ করে, তখন এটি কিছুটা দুঃখজনক ছিল। আর কোন জাদু নেই, হাহ :(
তবে আমি আনন্দিত যে আমি "ম্যাজিক ব্ল্যাক বক্স" এর ধারণাটি পিছনে রেখেছি। আমি মনে করি এটি একটি সত্যিই ক্ষতিকর ধারণা. আপনার এবং আমার মতোই প্রকৌশলী দ্বারা নির্মিত প্রযুক্তির প্রতিটি অংশ বোঝা, আমাকে অনেক অনুপ্রাণিত করে এবং আমি আশা করি এটি আপনার জন্যও একই কাজ করবে।
সুতরাং, এই পোস্টে আমার সাথে যোগ দিন যেখানে আমরা সেই ব্ল্যাক বক্সটি খুলতে এবং এর ভিতরের কাজগুলি বুঝতে স্ক্র্যাচ থেকে আমাদের নিজস্ব টাইপসেফ রাউটার তৈরি করব। এই নিবন্ধটি অনুমান করে যে আপনি ইতিমধ্যে প্রতিক্রিয়া জানেন এবং TypeScript এর সাথে স্বাচ্ছন্দ্য বোধ করেন।
আমাদের রাউটারের কোন বৈশিষ্ট্যগুলি এই নিবন্ধে কভার করা হবে তা আমাকে রূপরেখা দিন।
আমাদের হত্যাকারী বৈশিষ্ট্য টাইপসেফটি হবে। এর মানে হল আপনাকে আপনার রুটগুলিকে আগে থেকেই সংজ্ঞায়িত করতে হবে এবং TypeScript চেক করবে যে আপনি URL এর পরিবর্তে কিছু বাজে কথা পাস করেন না বা বর্তমান রুটে অনুপস্থিত প্যারামিটারগুলি পেতে চেষ্টা করুন৷ এর জন্য কিছু ধরণের জিমন্যাস্টিকসের প্রয়োজন হবে, তবে চিন্তা করবেন না, আমি আপনাকে দিয়ে যাব।
তা ছাড়াও, আমাদের রাউটার আপনি গড় রাউটার থেকে যা আশা করবেন তা সমর্থন করবে: URL-এ নেভিগেশন, রুট ম্যাচিং, রুট প্যারামিটার পার্সিং, ব্যাক/ফরওয়ার্ড নেভিগেশন এবং নেভিগেশন ব্লকিং।
এখন, সীমাবদ্ধতা সম্পর্কে। আমাদের রাউটার শুধুমাত্র ব্রাউজারে কাজ করবে (দুঃখিত নেটিভ প্রতিক্রিয়া!), এবং এটি SRR সমর্থন করবে না। SSR সমর্থন তুলনামূলকভাবে সহজ হওয়া উচিত, কিন্তু এই পোস্টটি ইতিমধ্যেই বিশাল, তাই আমি এটি কভার করব না।
এখন যেহেতু আমরা কী করতে চাই তার একটি দৃষ্টিভঙ্গি আছে, আমাদের গঠন এবং পরিভাষা সম্পর্কে কথা বলতে হবে। অনেকগুলি অনুরূপ ধারণা থাকবে, তাই তাদের আগে থেকে সংজ্ঞায়িত করা অত্যন্ত গুরুত্বপূর্ণ।
আমাদের লাইব্রেরিতে দুই ধরনের রুট থাকবে: কাঁচা রুট এবং পার্স করা রুট। কাঁচা রুট হল একটি স্ট্রিং যা দেখতে /user/:id/info
বা /login
এর মত; এটি URL-এর জন্য একটি টেমপ্লেট। Raw রুটে পরামিতি থাকতে পারে, যা একটি কোলন দিয়ে শুরু হওয়া বিভাগ, যেমন :id
।
এই বিন্যাসটি বিকাশকারীদের জন্য ব্যবহার করা সহজ, কিন্তু একটি প্রোগ্রামের জন্য এতটা নয়; আমরা সেই রুটগুলিকে আরও মেশিন-বান্ধব ফর্ম্যাটে রূপান্তর করব এবং এটিকে পার্স করা রুট বলব৷
কিন্তু ব্যবহারকারীরা রুট খোলে না, তারা ইউআরএল খোলে। এবং URL হল রিসোর্সের জন্য একটি শনাক্তকারী (আমাদের ক্ষেত্রে অ্যাপের ভিতরে পৃষ্ঠা); এটি দেখতে http://example.app/user/42/info?anonymous=1#bio
এর মত হতে পারে। আমাদের রাউটার বেশিরভাগই ইউআরএলের দ্বিতীয় অংশের ( /user/42/info?anonymous=1#bio
) বিষয়ে চিন্তা করে, যেটিকে আমরা পাথ বলব।
পাথ pathname ( /user/42/info
), অনুসন্ধান পরামিতি ( ?anonymous=1
) এবং হ্যাশ ( #bio
) নিয়ে গঠিত। যে বস্তুটি সেই উপাদানগুলিকে পৃথক ক্ষেত্র হিসাবে সংরক্ষণ করে তাকে অবস্থান বলা হবে।
প্রতিক্রিয়া লাইব্রেরি তৈরি করার সময়, আমি তিনটি ধাপে যেতে চাই। প্রথমত, আবশ্যিক API। এর ফাংশনগুলি যে কোনও জায়গা থেকে কল করা যেতে পারে এবং সেগুলি সাধারণত প্রতিক্রিয়ার সাথে আবদ্ধ হয় না। একটি রাউটারের ক্ষেত্রে, এটি navigate
, getLocation
বা subscribe
এর মত ফাংশন হবে।
তারপর, সেই ফাংশনগুলির উপর ভিত্তি করে, useLocation
বা useCurrentRoute
এর মতো হুক তৈরি করা হয়। এবং তারপর সেই ফাংশন এবং হুকগুলি উপাদানগুলি নির্মাণের ভিত্তি হিসাবে ব্যবহৃত হয়। এটি আমার অভিজ্ঞতায় অসাধারণভাবে কাজ করে এবং একটি সহজে প্রসারিত এবং বহুমুখী লাইব্রেরি তৈরি করার অনুমতি দেয়।
রাউটারের API defineRoutes
ফাংশন দিয়ে শুরু হয়। ব্যবহারকারীকে ফাংশনে সমস্ত কাঁচা রুটের একটি মানচিত্র পাস করার কথা, যা রুটগুলিকে পার্স করে এবং একই আকারের সাথে একটি ম্যাপিং প্রদান করে। সমস্ত ডেভেলপার-মুখী API, যেমন URL জেনারেশন বা রুট ম্যাচিং, পার্স করা রুট গ্রহণ করবে এবং কাঁচা নয়।
const routes = defineRoutes({ login: '/login', user: { me: '/user/me', byId: '/user/:id/info', } });
পরবর্তী ধাপ হল রাউটার ফাংশন createRouter
করতে পার্স করা রুটগুলি পাস করা। এটি আমাদের রাউটারের মাংস। এই ফাংশনটি সমস্ত ফাংশন, হুক এবং উপাদান তৈরি করবে। এটি অস্বাভাবিক দেখাতে পারে, কিন্তু এই ধরনের কাঠামো আমাদেরকে routes
সংজ্ঞায়িত রুটের নির্দিষ্ট সেটের জন্য গৃহীত আর্গুমেন্ট এবং প্রপসের ধরন তৈরি করতে দেয়, যাতে প্রকার নিরাপত্তা (এবং DX উন্নত করা) নিশ্চিত করা যায়।
const { Link, Route, useCurrentRoute, navigate, /* etc... */ } = createRouter(routes);
createRouter
এমন ফাংশনগুলি ফিরিয়ে দেবে যা আপনার অ্যাপের যেকোন জায়গায় ব্যবহার করা যেতে পারে (অত্যাবশ্যকীয় API), হুক যা আপনার উপাদানগুলিকে অবস্থান পরিবর্তনে প্রতিক্রিয়া জানাতে দেয় এবং তিনটি উপাদান: Link
, Route
এবং NotFound
। এটি বেশিরভাগ ব্যবহারের ক্ষেত্রে কভার করার জন্য যথেষ্ট হবে এবং আপনি সেই APIগুলির উপর ভিত্তি করে আপনার নিজস্ব উপাদান তৈরি করতে পারেন।
আমরা আমাদের পিচের ধরনের নিরাপত্তা অংশ মোকাবেলা করে শুরু করি। যেমনটি আমি আগে উল্লেখ করেছি, টাইপসেফ রাউটারের সাথে, টাইপস্ক্রিপ্ট আপনাকে এইরকম পরিস্থিতি সম্পর্কে আগাম সতর্ক করবে:
<Link href="/logim" />
অথবা এই মত:
const { userld } = useRoute(routes.user.byId);
এবং যদি আপনি এখনই দেখতে না পারেন যে এইগুলির সাথে কী ভুল, আপনার অবশ্যই একটি টাইপসেফ রাউটার দরকার :)
TypeScript এ টাইপ সিস্টেম খুবই শক্তিশালী। আমি বলতে চাচ্ছি, আপনি টাইপ-লেভেল প্রোগ্রামিং ব্যবহার করে একটি দাবা ইঞ্জিন , একটি অ্যাডভেঞ্চার গেম বা এমনকি একটি SQL ডাটাবেস তৈরি করতে পারেন।
আপনি ইতিমধ্যেই 'মান-স্তরের প্রোগ্রামিং'-এর সাথে পরিচিত যেখানে আপনি মানগুলি পরিচালনা করেন, যেমন, দুটি স্ট্রিং সংযুক্ত করা:
function concat(a, b) { return a + b; } concat('Hello, ', 'World!'); // 'Hello, World!'
কিন্তু আপনি এটি টাইপ দিয়েও করতে পারেন!
type Concat<A extends string, B extends string> = `${A}${B}`; type X = Concat<'Hello, ', 'World!'>; // ^? type X = "Hello, World!"
হ্যাঁ, এটি আপনার সাধারণ ফাংশনের মতো শক্তিশালী নয় এবং দেখতে আলাদা। কিন্তু এটি আপনাকে কিছু সুন্দর এবং দরকারী জিনিস করতে দেয়।
আমরা কাঁচা রুট থেকে প্যারামিটার বের করতে এবং নতুন ধরনের তৈরি করতে টাইপ-লেভেল প্রোগ্রামিং ব্যবহার করব যা ডেভেলপার Link
বা URL তৈরি করে এমন ফাংশনে ভুল মান পাস করার চেষ্টা করে না তা পরীক্ষা করতে পারে।
TS এর সাথে টাইপ-লেভেল প্রোগ্রামিং দ্রুত একটি অপঠনযোগ্য জগাখিচুড়ি হয়ে উঠতে পারে। সৌভাগ্যবশত আমাদের জন্য, ইতিমধ্যেই একাধিক প্রকল্প রয়েছে যা এই সমস্ত জটিলতাকে আড়াল করে এবং আমাদেরকে এভাবে পরিষ্কার কোড লিখতে দেয়:
export type RouteParam< Route extends RawRoute, > = Pipe< Route, [ Strings.Split<"/">, Tuples.Filter<Strings.StartsWith<":">>, Tuples.Map<Strings.TrimLeft<":">>, Tuples.ToUnion ] >;
বেশ ঝরঝরে, হাহ? এটি, যাইহোক, কাঁচা রুট থেকে প্যারামিটার পার্স করার জন্য আপনার প্রয়োজনীয় সমস্ত কোড। এই প্রকল্পে, আমরা হটস্ক্রিপ্ট লাইব্রেরি ব্যবহার করব - এটি আমাদের জটিলতা এবং টাইপ-লেভেল কোডের পরিমাণ কমাতে সাহায্য করবে।
তবে এটির প্রয়োজন নেই: আপনি যদি দুঃসাহসিক বোধ করেন তবে আপনি নিজেই এই সমস্ত ধরণের প্রয়োগ করার চেষ্টা করতে পারেন। আপনি Chicane রাউটারে কিছু অনুপ্রেরণা খুঁজে পেতে পারেন, যা তৃতীয় পক্ষের ধরনের লাইব্রেরি ব্যবহার না করেই অনুরূপ বৈশিষ্ট্য প্রয়োগ করে।
আপনি যদি অনুসরণ করতে যাচ্ছেন, আমি আপনাকে আপনার প্রিয় স্টার্টার ব্যবহার করে একটি নতুন প্রতিক্রিয়া প্রকল্প তৈরি করার পরামর্শ দিচ্ছি (আমি Vite ব্যবহার করি) এবং সেখানে কোডিং শুরু করুন। এইভাবে, আপনি এখনই আপনার রাউটার পরীক্ষা করতে সক্ষম হবেন।
অনুগ্রহ করে মনে রাখবেন যে Next.js-এর মতো ফ্রেমওয়ার্কগুলি তাদের নিজস্ব রাউটিং প্রদান করে যা এই প্রকল্পে হস্তক্ষেপ করতে পারে এবং পরিবর্তে 'ভ্যানিলা' প্রতিক্রিয়া ব্যবহার করে। আপনার যদি কোন অসুবিধা হয়, আপনি এখানে সম্পূর্ণ কোড খুঁজে পেতে পারেন।
তৃতীয় পক্ষের প্যাকেজগুলি ইনস্টল করে শুরু করুন: টাইপ-লেভেল ইউটিলিটিগুলির জন্য হটস্ক্রিপ্ট এবং URL/raw রুট থেকে প্যারামিটার পার্স করার জন্য regexparam ।
npm install hotscript regexparam
আমাদের ধরনের প্রথম বিল্ডিং ইট কাঁচা রুট হয়. কাঁচা পথ /
দিয়ে শুরু করা উচিত; আপনি কিভাবে TS এ কোড করবেন? এটার মত:
export type RawRoute = `/${string}`;
সহজ, তাই না? কিন্তু defineRoutes
একটি একক কাঁচা রুট গ্রহণ করে না, এটি ম্যাপিং গ্রহণ করে, সম্ভবত নেস্টেড, তাই এর কোড করা যাক। আপনি এই মত কিছু লিখতে প্রলুব্ধ হতে পারে:
export type RawRoutesMap = { [key: string]: RawRoute | RawRoutesMap };
এই কাজ হবে. যাইহোক, এই ধরনের অসীম গভীর হতে পারে, এবং TS এর কিছু ক্ষেত্রে এটি গণনা করা কঠিন হবে। TS-এর জীবন সহজ করতে, আমরা অনুমোদিত বাসা বাঁধার মাত্রা সীমিত করব। সমস্ত অ্যাপের জন্য 20 নেস্টিং লেভেল প্রচুর হওয়া উচিত এবং টিএস এটি সহজেই পরিচালনা করতে পারে।
কিন্তু আপনি কিভাবে পুনরাবৃত্ত ধরনের গভীরতা সীমাবদ্ধ করবেন? আমি এই SO উত্তর থেকে এই কৌশলটি শিখেছি; এখানে আমাদের প্রয়োজনীয়তার জন্য পরিবর্তিত একটি সংস্করণ রয়েছে:
export type RecursiveMap<T, MaxDepth extends number> = { [key: string]: RecursiveMap_<T, MaxDepth, []>; }; type RecursiveMap_<T, MaxDepth extends number, Stack extends unknown[]> = MaxDepth extends Stack["length"] ? T : T | { [key: string]: RecursiveMap_<T, MaxDepth, [1, ...Stack]> };
এটি আমাদের প্রথম জটিল প্রকার, তাই আমাকে এটি ব্যাখ্যা করা যাক। আমাদের এখানে দুটি প্রকার রয়েছে: RecursiveMap
একটি এন্ট্রি পয়েন্ট হিসাবে কাজ করে এবং RecursiveMap_
কল করে এটি একটি অতিরিক্ত টিপল প্যারামিটার পাস করে। এই টিপলটি ম্যাপিংয়ের গভীরতা ট্র্যাক করতে ব্যবহৃত হয়, প্রতিটি কলের সাথে আমরা এই অ্যারেতে একটি উপাদান যুক্ত করি।
এবং আমরা এটিকে কল করতে থাকি যতক্ষণ না এই টিপলের দৈর্ঘ্য MaxDepth
এর সমান হয়। টিএস-এ, যখন extends
নির্দিষ্ট মানগুলির সাথে ব্যবহার করা হয়, যাকে আক্ষরিকও বলা হয় (যেমন, বিশেষভাবে 42
, number
নয়), এর অর্থ 'সমান'।
এবং যেহেতু MaxDepth
এবং Stack["length"]
উভয়ই নির্দিষ্ট সংখ্যা, তাই এই কোডটিকে MaxDepth === Stack["length"]
হিসাবে পড়া যেতে পারে। দেখবেন এই নির্মাণে অনেক ব্যবহার হচ্ছে।
কেন শুধু সংখ্যা যোগ করার পরিবর্তে tuple ব্যবহার করুন? ওয়েল, টাইপস্ক্রিপ্টে দুটি সংখ্যা যোগ করা এত সহজ নয়! এটির জন্য একটি সম্পূর্ণ লাইব্রেরি রয়েছে এবং হটস্ক্রিপ্ট নম্বরগুলিও যোগ করতে পারে, তবে এটির জন্য প্রচুর কোডের প্রয়োজন (এমনকি আপনি এটি না দেখলেও), যা অতিরিক্ত ব্যবহার করলে আপনার টিএস সার্ভার এবং কোড এডিটরকে ধীর করে দিতে পারে।
সুতরাং, আমার অঙ্গুষ্ঠের নিয়ম হল যতটা যুক্তিসঙ্গতভাবে সম্ভব জটিল প্রকারগুলি এড়ানো।
এই ইউটিলিটি টাইপের সাথে, আমরা আমাদের ম্যাপিংকে সহজ হিসাবে সংজ্ঞায়িত করতে পারি:
export type RawRoutesMap = RecursiveMap<RawRoute, 20>;
যে কাঁচা রুট ধরনের জন্য সব. সারিতে পরেরটি পার্স করা রুট। পার্সড রুট হল একটি জাভাস্ক্রিপ্ট অবজেক্ট যেখানে কয়েকটি অতিরিক্ত ক্ষেত্র এবং একটি ফাংশন রয়েছে; এটি দেখতে কেমন তা এখানে:
export type ParsedRoute<R extends RawRoute> = { keys: RouteParam<R>[]; build(...params: PathConstructorParams<R>): Path<R>; raw: R; ambiguousness: number, pattern: RegExp; };
আসুন keys
ক্ষেত্র থেকে এটি আনপ্যাক করা শুরু করি। এটি কেবলমাত্র প্যারামিটারগুলির একটি অ্যারে যা এই রুটের জন্য প্রয়োজনীয়৷ এটি কীভাবে করা হয়েছে তা এখানে:
import { Pipe, Strings, Tuples } from "hotscript"; export type RouteParam< Route extends RawRoute, > = Pipe< Route, [ Strings.Split<"/">, Tuples.Filter<Strings.StartsWith<":">>, Tuples.Map<Strings.TrimLeft<":">>, Tuples.ToUnion ] >;
হটস্ক্রিপ্টে, একটি ফাংশন কল করার দুটি উপায় রয়েছে: Call
বা Pipe
। আপনি যখন একটি একক ফাংশন কল করতে হবে তখন Call
দরকারী, কিন্তু আমাদের ক্ষেত্রে, আমাদের কাছে সেগুলির মধ্যে 4টি আছে! Pipe
ইনপুট গ্রহণ করে এবং, ভাল, একটি প্রদত্ত টিপলের প্রথম ফাংশনে পাইপ করে।
প্রত্যাবর্তিত মান দ্বিতীয় ফাংশন এবং তাই ইনপুট হিসাবে পাস করা হয়. আমাদের ক্ষেত্রে, যদি আমাদের কাছে থাকে, উদাহরণস্বরূপ, কাঁচা রুট /user/:userId/posts/:postId
, এটি এভাবে রূপান্তরিত হবে:
export type Beep = Pipe< "/user/:userId/posts/:postId", [ Strings.Split<"/">, // ["user", ":userId", "posts", ":postId"] Tuples.Filter<Strings.StartsWith<":">>, // [":userId", ":postId"] Tuples.Map<Strings.TrimLeft<":">>, // ["userId", "postId"] Tuples.ToUnion // "userId" | "postId" ] >;
দেখা? এই হলো টাইপ লেভেলের প্রোগ্রামিং এর জাদু! এখন, এর যে build
ফাংশন মোকাবেলা করা যাক. এটি রুট প্যারামিটার (যেমন userId
এবং postId
) এবং ঐচ্ছিক অনুসন্ধান প্যারাম/হ্যাশ গ্রহণ করে এবং সেগুলিকে একটি পাথে একত্রিত করে। একটি PathConstructorParams
বাস্তবায়ন দেখুন:
// Allows us to also accept number and // any other type which can be converted into string export type StringLike = { toString: () => string }; export type SearchAndHashPathConstructorParams = { hash?: string, search?: string | { [key: string]: string, } }; export type RouteParamsMap< Route extends RawRoute, Val extends string | StringLike = string, > = { [key in RouteParam<Route>]: Val }; export type PathConstructorParams<R extends RawRoute> = | [RouteParamsMap<R, StringLike>] | [RouteParamsMap<R, StringLike>, SearchAndHashPathConstructorParams];
ফাংশন প্যারামিটারগুলিকে একটি অ্যারে হিসাবে সংজ্ঞায়িত করা হয় (যা পরে ...এর সংজ্ঞায় ছড়িয়ে পড়ে
build
ফাংশন), যেখানে প্রথম উপাদানটি হল RouteParamsMap
এবং দ্বিতীয়টি ঐচ্ছিক SearchAndHashPathConstructorParams
। build
মান ফেরত দেওয়ার বিষয়ে কী? আমরা ইতিমধ্যে এটির পথ প্রতিষ্ঠা করেছি, কিন্তু আপনি কীভাবে এটি টাইপস্ক্রিপ্টের সাথে বর্ণনা করবেন?
ঠিক আছে, এটি অনেকটা RouteParam
এর মতোই, কিন্তু এর জন্য একটু বেশি ধরনের জিমন্যাস্টিকস প্রয়োজন!
import { Fn } from "hotscript"; interface ReplaceParam extends Fn { return: this["arg0"] extends `:${string}` ? string : this["arg0"]; } // Leading slashes will be removed by Split<"/">, so we need to // add them back after our manipulations type Pathname< Route extends RawRoute, > = `/${Pipe< Route, [ Strings.Split<"/">, Tuples.Map<ReplaceParam>, Tuples.Join<"/"> ] >}${Route extends `${string}/` ? '/' : ''}`; export type Path< Route extends RawRoute, > = Pathname<Route> | `${Pathname<Route>}?${string}` | `${Pathname<Route>}#${string}`;
আমরা এখানে যা করি তা হল আমাদের রুটকে সেগমেন্টে বিভক্ত করা, প্রতিটি সেগমেন্টের উপর ম্যাপ করা এবং প্রতিটিতে আমাদের কাস্টম ফাংশন ReplaceParam
কল করা। এটি বর্তমান সেগমেন্টটি একটি প্যারামিটার কিনা তা পরীক্ষা করে এবং এটিকে string
দিয়ে প্রতিস্থাপন করে বা সেগমেন্টটি যেমন- আছে সেভাবে প্রদান করে। ReplaceParam
'ফাংশন' একটু অদ্ভুত লাগতে পারে, কিন্তু আপনি হটস্ক্রিপ্টের সাথে কাস্টম ফাংশনগুলিকে এভাবেই সংজ্ঞায়িত করেন।
আমরা স্পষ্টভাবে বলেছি যে পাথটি হয় শুধু পাথ থেকে, একটি প্রশ্ন চিহ্ন দ্বারা অনুসরণ করা পথ (এটি অনুসন্ধান প্যারাম এবং হ্যাশ সহ URL গুলিকে কভার করে), বা একটি হ্যাশ প্রতীক (এটি অনুসন্ধান প্যারাম ছাড়াই কিন্তু হ্যাশ সহ URLগুলিকে কভার করে)।
আমাদের মিলিত রুট বর্ণনা করার জন্য একটি প্রকারেরও প্রয়োজন হবে, যেমন, URL থেকে ক্যাপচার করা প্যারামিটার সহ পার্স করা রুট:
// Interface (and not type) because we need to use `this` export interface RouteWithParams<R extends RawRoute> { route: ParsedRoute<R>, params: RouteParamsMap<R>, // TS can't properly infer type of route object with simple // check like currentRoute.route === routes.user.byId, so we // need our custom type guard matches: <T extends RawRoute>(route: ParsedRoute<T>) => this is RouteWithParams<T>, }
শেষ প্রকার হল ParsedRoutesMap
; এটি RawRoutesMap
এর অনুরূপ, কিন্তু, ভাল, পার্স করা রুটের জন্য।
// This accepts RawRoutesMap and transforms it into // mapping of parsed routes of same shape export type ParsedRoutesMap<RM extends RawRoutesMap> = { [Key in keyof RM]: RM[Key] extends RawRoute ? ParsedRoute<RM[Key]> : RM[Key] extends RawRoutesMap ? ParsedRoutesMap<RM[Key]> : never; };
এবং যে নোট, আমরা প্রকারের সাথে শেষ. আরও কিছু থাকবে, তবে সেগুলি সহজ, এবং আমরা বাস্তবায়নের সাথে সাথে সেগুলিকে কভার করব। যদি টাইপ-লেভেল প্রোগ্রামিং এমন কিছু হয় যা আপনি আরও চেষ্টা করতে চান, আপনি আরও জানতে টাইপ-লেভেলের টাইপস্ক্রিপ্ট চেক করতে পারেন এবং টাইপ-চ্যালেঞ্জগুলি সমাধান করার চেষ্টা করতে পারেন (তাদেরও সংস্থানগুলির একটি ভাল তালিকা রয়েছে)।
অবশেষে, আমরা নিয়মিত মান-স্তরের কোডিং-এ ফিরে এসেছি। চলুন defineRoutes
প্রয়োগ করে বল রোলিং করা যাক।
export const typedKeys = <const T extends {}> (obj: T) => { return Object.keys(obj) as Array<keyof T>; }; export const defineRoutes = <const T extends RawRoutesMap>(routesMap: T): ParsedRoutesMap<T> => { const entries = typedKeys(routesMap).map((key) => { const entry = routesMap[key]; if (typeof entry === 'string') { return [key, parseRoute(entry)] as const; } else { // Nested map return [key, defineRoutes(entry)] as const; } }); return Object.fromEntries(entries); };
এখানে জটিল কিছু নেই; এর parseRoute
ফাংশনের আরও গভীরে ডুব দেওয়া যাক।
import { parse, inject, type RouteParams as RegexRouteParams } from "regexparam"; export class InvalidRoute extends Error { }; export class InvalidRouteParams extends Error { }; const parseRoute = <const R extends RawRoute>(route: R): ParsedRoute<R> => { if (!route.startsWith('/')) { throw new InvalidRoute('route should start with slash (/)') } const { keys, pattern } = parse(route); const hasRequiredParams = keys.length > 0; const parsedRoute: ParsedRoute<R> = { build(...args) { const params = ( hasRequiredParams ? args[0] : undefined ) as RouteParamsMap<R, StringLike> | undefined; const searchAndHash = ( hasRequiredParams ? args[1] : args[0] ) as SearchAndHashPathConstructorParams | undefined; if (hasRequiredParams) { if (!params) { throw new InvalidRouteParams( `Parameters for route ${route} weren't provided` ); } const missingKeys = keys.filter(k => !(k in params)); if (missingKeys.length) { throw new InvalidRouteParams( `Missing parameters for route ${route}: ${missingKeys.join(', ')}` ); } } else if (args.length > 1) { throw new InvalidRouteParams( `Route ${route} doesn't accept any parameters, received ${args[0]}` ); } let path = hasRequiredParams ? inject(route, params as RegexRouteParams<R>) : route; if (searchAndHash && searchAndHash.search) { if (typeof searchAndHash.search === 'string') { path += searchAndHash.search.startsWith('?') ? searchAndHash.search : '?' + searchAndHash.search; } else { path += '?' + new URLSearchParams(searchAndHash.search).toString(); } } if (searchAndHash && searchAndHash.hash) { path += searchAndHash.hash.startsWith('#') ? searchAndHash.hash : '#' + searchAndHash.hash; } return path as Path<R>; }, raw: route, keys: keys as RouteParam<R>[] || [], ambiguousness: keys.length, pattern: pattern, }; return parsedRoute; };
parseRoute
খুব সহজ, যদিও লক্ষণীয়ভাবে দীর্ঘ। রুট পার্স করতে এবং প্যারামিটার বের করতে, আমরা regexparam
লাইব্রেরি ব্যবহার করি। এটি আমাদেরকে রুটের জন্য প্রয়োজনীয় পরামিতিগুলির একটি অ্যারে পেতে দেয় এবং একটি রেগুলার এক্সপ্রেশন তৈরি করে যা আমরা পরবর্তীতে রুটের সাথে URL-এর সাথে মেলাতে ব্যবহার করব।
আমরা এই তথ্যটি এই বস্তুটি তৈরি করতে ব্যবহৃত মূল কাঁচা রুট এবং অস্পষ্টতা স্তরের সাথে সঞ্চয় করি (যা রুটের কয়েকটি পরামিতি মাত্র)।
প্রতিটি রাউটারকে তার অবস্থা কোথাও সংরক্ষণ করতে হবে। ব্রাউজারে অ্যাপের ক্ষেত্রে, এটি সত্যিই 4টি বিকল্পে ফুটে ওঠে: ইন-মেমরি (হয় রুট কম্পোনেন্টের ভিতরে একটি স্টেট ভেরিয়েবলে বা উপাদান গাছের বাইরে একটি ভেরিয়েবলে), History API, বা URL এর হ্যাশ অংশ।
ইন-মেমরি রাউটিং আপনার পছন্দ হতে পারে যদি আপনি যে ব্যবহারকারীকে আপনার কাছে মোটেও রুট আছে তা দেখাতে না চান, উদাহরণস্বরূপ, আপনি যদি প্রতিক্রিয়াতে একটি গেম কোডিং করছেন। আপনার প্রতিক্রিয়া অ্যাপটি একটি বড় অ্যাপ্লিকেশনে শুধুমাত্র একটি পৃষ্ঠা হলে হ্যাশে রুটটি সংরক্ষণ করা সহজ হতে পারে এবং আপনি চাইলেই URL পরিবর্তন করতে পারবেন না।
কিন্তু বেশিরভাগ ক্ষেত্রে, ইতিহাস API ব্যবহার করা সেরা বিকল্প হবে। এটি SSR এর সাথে সামঞ্জস্যপূর্ণ (অন্যান্য বিকল্পগুলি নয়), ব্যবহারকারীর অভ্যস্ত আচরণের ধরণগুলি অনুসরণ করে এবং কেবল পরিষ্কার দেখায়৷ এই প্রকল্পে, আমরা এটি ব্যবহার করব। যদিও এটির একটি উল্লেখযোগ্য ত্রুটি রয়েছে: এটি অতিরিক্ত মোড়ক ছাড়া বেশিরভাগই অব্যবহারযোগ্য।
হিস্ট্রি AP-এর মাধ্যমে, আপনি popstate
ইভেন্টে সদস্যতা নিতে পারেন এবং ইউআরএল পরিবর্তন হলে ব্রাউজার আপনাকে জানাবে। কিন্তু শুধুমাত্র যদি পরিবর্তনটি ব্যবহারকারী দ্বারা শুরু হয়, উদাহরণস্বরূপ, পিছনের বোতামে ক্লিক করে৷ যদি একটি URL পরিবর্তন কোড থেকে শুরু করা হয়, তাহলে আপনাকে এটির উপর নজর রাখতে হবে।
আমি অধ্যয়ন করা বেশিরভাগ রাউটার তাদের নিজস্ব র্যাপার ব্যবহার করে: রিঅ্যাক্ট-রাউটার এবং চিকেন ব্যবহারের ইতিহাস এনপিএম প্যাকেজ, ট্যানস্ট্যাক রাউটারের নিজস্ব বাস্তবায়ন রয়েছে এবং ওয়াউটারের একটি পূর্ণাঙ্গ র্যাপার নেই তবে এখনও বানর-প্যাচ ইতিহাস রয়েছে।
সুতরাং, আসুন আমাদের নিজস্ব মোড়ক বাস্তবায়ন করা যাক.
export type HistoryLocation = Pick<Location, | 'origin' | 'href' | 'hash' | 'search' | 'pathname' >; export type NavigationBlocker = (isSoftNavigation: boolean) => boolean; export const createHistory = () => { const winHistory = window.history; const winLocation = window.location; const getLocation = (): HistoryLocation => { return { origin: winLocation.origin, href: winLocation.href, pathname: winLocation.pathname, search: winLocation.search, hash: winLocation.hash, }; }; /* Some magic code */ return /* something... */; };
আমরা দুই ধরনের ব্যবহার করব, HistoryLocation
এবং NavigationBlocker
। প্রথম, বিল্ট-ইন Location
টাইপের কিছুটা সীমিত সংস্করণ (এটি window.location
টাইপ), এবং দ্বিতীয়টি কভার করা হবে যখন আমরা নেভিগেশন ব্লকিং এ পৌঁছাই। এই অধ্যায় থেকে আরও সমস্ত কোড createHistory
ফাংশনের ভিতরে যাবে।
আসুন ইতিহাস পরিবর্তনের সদস্যতা বাস্তবায়নের সাথে শুরু করি। আমরা এই প্রকল্পে সদস্যতা নেওয়ার জন্য প্রতিক্রিয়া-শৈলী ফাংশন ব্যবহার করব: আপনি একটি কলব্যাক পাস করে subscribe
কল করুন এবং এটি অন্য একটি ফাংশন প্রদান করে যা আপনি যখন সদস্যতা ত্যাগ করতে চান তখন আপনাকে কল করতে হবে।
const subscribers: Set<VoidFunction> = new Set(); const onChange = () => { subscribers.forEach(fn => { try { fn(); } catch (err) { console.error('Error while handling location update', err); } }) }; const subscribe = (listener: VoidFunction) => { subscribers.add(listener); return () => { subscribers.delete(listener); }; };
পরবর্তী ধাপ হল অবস্থান পরিবর্তনের প্রতি প্রতিক্রিয়া জানানো, যার মধ্যে প্রোগ্রামগতভাবে করা পরিবর্তনগুলিও অন্তর্ভুক্ত। তুমি এটা কি ভাবে করবে? বানর-প্যাচিং সঙ্গে, অবশ্যই. এটি দেখতে কিছুটা নোংরা হতে পারে (এবং এটি সত্যিই), তবে দুর্ভাগ্যবশত আমাদের কাছে আরও ভাল বিকল্প নেই।
const origPushState = winHistory.pushState.bind(winHistory); const origReplaceState = winHistory.replaceState.bind(winHistory); winHistory.pushState = (data, unused, url) => { // tryNavigate will be covered later tryNavigate(() => { origPushState(data, unused, url); onChange(); }); }; winHistory.replaceState = (data, unused, url) => { tryNavigate(() => { origReplaceState(data, unused, url); onChange(); }); }; // This event is emmited when user initiates navigation // or when calling history.go, history.back and history.forward window.addEventListener('popstate', onChange);
এবং আমাদের ইতিহাস বাস্তবায়নের শেষ প্রধান অনুপস্থিত অংশ হল ন্যাভিগেশন ব্লকিং: একটি বৈশিষ্ট্য যা আপনাকে নেভিগেশন অনুরোধকে বাধা দিতে এবং শর্তসাপেক্ষে বাতিল করতে দেয়। ন্যাভিগেশন ব্লকিংয়ের একটি ক্যানন উদাহরণ ব্যবহারকারীকে একটি ফর্মের মধ্যে তাদের অগ্রগতি হারাতে বাধা দেবে।
let blockers: NavigationBlocker[] = []; const beforeUnloadHandler = (event: Event) => { const blocked = blockers.some(blocker => blocker(false)); if (blocked) { event.preventDefault(); // @ts-ignore For older browsers event.returnValue = ''; return ''; } }; const tryNavigate = (cb: VoidFunction) => { const blocked = blockers.some(blocker => blocker(true)); if (blocked) return; cb(); }; const addBlocker = (blocker: NavigationBlocker) => { blockers.push(blocker); if (blockers.length === 1) { addEventListener('beforeunload', beforeUnloadHandler, { capture: true }); } return () => { blockers = blockers.filter(b => b !== blocker); if (blockers.length === 0) { removeEventListener('beforeunload', beforeUnloadHandler, { capture: true }); } } };
আমাদের বাস্তবায়নে, একটি ব্লকার একটি ফাংশন যা একটি বুলিয়ান প্রদান করে যা নির্দেশ করে যে আমাদের এই নেভিগেশনটি ব্লক করতে হবে কিনা। নেভিগেশন ব্লকিংয়ের ক্ষেত্রে, দুটি ধরণের নেভিগেশন রয়েছে এবং আমাদের সেগুলিকে আলাদাভাবে পরিচালনা করতে হবে।
একদিকে, নরম নেভিগেশন রয়েছে - যখন ব্যবহারকারী আমাদের অ্যাপের এক পৃষ্ঠা থেকে আমাদের অ্যাপের অন্য পৃষ্ঠায় নেভিগেট করেন। আমরা এটি সম্পূর্ণরূপে নিয়ন্ত্রণ করি এবং এইভাবে এটিকে ব্লক করতে পারি, যেকোনো কাস্টম UI প্রদর্শন করতে পারি (ব্যবহারকারীর অভিপ্রায় নিশ্চিত করতে), বা নেভিগেশন ব্লক করার পরে ক্রিয়া সম্পাদন করতে পারি।
অন্যদিকে, কঠিন নেভিগেশন আছে - যখন ব্যবহারকারী অন্য সাইটে নেভিগেট করে বা ট্যাবটি সম্পূর্ণভাবে বন্ধ করে দেয়। ব্রাউজার জাভাস্ক্রিপ্টকে এই নেভিগেশনটি সম্পাদন করা উচিত কিনা তা সিদ্ধান্ত নেওয়ার অনুমতি দিতে পারে না, কারণ এটি একটি নিরাপত্তা উদ্বেগ হবে৷ কিন্তু ব্রাউজার জাভাস্ক্রিপ্টকে নির্দেশ করতে দেয় যে আমরা ব্যবহারকারীকে একটি অতিরিক্ত নিশ্চিতকরণ ডায়ালগ দেখাতে চাই।
সফ্ট নেভিগেশন ব্লক করার সময়, আপনি অতিরিক্ত UI (যেমন, কাস্টম কনফার্মেশন ডায়ালগ) প্রদর্শন করতে চাইতে পারেন, কিন্তু হার্ড নেভিগেশনের ক্ষেত্রে, এটি আসলেই কোন অর্থে হয় না কারণ ব্যবহারকারী কেবল তখনই এটি দেখতে পাবে যদি তারা পৃষ্ঠায় থাকার সিদ্ধান্ত নেয় এবং , সেই সময়ে, এটা অকেজো এবং বিভ্রান্তিকর।
যখন আমাদের ইতিহাস নেভিগেশন ব্লকার ফাংশনকে কল করে, তখন এটি একটি বুলিয়ান প্রদান করবে, যা নির্দেশ করে যে আমরা নরম নেভিগেশন করছি কিনা।
এবং এই সমস্ত কিছুর সাথে, আমাদের কেবল আমাদের ইতিহাসের বস্তুটি ফিরিয়ে দিতে হবে:
return { subscribe, getLocation, push: winHistory.pushState, replace: winHistory.replaceState, go: (distance: number) => tryNavigate(() => winHistory.go.call(winHistory, distance)), back: () => tryNavigate(() => winHistory.back.call(winHistory)), forward: () => tryNavigate(() => winHistory.forward.call(winHistory)), addBlocker, };
আমরা অবশেষে এখানে এসেছি. ইম্পেরেটিভ এপিআই হবে আরও সব হুক এবং কম্পোনেন্টের ভিত্তি এবং ডেভেলপারকে তাদের চাহিদা পূরণের জন্য কাস্টম হুক তৈরি করার অনুমতি দেবে। প্রথমত, আমাদের রুট ম্যাপকে একটি সমতল অ্যারেতে রূপান্তর করতে হবে। এইভাবে, সমস্ত রুট লুপ করা অনেক সহজ হবে, যেটি কাজে আসবে যখন আমরা রুট-ম্যাচিং অংশে কাজ শুরু করব।
আমাদের উভয় ধরনের ইউটিলিটি প্রয়োজন (যা ParsedRoutesMap
ParsedRoute
এর মিলনে রূপান্তরিত করবে) এবং ফাংশন (যা routesMap
পার্স করা রুটের অ্যারেতে রূপান্তরিত করবে)। টাইপ দিয়ে শুরু করা যাক:
export type Values<T extends {}> = T[keyof T]; type FlattenRouteMap<T> = T extends ParsedRoute<any> | RawRoute ? T : T extends ParsedRoutesMap<RawRoutesMap> | RawRoutesMap ? AllRoutesFromMap<T> : never; export type AllRoutesFromMap< RM extends ParsedRoutesMap<RawRoutesMap> | RawRoutesMap > = FlattenRouteMap<Values<RM>>;
এটিকে দুটি প্রকারে বিভক্ত করা অপ্রয়োজনীয় মনে হতে পারে, তবে এটির জন্য একটি খুব গুরুত্বপূর্ণ কারণ রয়েছে: আপনি যদি এটিকে একটি একক টাইপ হিসাবে প্রয়োগ করেন যা নিজেকে কল করে, TS অভিযোগ করবে যে টাইপটি অত্যধিক গভীর এবং সম্ভবত অসীম। সুতরাং, আমরা একে অপরকে কল করে এমন দুটি প্রকারে বিভক্ত করে এটিকে ঘিরে কাজ করি।
একটি মান-স্তরের ফাংশনের জন্য, পাস করা মানটি পার্স করা রুট কিনা তা পরীক্ষা করার জন্য আমাদের একটি টাইপ গার্ডেরও প্রয়োজন হবে।
export const isParsedRoute = <T extends `/${string}` = `/${string}`>( route: any ): route is ParsedRoute<T> => { return !!route && typeof route === 'object' && typeof route.raw === 'string' && typeof route.build === 'function'; } export const getAllRoutes = <T extends RawRoutesMap>( routesMap: ParsedRoutesMap<T> ): ParsedRoute<AllRoutesFromMap<T>>[] => { type PossibleRawRoute = AllRoutesFromMap<T>; return typedKeys(routesMap).flatMap((k) => { const val = routesMap[k]; if (isParsedRoute<PossibleRawRoute>(val)) { return [val] as const; } // At this point we know that val isn't ParsedRoute, so it has to be map of routes // but TS can't infer that, so we help him a little by casting val to correct type return getAllRoutes(val as ParsedRoutesMap<T>); }); };
এখন, আমাদের রাউটার বাস্তবায়ন শুরু করা যাক। ইতিহাসের মতো, এই এবং পরবর্তী দুটি অধ্যায়ে, সমস্ত কোড createRouter
ফাংশনে যাবে, যদি না অন্যথায় বলা হয়।
import { useSyncExternalStore, ComponentType, useMemo, MouseEventHandler, ComponentProps, useEffect } from 'react'; export const createRouter = <T extends RawRoutesMap>(routesMap: ParsedRoutesMap<T>) => { // Type for any possible route from passed routesMap type RouteType = AllRoutesFromMap<T>; // Similar to above, but for matched routes, ie includes URL parameters type BindedRouteWithParams = RouteWithParams<RouteType>; // Some of our functions will accept route filter, // which can be single parsed route, array or object type RouteFilter<T extends RouteType> = | ParsedRoute<T> | ParsedRoute<T>[] | Record<string, ParsedRoute<T>>; const history = createHistory(); const routes = getAllRoutes(routesMap); };
প্রথমত, আসুন আমাদের রাউটারকে বর্তমান অবস্থানের সাথে পরিচিত কোন একটি রুটের সাথে মেলাতে শেখাই। কয়েকটি ইউটিলিটি গ্লোবাল স্কোপে বা আলাদা ফাইলে যেতে পারে, createRouter
ফাংশনের ভিতরে নয়:
export const filterOutFalsy = <T>(obj: T[]): Exclude<T, undefined>[] => { return obj.filter(Boolean) as Exclude<T, undefined>[]; }; export class RouteMatchingConflict extends Error { }; // This will be used later export class RouteMismatch extends Error { };
এবং এই কোড createRouter
ফাংশনে যায়।
const extractRouteParams = <T extends RawRoute>( pathname: string, parsedRoute: ParsedRoute<T> ) => { const match = parsedRoute.pattern.exec(pathname); if (!match) return undefined; // Extract all route parameters from match array // and construct object from them return Object.fromEntries(parsedRoute.keys.map((key, index) => { return [key, match[index + 1]]; })) as RouteParamsMap<T>; }; const findMatchingRoute = ( location: HistoryLocation ): BindedRouteWithParams | undefined => { const matchingRoutes = filterOutFalsy(routes.map(route => { const params = extractRouteParams<RawRoute>( location.pathname, route ); if (!params) return undefined; return { route, params, matches<T extends RawRoute>(r: ParsedRoute<T>) { return route === r; }, }; })); if (matchingRoutes.length === 0) return undefined; if (matchingRoutes.length === 1) return matchingRoutes[0]; // At this point we have multiple matching routes :/ // Gotta decide which one we prefer let lowestAmbiguousnessLevel = Infinity; let lowestAmbiguousnessMatches: BindedRouteWithParams[] = []; matchingRoutes.forEach((match) => { if (match.route.ambiguousness === lowestAmbiguousnessLevel) { lowestAmbiguousnessMatches.push(match); } else if (match.route.ambiguousness < lowestAmbiguousnessLevel) { lowestAmbiguousnessLevel = match.route.ambiguousness; lowestAmbiguousnessMatches = [match]; } }); if (lowestAmbiguousnessMatches.length !== 1) { throw new RouteMatchingConflict( `Multiple routes with same ambiguousness level matched pathname ${location.pathname}: ${lowestAmbiguousnessMatches.map(m => m.route.raw).join(', ')}` ); } return lowestAmbiguousnessMatches[0]; }; let currentRoute = findMatchingRoute(history.getLocation()); // This function will be later returned from createRouter function const getCurrentRoute = () => currentRoute;
এখানে আমরা সমস্ত পরিচিত রুট পেরিয়ে যাই এবং প্রতিটিকে বর্তমান অবস্থানের সাথে মেলানোর চেষ্টা করি। যদি রুটের রেজেক্স URL এর সাথে মেলে — আমরা URL থেকে রুট প্যারামিটার পাই, অন্যথায়, আমরা null
পাই। প্রতিটি মিলে যাওয়া রুটের জন্য, আমরা একটি RouteWithParams
অবজেক্ট তৈরি করি এবং এটি একটি অ্যারেতে সংরক্ষণ করি। এখন, যদি আমাদের 0 বা 1টি মিলে যাওয়া রুট থাকে, সবকিছুই সহজ।
যাইহোক, যদি একাধিক রুট বর্তমান অবস্থানের সাথে মিলে যায়, তাহলে আমাদের সিদ্ধান্ত নিতে হবে কোনটির অগ্রাধিকার বেশি। এটি সমাধান করার জন্য, আমরা ambiguousness
ক্ষেত্র ব্যবহার করি। আপনি হয়তো মনে রাখতে পারেন, এই রুটের বেশ কয়েকটি প্যারামিটার রয়েছে এবং সর্বনিম্ন ambiguousness
সহ একটি রুটকে অগ্রাধিকার দেওয়া হয়েছে।
উদাহরণস্বরূপ, যদি আমাদের দুটি রুট থাকে /app/dashboard
এবং /app/:section
, অবস্থান http://example.com/app/dashboard
উভয় রুটের সাথে মিলবে। কিন্তু এটা বেশ স্পষ্ট যে এই URLটি /app/dashboard
রুটের সাথে মিলিত হওয়া উচিত, /app/:section
সাথে নয়।
যদিও এই অ্যালগরিদম বুলেটপ্রুফ নয়। উদাহরণস্বরূপ, রুট /app/:user/settings/:section
এবং /app/dashboard/:section/:region
উভয়ই URL এর সাথে মিলবে http://example.com/app/dashboard/settings/asia
। এবং যেহেতু তাদের একই অস্পষ্টতা স্তর রয়েছে, আমাদের রাউটার কোনটিকে অগ্রাধিকার দেওয়া উচিত তা সিদ্ধান্ত নিতে সক্ষম হবে না।
এখন, অবস্থান পরিবর্তনের প্রতিক্রিয়া জানাতে এবং currentRoute
পরিবর্তনশীল আপডেট করার জন্য আমাদের এই কোডটি একসাথে আঠালো করতে হবে;
const areRoutesEqual = <A extends RawRoute, B extends RawRoute>( a: RouteWithParams<A> | undefined, b: RouteWithParams<B> | undefined ): boolean => { if (!a && !b) return true; // Both are undefined if ((!a && b) || (a && !b)) return false; // Only one is undefined if (!a!.matches(b!.route)) return false; // Different routes // Same routes, but maybe parameters are different? const allParamsMatch = a.route.keys.every(key => a.params[key] === b!.params[key]); return allParamsMatch; }; history.subscribe(() => { const newRoute = findMatchingRoute(history.getLocation()); if (!areRoutesEqual(newRoute, currentRoute)) { currentRoute = newRoute; notifyRouteChange(); // Will be covered later } });
এখন, আমাদের রাউটার অবস্থান পরিবর্তনে প্রতিক্রিয়া দেখায়, এবং ব্যবহারকারী সর্বদা বর্তমান রুট পেতে পারে, হ্যাঁ! কিন্তু রুট পরিবর্তন সাবস্ক্রাইব করার ক্ষমতা ছাড়া এটি খুব দরকারী নয়, তাই আসুন এটি যোগ করা যাক। আমরা ইতিহাসের মোড়কে যে পদ্ধতিটি ব্যবহার করি তার অনুরূপ পদ্ধতিটি।
const subscribers: Set<VoidFunction> = new Set(); const subscribe = (cb: VoidFunction) => { subscribers.add(cb); return () => void subscribers.delete(cb); }; const notifyRouteChange = () => { subscribers.forEach(cb => { try { cb(); } catch (err) { console.error('Error in route change subscriber', err); } }); };
নেভিগেশন সঞ্চালনের জন্য, আমরা navigate
এবং navigateUnsafe
ফাংশনগুলিকে প্রকাশ করব, যা history.push
এবং history.replace
এর চারপাশে একটি সাধারণ মোড়ক।
// This function accepts any string path (no type-safety) const navigateUnsafe = ( path: string, { action = 'push' }: { action?: 'push' | 'replace' } = {} ) => { history[action]({}, '', path) }; // And this function accepts only paths that correspond to one of routes const navigate = ( path: Path<RouteType>, options: { action?: 'push' | 'replace' } = {} ) => { navigateUnsafe(path, options); };
ওয়েল, এখন যে একটি বাস্তব রাউটার! খুব খালি-হাড়, কিন্তু তবুও কাজ করে। আমাদের কাছে এখনও কিছু হুক এবং উপাদান রয়েছে যা বাস্তবায়ন করার জন্য, তবে এটি এখান থেকে অনেক সহজ হয়ে যায়।
হুকগুলির জন্য, আমরা সাধারণ দিয়ে শুরু করতে পারি যা বর্তমান অবস্থান এবং বর্তমান রুট ফিরিয়ে দেয়। এগুলি নিজেরাই বেশ সহজ, কিন্তু SyncExternalStore ব্যবহার করে সেগুলিকে এক-লাইনারে পরিণত করে৷ আমরা আগে যেভাবে আমাদের প্রয়োজনীয় API ডিজাইন করেছি তা আমাদের এই হুকগুলির জন্য কোডকে ব্যাপকভাবে হ্রাস করতে দেয়।
const useLocation = () => { return useSyncExternalStore(history.subscribe, history.getLocation); }; const useCurrentRoute = () => { return useSyncExternalStore(subscribe, getCurrentRoute); };
শুধুমাত্র একটি নির্দিষ্ট রুট/রুটের সেটে রেন্ডার করা অনুমিত উপাদানগুলি কোডিং করার সময়, আপনি বর্তমান রুট পেতে useCurrentRoute
ব্যবহার করতে পারেন, এটি মানদণ্ডের সাথে মেলে কিনা তা পরীক্ষা করুন এবং তারপরে এর পরামিতিগুলি ব্যবহার করুন (অথবা একটি ত্রুটি নিক্ষেপ করুন)।
কিন্তু এটি এমন একটি সাধারণ কাজ যে এটির জন্য আমাদের ব্যবহারকারীদের তাদের নিজস্ব হুক লিখতে বাধ্য করা একটি অপরাধ হবে - আমাদের রাউটারকে এটি বাক্সের বাইরে সরবরাহ করা উচিত।
function useRoute<T extends RouteType>(filter: RouteFilter<T>, strict?: true): RouteWithParams<T>; function useRoute<T extends RouteType>(filter: RouteFilter<T>, strict: false): RouteWithParams<T> | undefined; function useRoute<T extends RouteType>(filter: RouteFilter<T>, strict?: boolean): RouteWithParams<T> | undefined { const currentRoute = useCurrentRoute(); const normalizedFilter = Array.isArray(filter) ? filter : isParsedRoute(filter) ? [filter] : Object.values(filter); const isMatching = !!currentRoute && normalizedFilter.some(route => currentRoute.matches(route)); if (isMatching) return currentRoute as RouteWithParams<T>; else { if (strict === false) return undefined; throw new RouteMismatch( `Current route doesn't match provided filter(s)` ); } }
এই হুকের দুটি সংস্করণ রয়েছে: কঠোর এবং শিথিল। যদি ব্যবহারকারী দ্বিতীয় প্যারামিটার হিসাবে true
পাস করে (অথবা কিছু পাস না করে, ডিফল্ট মান হিসাবে true
) তবে বর্তমান রুটটি প্রদত্ত ফিল্টারগুলির মধ্যে একটির সাথে মেলে না তাহলে এই হুকটি একটি ত্রুটি ছুঁড়বে৷
এইভাবে, আপনি নিশ্চিত হতে পারেন যে হুকটি একটি মিলিত রুট ফিরিয়ে দেবে বা মোটেও ফিরে আসবে না। যদি দ্বিতীয় প্যারামিটারটি মিথ্যা হয়, একটি ব্যতিক্রম নিক্ষেপ করার পরিবর্তে, বর্তমান রুটটি ফিল্টারগুলির সাথে না মিললে হুকটি কেবল অনির্ধারিতভাবে ফিরে আসবে৷
TypeScript-এ এই আচরণটি বর্ণনা করতে, আমরা ফাংশন ওভারলোডিং নামে একটি বৈশিষ্ট্য ব্যবহার করি। এটি আমাদেরকে বিভিন্ন প্রকারের সাথে একাধিক ফাংশন সংজ্ঞা নির্ধারণ করতে দেয় এবং ব্যবহারকারী যখন এই ধরনের ফাংশন কল করে তখন টাইপস্ক্রিপ্ট স্বয়ংক্রিয়ভাবে একটি ব্যবহার করার জন্য বেছে নেবে।
পাথ প্যারামিটার ছাড়াও, কিছু ডেটা সার্চ প্যারামিটারে পাস হতে পারে, তাই স্ট্রিং থেকে ম্যাপিং-এ পার্স করার জন্য একটি হুক যোগ করা যাক। এর জন্য, আমরা বিল্ট-ইন ব্রাউজার API URLSearchParams ব্যবহার করব।
const useSearchParams = () => { const location = useLocation(); return useMemo(() => { return Object.fromEntries( (new URLSearchParams(location.search)).entries() ); }, [location.search]); };
এবং এই বিভাগে শেষ হুক হল useNavigationBlocker
যা বেশ সহজ: এটি শুধুমাত্র কলব্যাক গ্রহণ করবে এবং history.addBlocker
কলগুলিকে মোড়ানো হবে একটি প্রভাবে, ব্লকারটি পরিবর্তন হলে পুনরায় সংযুক্ত করতে।
const useNavigationBlocker = (cb: NavigationBlocker) => { useEffect(() => { return history.addBlocker(cb); }, [cb]); };
এখন, এর উপাদানগুলিতে ঝাঁপ দেওয়া যাক!
রাউটিং লাইব্রেরি উল্লেখ করার সময় প্রথম উপাদানটি কী মনে আসে? আমি বাজি ধরছি এটি Route
বা, অন্তত, অনুরূপ কিছু। যেমন আপনি আগে দেখেছেন, আমাদের হুকগুলি একটি ভাল-ডিজাইন করা আবশ্যিক API এর কারণে খুব সহজ ছিল যা সমস্ত ভারী উত্তোলন করে।
একই উপাদান জন্য যায়; কোডের কয়েকটি লাইনে ব্যবহারকারীর দ্বারা সহজেই প্রয়োগ করা যেতে পারে। কিন্তু আমরা সেখানে একটি গুরুতর রাউটিং লাইব্রেরি, আসুন বাক্সে ব্যাটারি অন্তর্ভুক্ত করি :)
type RouteProps = { component: ComponentType, match: RouteFilter<RouteType> }; const Route = ({ component: Component, match }: RouteProps) => { const matchedRoute = useRoute(match, false); if (!matchedRoute) return null; return (<Component />); };
আচ্ছা, এটা সহজ ছিল! আমরা কিভাবে NotFound
কম্পোনেন্ট বাস্তবায়ন করব তা অনুমান করতে চান? :)
type NotFoundProps = { component: ComponentType }; const NotFound = ({ component: Component }: NotFoundProps) => { const currentRoute = useCurrentRoute(); if (currentRoute) return null; return (<Component />); };
এবং আমাদের রাউটারের জন্য প্রয়োজনীয় শেষ উপাদানটি হল Link
, যা একটু বেশি জটিল। আপনি শুধু <a href="/app/dashboard" />
ব্যবহার করতে পারবেন না কারণ এটি সর্বদা কঠিন নেভিগেশন শুরু করবে এবং কোনো প্রকার নিরাপত্তা প্রদান করবে না। সুতরাং, আসুন এই সমস্যাগুলি সমাধান করা যাক:
type LinkProps = Omit<ComponentProps<"a">, 'href'> & ( // Our link accepts either type-strict href // or relaxed unsafeHref { href: Path<RouteType>, unsafeHref?: undefined } | { href?: undefined, unsafeHref: string } ) & { action?: 'push' | 'replace' }; const Link = ({ action = 'push', onClick, href, unsafeHref, ...props }: LinkProps) => { const hrefToUse = (href ?? unsafeHref)!; const targetsCurrentTab = props.target !== '_blank'; const localOnClick: MouseEventHandler<HTMLAnchorElement> = (event) => { if (onClick) { onClick(event); if (event.isDefaultPrevented()) { // User-defined click handler cacnelled navigation, we should exit too return; } } const inNewTab = !targetsCurrentTab || event.ctrlKey || event.shiftKey || event.metaKey || event.button === 1; if (!isExternal && !inNewTab) { event.preventDefault(); navigateUnsafe(hrefToUse, { action }); } }; const isExternal = useMemo(() => { if (!hrefToUse) return false; return new URL(hrefToUse, window.location.href).origin !== location.origin; }, [hrefToUse]); return <a {...props} href={hrefToUse} onClick={localOnClick} /> };
একইভাবে navigate
জন্য, Link
কম্পোনেন্ট টাইপ ইউআরএলটি পরীক্ষা করে যা আপনি এটিতে পাস করেন কিন্তু আপনাকে একটি নির্বিচারে স্ট্রিং ইউআরএল (এস্কেপ হ্যাচ বা বাহ্যিক লিঙ্কের জন্য) প্রদান করার অনুমতি দেয়। <a>
-এর আচরণ ওভাররাইড করতে, আমরা আমাদের নিজস্ব onClick
শ্রোতাকে সংযুক্ত করি, যার ভিতরে আমাদের মূল onClick
(আমাদের Link
উপাদানে পাস) কল করতে হবে।
এর পরে, আমরা পরীক্ষা করি যে নেভিগেশনটি ইতিমধ্যে ডেভেলপার দ্বারা বাতিল করা হয়নি (যদি এটি হয় তবে আমাদের ইভেন্টটি উপেক্ষা করা উচিত)। সবকিছু ঠিক থাকলে, লিঙ্কটি বাহ্যিক নয় এবং এটি বর্তমান ট্যাবে খোলা উচিত কিনা তা আমরা পরীক্ষা করি। এবং শুধুমাত্র তখনই আমরা বিল্ট-ইন হার্ড নেভিগেশন বাতিল করতে পারি এবং পরিবর্তে, আমাদের নিজস্ব navigateUnsafe
ফাংশনকে কল করতে পারি।
এবং এখন, createRouter
ফাংশন থেকে আমাদের শুধুমাত্র আমাদের সমস্ত ফাংশন, হুক এবং উপাদানগুলি (সাথে ইতিহাস থেকে সরাসরি রপ্তানি করা কয়েকটি ফাংশন) ফেরত দিতে হবে।
return { // Directly re-exported from history go: history.go, back: history.back, forward: history.forward, addBlocker: history.addBlocker, getLocation: history.getLocation, subscribeToLocation: history.subscribe, // Imperative API subscribe, getCurrentRoute, navigate, navigateUnsafe, // Hooks useLocation, useCurrentRoute, useRoute, useSearchParams, useNavigationBlocker, // Components Link, Route, NotFound, };
এবং যে সঙ্গে, আমাদের রাউটার সম্পন্ন. এখন, আমরা এইমাত্র তৈরি করা আমাদের টিনি-ওয়েনি রাউটারটি প্রদর্শন করতে আমাদের টিনি-ওয়েনি অ্যাপ তৈরি করতে পারি! যাইহোক, আপনি এখানে এই রাউটারের সম্পূর্ণ কোড (একটি উদাহরণ অ্যাপের কোড সহ) খুঁজে পেতে পারেন।
সুতরাং, কিভাবে এই সব কোড একসাথে টাই আপ করে? বেশ ঝরঝরে, আমি নিজে যদি বলি! কল্পনা করুন যে আপনি একটি নোট নেওয়ার অ্যাপ তৈরি করছেন। প্রথমত, আপনি রুটগুলি সংজ্ঞায়িত করে এবং এর মতো একটি রাউটার তৈরি করে শুরু করবেন:
export const routes = defineRoutes({ // Yep, this will be very minimal app root: '/', newNote: '/new', note: '/note/:noteId', }); const router = createRouter(routes); // Export functions and component you will be using export const { navigate, Link, Route, NotFound, useRoute, useNavigationBlocker } = router;
এবং তারপর, আপনি পৃষ্ঠা উপাদানগুলির সাথে সংজ্ঞায়িত রুটগুলিকে লিঙ্ক করেন:
function App() { return ( <div className="app"> <div className="links"> {/* This is how you build URL for Link */} <Link href={routes.root.build({})}>View all notes</Link> <Link href={routes.newNote.build({})}>Create new</Link> </div> <Route match={routes.root} component={NotesListPage} /> <Route match={routes.newNote} component={NewNotePage} /> <Route match={routes.note} component={NoteDetailsPage} /> <NotFound component={NotFoundPage} /> </div> ) }
NoteDetailsPage
এ থাকাকালীন, আপনাকে URL থেকে একটি নোট আইডি পেতে হবে, যাতে আপনি একটি useRoute
হুক ব্যবহার করেন:
export const NoteDetailsPage = () => { const { getNote } = useNotes(); const { params } = useRoute(routes.note); const note = getNote(params.noteId); return note ? (<> <h1>{note.title}</h1> <div>{note.text}</div> </>) : (<h1>Not found</h1>); };
এবং একটি নতুন নোট তৈরি করার সময়, আপনি সম্ভবত ব্যবহারকারীর অভিপ্রায় নিশ্চিত করতে চান যদি তারা নোটটি সংরক্ষণ না করেই চলে যায়:
export const NewNotePage = () => { const saveNote = () => { isSubmittingNoteRef.current = true; const note = createNote(title, text); // And this is programmatic redirect navigate(routes.note.build({ noteId: note.id })); isSubmittingNoteRef.current = false; }; const [title, setTitle] = useState(''); const [text, setText] = useState(''); const { createNote } = useNotes(); const isSubmittingNoteRef = useRef(false); useNavigationBlocker((isSoftNavigation) => { const dirty = title !== '' || text !== ''; if (!dirty || isSubmittingNoteRef.current) return false; if (isSoftNavigation) { const confirmation = confirm('Do you want to leave?'); return !confirmation; } else { return true; } }); return <> <h1>New note</h1> <input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <textarea placeholder="Text" value={text} onChange={e => setText(e.target.value)} /> <button onClick={saveNote}>Save</button> </>; };
যদিও আমাদের রাউটার প্রকৃতপক্ষে রুট করে, এটি TanStack রাউটার, রিঅ্যাক্ট-রাউটার, বা Next.js রাউটারের মতো উৎপাদন-প্রস্তুত সমাধানগুলির জন্য কোন মিল নয়। আমি বলতে চাচ্ছি এটা মাত্র ~500 লাইনের কোড, সেটা বেশি নয়। কিন্তু ঠিক কি অনুপস্থিত?
প্রথমত, সার্ভার সাইড রেন্ডারিং। আজ, সমস্ত অ্যাপের এসএসআর প্রয়োজন নাও হতে পারে, তবে সমস্ত রাউটিং লাইব্রেরি এটি সমর্থন করবে বলে আশা করা হচ্ছে। একটি স্ট্রিং-এ সার্ভার-সাইড রেন্ডারিং যোগ করা (SSR স্ট্রিমিং নয়!) একটি ভিন্ন history
তৈরি করবে যা বর্তমান অবস্থানকে মেমরিতে সংরক্ষণ করবে (যেহেতু সার্ভারে কোনো ইতিহাস API নেই) এবং এটি createRouter
ফাংশনে প্লাগ করবে।
স্ট্রিমিং এসএসআর বাস্তবায়ন করা কতটা কঠিন হবে সে সম্পর্কে আমি সচেতন নই, তবে আমি অনুমান করি এটি সাসপেন্সের সমর্থনের সাথে দৃঢ়ভাবে সংযুক্ত হবে।
দ্বিতীয়ত, এই রাউটারটি সমসাময়িক রেন্ডারিংয়ের সাথে একত্রিত হয় না। বেশিরভাগই আমাদের useSyncExternalStore
ব্যবহারের কারণে, কারণ এটি নন-ব্লকিং ট্রানজিশনের সাথে সামঞ্জস্যপূর্ণ নয়। এটি ছিঁড়ে যাওয়া এড়াতে এইভাবে কাজ করে: এমন একটি পরিস্থিতি যেখানে UI-এর একটি অংশ একটি নির্দিষ্ট স্টোর মান দিয়ে রেন্ডার করা হয়েছে, কিন্তু বাকি UI অন্য একটি দিয়ে রেন্ডার করা হয়েছে।
এবং এই কারণে, রাউটারটি সাসপেন্সের সাথে ভালভাবে একত্রিত হয় না, যেহেতু স্থগিত হওয়া প্রতিটি অবস্থানের আপডেটের জন্য, একটি ফলব্যাক দেখানো হবে। যাইহোক, আমি এই প্রবন্ধে প্রতিক্রিয়া-এ একযোগে কভার করেছি, এবং এই একটিতে , আমি সাসপেন্স, ডেটা আনয়ন এবং হুক use
সম্পর্কে কথা বলেছি।
তবে এই খারাপ দিকগুলির সাথেও, আমি আশা করি আপনি এই নিবন্ধটি আকর্ষণীয় পেয়েছেন এবং পথে আপনার নিজস্ব রাউটার তৈরি করেছেন :)