लगभग 6 साल पहले जब मैं रिएक्ट सीख रहा था, तो रिएक्ट-राउटर मेरे द्वारा चुनी गई पहली तृतीय-पक्ष लाइब्रेरी में से एक थी। मेरा मतलब है, यह समझ में आता है: रूटिंग आधुनिक सिंगल पेज एप्लिकेशन के प्रमुख पहलुओं में से एक है। मैंने इसे हल्के में लिया.
मेरे लिए, रिएक्ट-राउटर एक जादू का पिटारा था, मुझे नहीं पता था कि यह आंतरिक रूप से कैसे काम करता है। इसलिए, जब कुछ समय बाद मुझे स्वाभाविक रूप से पता चला कि रूटिंग सामान्य रूप से कैसे काम करती है, तो यह थोड़ा दुखद था। अब कोई जादू नहीं, हुह :(
लेकिन मुझे खुशी है कि मैंने "जादुई ब्लैक बॉक्स" की अवधारणा को पीछे छोड़ दिया। मुझे लगता है कि यह सचमुच हानिकारक विचार है। यह समझना कि तकनीक का हर टुकड़ा आपके और मेरे जैसे इंजीनियरों द्वारा बनाया गया है, मुझे बहुत प्रेरित करता है, और मुझे आशा है कि यह आपके लिए भी ऐसा ही करेगा।
तो, इस पोस्ट में मेरे साथ शामिल हों जहां हम उस ब्लैक बॉक्स को खोलने और उसके आंतरिक कामकाज को समझने के लिए स्क्रैच से अपना खुद का टाइपसेफ राउटर बनाएंगे। यह आलेख मानता है कि आप पहले से ही रिएक्ट जानते हैं और टाइपस्क्रिप्ट के साथ सहज हैं।
आइए मैं बताता हूं कि इस लेख में हमारे राउटर की कौन सी विशेषताएं शामिल की जाएंगी।
हमारा किलर फीचर टाइपसेफ्टी होगा। इसका मतलब है कि आपको अपने मार्गों को पहले से परिभाषित करने की आवश्यकता होगी और टाइपस्क्रिप्ट जांच करेगा कि आप यूआरएल के बजाय कुछ बकवास पास नहीं कर रहे हैं या वर्तमान मार्ग में गायब पैरामीटर प्राप्त करने का प्रयास नहीं कर रहे हैं। इसके लिए कुछ प्रकार के जिम्नास्टिक की आवश्यकता होगी, लेकिन चिंता न करें, मैं आपको बताऊंगा।
इसके अलावा, हमारा राउटर उन सभी चीजों का समर्थन करेगा जो आप एक औसत राउटर से उम्मीद करते हैं: यूआरएल पर नेविगेशन, रूट मिलान, रूट पैरामीटर पार्सिंग, बैक/फॉरवर्ड नेविगेशन और नेविगेशन ब्लॉकिंग।
अब, सीमाओं के बारे में। हमारा राउटर केवल ब्राउज़र में काम करेगा (क्षमा करें रिएक्ट नेटिव!), और यह एसआरआर का समर्थन नहीं करेगा। एसएसआर समर्थन अपेक्षाकृत आसान होना चाहिए, लेकिन यह पोस्ट पहले से ही बहुत बड़ी है, इसलिए मैं इसे कवर नहीं करूंगा।
अब जब हमारे पास एक दृष्टिकोण है कि हम क्या बनाना चाहते हैं, तो हमें संरचना और शब्दावली के बारे में बात करने की ज़रूरत है। बहुत सारी समान अवधारणाएँ होंगी, इसलिए उन्हें पहले से परिभाषित करना महत्वपूर्ण है।
हमारी लाइब्रेरी में दो प्रकार के मार्ग होंगे: कच्चे मार्ग और पार्स किए गए मार्ग। कच्चा मार्ग केवल एक स्ट्रिंग है जो /user/:id/info
या /login
जैसा दिखता है; यह यूआरएल के लिए एक टेम्पलेट है. कच्चे रूट में पैरामीटर हो सकते हैं, जो ऐसे अनुभाग हैं जो कोलन से शुरू होते हैं, जैसे :id
।
यह प्रारूप डेवलपर्स के लिए उपयोग करना आसान है, लेकिन किसी प्रोग्राम के लिए इतना आसान नहीं है; हम उन मार्गों को अधिक मशीन-अनुकूल प्रारूप में बदल देंगे और इसे पार्स्ड मार्ग कहेंगे।
लेकिन उपयोगकर्ता रूट नहीं खोलते, वे यूआरएल खोलते हैं। और यूआरएल संसाधन के लिए एक पहचानकर्ता है (हमारे मामले में ऐप के अंदर का पेज); यह http://example.app/user/42/info?anonymous=1#bio
जैसा दिख सकता है। हमारा राउटर अधिकतर URL के दूसरे भाग ( /user/42/info?anonymous=1#bio
) की परवाह करता है, जिसे हम path कहेंगे।
पथ में पथनाम ( /user/42/info
), खोज पैरामीटर ( ?anonymous=1
) और हैश ( #bio
) शामिल हैं। वह ऑब्जेक्ट जो उन घटकों को अलग-अलग फ़ील्ड के रूप में संग्रहीत करता है उसे स्थान कहा जाएगा।
रिएक्ट लाइब्रेरी बनाते समय, मैं तीन चरणों में जाना पसंद करता हूँ। सबसे पहले, अनिवार्य एपीआई। इसके कार्यों को किसी भी स्थान से बुलाया जा सकता है, और वे आमतौर पर रिएक्ट से बिल्कुल भी बंधे नहीं होते हैं। राउटर के मामले में, यह navigate
, getLocation
या subscribe
जैसे कार्य होंगे।
फिर, उन कार्यों के आधार पर, useLocation
या useCurrentRoute
जैसे हुक बनाए जाते हैं। और फिर उन कार्यों और हुकों का उपयोग घटकों के निर्माण के लिए आधार के रूप में किया जाता है। मेरे अनुभव में यह असाधारण रूप से अच्छी तरह से काम करता है, और आसानी से विस्तार योग्य और बहुमुखी पुस्तकालय बनाने की अनुमति देता है।
राउटर का एपीआई defineRoutes
फ़ंक्शन से शुरू होता है। उपयोगकर्ता को फ़ंक्शन में सभी कच्चे मार्गों का एक नक्शा पास करना होता है, जो मार्गों को पार्स करता है और उसी आकार के साथ मैपिंग लौटाता है। सभी डेवलपर-सामना वाले एपीआई, जैसे यूआरएल जेनरेशन या रूट मिलान, पार्स किए गए रूट स्वीकार करेंगे, कच्चे नहीं।
const routes = defineRoutes({ login: '/login', user: { me: '/user/me', byId: '/user/:id/info', } });
अगला कदम पार्स किए गए मार्गों को createRouter
फ़ंक्शन में पास करना है। यह हमारे राउटर का मांस है। यह फ़ंक्शन सभी फ़ंक्शन, हुक और घटक बनाएगा। यह असामान्य लग सकता है, लेकिन ऐसी संरचना हमें स्वीकृत तर्कों और प्रॉप्स के प्रकारों को में परिभाषित routes
के एक विशिष्ट सेट के अनुरूप बनाने की अनुमति देती है, जिससे प्रकार की सुरक्षा सुनिश्चित होती है (और डीएक्स में सुधार होता है)।
const { Link, Route, useCurrentRoute, navigate, /* etc... */ } = createRouter(routes);
createRouter
उन फ़ंक्शंस को लौटाएगा जिनका उपयोग आपके ऐप में कहीं भी किया जा सकता है (अनिवार्य एपीआई), हुक जो आपके घटकों को स्थान परिवर्तनों पर प्रतिक्रिया करने की अनुमति देते हैं, और तीन घटक: Link
, Route
और NotFound
। यह अधिकांश उपयोग के मामलों को कवर करने के लिए पर्याप्त होगा, और आप उन एपीआई के आधार पर अपने स्वयं के घटक बना सकते हैं।
हम अपनी पिच के टाइपसेफ्टी भाग से निपटने से शुरुआत करते हैं। जैसा कि मैंने पहले उल्लेख किया है, टाइपसेफ राउटर के साथ, टाइपस्क्रिप्ट आपको इस तरह की स्थिति के बारे में पहले से चेतावनी देगा:
<Link href="/logim" />
या इस तरह:
const { userld } = useRoute(routes.user.byId);
और यदि आप तुरंत नहीं देख पा रहे हैं कि इनमें क्या खराबी है, तो आपको निश्चित रूप से एक टाइपसेफ राउटर की आवश्यकता है :)
टाइपस्क्रिप्ट में टाइप सिस्टम बहुत शक्तिशाली है। मेरा मतलब है, आप टाइप-लेवल प्रोग्रामिंग का उपयोग करके एक शतरंज इंजन , एक साहसिक गेम या यहां तक कि एक 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
या यूआरएल का निर्माण करने वाले फ़ंक्शन में गलत मान पास करने का प्रयास नहीं करता है।
टीएस के साथ टाइप-स्तरीय प्रोग्रामिंग शीघ्र ही एक अपठनीय गड़बड़ी बन सकती है। हमारे लिए सौभाग्य से, पहले से ही कई परियोजनाएं हैं जो इस सारी जटिलता को छिपा देती हैं और हमें इस तरह से साफ कोड लिखने की अनुमति देती हैं:
export type RouteParam< Route extends RawRoute, > = Pipe< Route, [ Strings.Split<"/">, Tuples.Filter<Strings.StartsWith<":">>, Tuples.Map<Strings.TrimLeft<":">>, Tuples.ToUnion ] >;
बहुत साफ-सुथरा, हुह? वैसे, कच्चे मार्ग से मापदंडों को पार्स करने के लिए आपको बस यही कोड चाहिए होता है। इस प्रोजेक्ट में, हम हॉटस्क्रिप्ट लाइब्रेरी का उपयोग करेंगे - यह हमें जटिलता और टाइप-स्तरीय कोड की मात्रा को कम करने में मदद करेगी।
लेकिन इसकी आवश्यकता नहीं है: यदि आप साहसी महसूस करते हैं, तो आप इन सभी प्रकारों को स्वयं लागू करने का प्रयास कर सकते हैं। आप चिकेन राउटर में कुछ प्रेरणा पा सकते हैं, जो तृतीय-पक्ष प्रकार की लाइब्रेरी का उपयोग किए बिना समान सुविधाओं को लागू करता है।
यदि आप अनुसरण करना चाहते हैं, तो मेरा सुझाव है कि आप अपने पसंदीदा स्टार्टर (मैं Vite का उपयोग करता हूं) का उपयोग करके एक नया रिएक्ट प्रोजेक्ट बनाएं और वहां कोडिंग शुरू करें। इस तरह, आप तुरंत अपने राउटर का परीक्षण कर पाएंगे।
कृपया ध्यान दें कि नेक्स्ट.जेएस जैसे फ्रेमवर्क अपनी स्वयं की रूटिंग प्रदान करते हैं जो इस प्रोजेक्ट में हस्तक्षेप कर सकते हैं, और इसके बजाय 'वेनिला' रिएक्ट का उपयोग करते हैं। यदि आपको कोई कठिनाई है, तो आप पूरा कोड यहां पा सकते हैं।
तृतीय-पक्ष पैकेज स्थापित करके प्रारंभ करें: टाइप-स्तरीय उपयोगिताओं के लिए हॉटस्क्रिप्ट और URL/रॉ रूट से पैरामीटर पार्स करने के लिए regexparam ।
npm install hotscript regexparam
हमारे प्रकार की पहली इमारत ईंट कच्चा मार्ग है। कच्चा मार्ग /
से शुरू होना चाहिए; आप इसे टीएस में कैसे कोड करेंगे? इस कदर:
export type RawRoute = `/${string}`;
आसान, है ना? लेकिन defineRoutes
एक भी कच्चे मार्ग को स्वीकार नहीं करता है, यह मैपिंग को स्वीकार करता है, संभवतः नेस्टेड, तो चलिए इसे कोड करते हैं। हो सकता है कि आप ऐसा कुछ लिखने के लिए प्रलोभित हों:
export type RawRoutesMap = { [key: string]: RawRoute | RawRoutesMap };
ये काम करेगा. हालाँकि, यह प्रकार असीम रूप से गहरा हो सकता है, और कुछ मामलों में टीएस को इसकी गणना करने में कठिनाई होगी। टीएस के जीवन को आसान बनाने के लिए, हम अनुमत घोंसले के स्तर को सीमित करेंगे। सभी ऐप्स के लिए 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"]
के रूप में पढ़ा जा सकता है। इस कंस्ट्रक्शन का इस्तेमाल आप खूब देखेंगे.
केवल संख्याएँ जोड़ने के बजाय टुपल का उपयोग क्यों करें? खैर, टाइपस्क्रिप्ट में दो नंबर जोड़ना इतना आसान नहीं है! इसके लिए एक पूरी लाइब्रेरी है, और हॉटस्क्रिप्ट नंबर भी जोड़ सकता है, लेकिन इसके लिए बहुत सारे कोड की आवश्यकता होती है (भले ही आप इसे न देखें), जो अत्यधिक उपयोग किए जाने पर आपके टीएस सर्वर और कोड संपादक को धीमा कर सकता है।
इसलिए, मेरे अंगूठे का नियम यह है कि जितना संभव हो सके जटिल प्रकारों से बचें।
इस उपयोगिता प्रकार के साथ, हम अपनी मैपिंग को सरलता से परिभाषित कर सकते हैं:
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
'फ़ंक्शन' थोड़ा अजीब लग सकता है, लेकिन हॉटस्क्रिप्ट के साथ आप कस्टम फ़ंक्शंस को इसी तरह परिभाषित करते हैं।
हम स्पष्ट रूप से बताते हैं कि पथ में या तो सिर्फ पथ शामिल है, पथ के बाद एक प्रश्न चिह्न होता है (यह खोज पैरामीटर और हैश के साथ यूआरएल को कवर करता है), या एक हैश प्रतीक (यह खोज पैरामीटर के बिना लेकिन हैश के साथ यूआरएल को कवर करता है)।
हमें मिलान किए गए मार्ग का वर्णन करने के लिए एक प्रकार की भी आवश्यकता होगी, यानी, यूआरएल से कैप्चर किए गए पैरामीटर के साथ पार्स किया गया मार्ग:
// 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
लाइब्रेरी का उपयोग करते हैं। यह हमें रूट के लिए आवश्यक मापदंडों की एक श्रृंखला प्राप्त करने की अनुमति देता है और एक नियमित अभिव्यक्ति उत्पन्न करता है जिसे हम बाद में रूट के साथ यूआरएल से मिलान करने के लिए उपयोग करेंगे।
हम इस जानकारी को इस ऑब्जेक्ट के निर्माण के लिए उपयोग किए गए मूल कच्चे मार्ग और अस्पष्टता स्तर (जो मार्ग में केवल कई पैरामीटर हैं) के साथ संग्रहीत करते हैं।
प्रत्येक राउटर को अपना स्टेट कहीं न कहीं स्टोर करना होता है। ब्राउज़र में ऐप्स के मामले में, यह वास्तव में 4 विकल्पों तक सीमित हो जाता है: इन-मेमोरी (या तो रूट घटक के अंदर एक राज्य चर में या घटकों के पेड़ के बाहर एक चर में), इतिहास एपीआई, या यूआरएल का हैश भाग।
यदि आप उपयोगकर्ता को बिल्कुल भी रूट नहीं दिखाना चाहते हैं, तो इन-मेमोरी रूटिंग आपकी पसंद हो सकती है, उदाहरण के लिए, यदि आप रिएक्ट में एक गेम को कोड कर रहे हैं। रूट को हैश में संग्रहीत करना तब आसान हो सकता है जब आपका रिएक्ट ऐप एक बड़े एप्लिकेशन में केवल एक पृष्ठ हो, और आप यूआरएल को अपनी इच्छानुसार नहीं बदल सकते।
लेकिन अधिकांश मामलों के लिए, हिस्ट्री एपीआई का उपयोग करना सबसे अच्छा विकल्प होगा। यह एसएसआर के साथ संगत है (अन्य विकल्प नहीं हैं), उपयोगकर्ता के आदी व्यवहार पैटर्न का पालन करता है, और बिल्कुल साफ दिखता है। इस परियोजना में, हम इसका भी उपयोग करेंगे। हालाँकि इसमें एक उल्लेखनीय दोष है: अतिरिक्त रैपर के बिना यह अधिकतर अनुपयोगी है।
हिस्ट्री एपी के साथ, आप popstate
इवेंट की सदस्यता ले सकते हैं, और यूआरएल बदलने पर ब्राउज़र आपको बताएगा। लेकिन केवल तभी जब परिवर्तन उपयोगकर्ता द्वारा शुरू किया गया हो, उदाहरण के लिए, बैक बटन पर क्लिक करके। यदि कोई यूआरएल परिवर्तन कोड से शुरू किया गया है, तो आपको स्वयं इसका ट्रैक रखना होगा।
जिन राउटर्स का मैंने अध्ययन किया उनमें से अधिकांश अपने स्वयं के रैपर का उपयोग करते हैं: रिएक्ट-राउटर और चिकेन इतिहास एनपीएम पैकेज का उपयोग करते हैं, टैनस्टैक राउटर का अपना कार्यान्वयन है और राउटर में पूर्ण-फ्लेजर रैपर नहीं है लेकिन फिर भी इसे मंकी-पैच इतिहास करना पड़ता है।
तो, आइए अपना खुद का रैपर लागू करें।
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 }); } } };
हमारे कार्यान्वयन में, अवरोधक एक फ़ंक्शन है जो एक बूलियन लौटाता है जो दर्शाता है कि हमें इस नेविगेशन को अवरुद्ध करने की आवश्यकता है या नहीं। नेविगेशन अवरोधन के संबंध में, नेविगेशन दो प्रकार के होते हैं, और हमें उन्हें अलग-अलग तरीके से संभालने की आवश्यकता होगी।
एक ओर, सॉफ्ट नेविगेशन है - जब उपयोगकर्ता हमारे ऐप में एक पेज से दूसरे पेज पर नेविगेट करता है। हम इसे पूरी तरह से नियंत्रित करते हैं और इस प्रकार इसे ब्लॉक कर सकते हैं, कोई भी कस्टम यूआई प्रदर्शित कर सकते हैं (उपयोगकर्ता के इरादे की पुष्टि करने के लिए), या नेविगेशन को ब्लॉक करने के बाद कार्रवाई कर सकते हैं।
दूसरी ओर, कठिन नेविगेशन होता है - जब उपयोगकर्ता किसी अन्य साइट पर नेविगेट करता है या टैब पूरी तरह से बंद कर देता है। ब्राउज़र जावास्क्रिप्ट को यह तय करने की अनुमति नहीं दे सकता कि यह नेविगेशन किया जाना चाहिए या नहीं, क्योंकि यह एक सुरक्षा चिंता का विषय होगा। लेकिन ब्राउज़र जावास्क्रिप्ट को यह इंगित करने की अनुमति देता है कि क्या हम उपयोगकर्ता को एक अतिरिक्त पुष्टिकरण संवाद दिखाना चाहते हैं।
सॉफ्ट नेविगेशन को ब्लॉक करते समय, आप अतिरिक्त यूआई (उदाहरण के लिए, कस्टम पुष्टिकरण संवाद) प्रदर्शित करना चाह सकते हैं, लेकिन हार्ड नेविगेशन के मामले में, इसका वास्तव में कोई मतलब नहीं है क्योंकि उपयोगकर्ता इसे केवल तभी देख पाएंगे जब वे पेज पर बने रहने का निर्णय लेंगे और , उस समय, यह बेकार और भ्रमित करने वाला है।
जब हमारा इतिहास नेविगेशन ब्लॉकर फ़ंक्शन को कॉल करता है, तो यह एक बूलियन प्रदान करेगा, जो दर्शाता है कि हम सॉफ्ट नेविगेशन कर रहे हैं या नहीं।
और इन सबके साथ, हमें बस अपना इतिहास ऑब्जेक्ट वापस करना होगा:
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>>;
इसे दो प्रकारों में विभाजित करना अनावश्यक लग सकता है, लेकिन इसका एक बहुत ही महत्वपूर्ण कारण है: यदि आप इसे एकल प्रकार के रूप में लागू करते हैं जो स्वयं को कॉल करता है, तो टीएस शिकायत करेगा कि प्रकार अत्यधिक गहरा है और संभवतः अनंत है। इसलिए, हम इसे दो प्रकारों में विभाजित करके काम करते हैं जो एक-दूसरे को बुलाते हैं।
मान-स्तरीय फ़ंक्शन के लिए, हमें यह जांचने के लिए एक टाइप गार्ड की भी आवश्यकता होगी कि पारित मान एक पार्स किया गया मार्ग है या नहीं।
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;
यहां हम सभी ज्ञात मार्गों पर जाते हैं और प्रत्येक को वर्तमान स्थान से मिलाने का प्रयास करते हैं। यदि रूट का रेगेक्स यूआरएल से मेल खाता है - तो हमें यूआरएल से रूट पैरामीटर मिलते हैं, अन्यथा, हमें null
मिलता है। प्रत्येक मिलान किए गए रूट के लिए, हम एक RouteWithParams
ऑब्जेक्ट बनाते हैं और इसे एक सरणी में सहेजते हैं। अब, यदि हमारे पास 0 या 1 मिलान मार्ग हैं, तो सब कुछ सरल है।
हालाँकि, यदि एक से अधिक मार्ग वर्तमान स्थान से मेल खाते हैं, तो हमें यह तय करना होगा कि किसकी प्राथमिकता अधिक है। इसे हल करने के लिए, हम ambiguousness
फ़ील्ड का उपयोग करते हैं। जैसा कि आपको याद होगा, इस मार्ग में कई पैरामीटर हैं, और सबसे कम ambiguousness
वाले मार्ग को प्राथमिकता दी जाती है।
उदाहरण के लिए, यदि हमारे पास दो मार्ग /app/dashboard
और /app/:section
हैं, तो स्थान http://example.com/app/dashboard
दोनों मार्गों से मेल खाएगा। लेकिन यह बिल्कुल स्पष्ट है कि यह यूआरएल /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); };
खैर, अब यह एक असली राउटर है! बहुत कमज़ोर, लेकिन फिर भी काम कर रहा हूँ। हमारे पास कार्यान्वयन के लिए अभी भी कुछ हुक और घटक हैं, लेकिन यहां से यह बहुत आसान हो जाता है।
हुक के लिए, हम सरल हुक से शुरुआत कर सकते हैं जो वर्तमान स्थान और वर्तमान मार्ग लौटाते हैं। वे अपने आप में काफी आसान हैं, लेकिन यूज़सिंकएक्सटर्नलस्टोर उन्हें वन-लाइनर में बदल देता है। जिस तरह से हमने पहले अपनी अनिवार्य एपीआई को डिज़ाइन किया था, उससे हमें इन हुक के लिए कोड को काफी कम करने की अनुमति मिली।
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
डिफ़ॉल्ट मान है) यदि वर्तमान मार्ग प्रदान किए गए फ़िल्टर में से किसी एक से मेल नहीं खाता है तो यह हुक एक त्रुटि फेंक देगा।
इस तरह, आप निश्चिंत हो सकते हैं कि हुक एक मिलान मार्ग लौटाएगा या बिल्कुल भी नहीं लौटाएगा। यदि दूसरा पैरामीटर गलत है, तो अपवाद फेंकने के बजाय, यदि वर्तमान मार्ग फ़िल्टर से मेल नहीं खाता है, तो हुक बस अपरिभाषित वापस आ जाएगा।
टाइपस्क्रिप्ट में इस व्यवहार का वर्णन करने के लिए, हम फ़ंक्शन ओवरलोडिंग नामक सुविधा का उपयोग करते हैं। यह हमें विभिन्न प्रकारों के साथ कई फ़ंक्शन परिभाषाओं को परिभाषित करने की अनुमति देता है, और जब उपयोगकर्ता ऐसे फ़ंक्शन को कॉल करता है तो टाइपस्क्रिप्ट स्वचालित रूप से उपयोग के लिए एक को चुन लेगा।
पथ पैरामीटर के अलावा, कुछ डेटा खोज पैरामीटर में पारित किया जा सकता है, तो आइए उन्हें स्ट्रिंग से मैपिंग में पार्स करने के लिए एक हुक जोड़ें। इसके लिए, हम अंतर्निहित ब्राउज़र 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
है या, कम से कम, कुछ इसी तरह का है। जैसा कि आपने पहले देखा, हमारे हुक एक अच्छी तरह से डिज़ाइन किए गए अनिवार्य एपीआई के कारण बहुत सरल थे जो सभी भारी सामान उठाता है।
घटकों के लिए भी यही बात लागू होती है; इन्हें उपयोगकर्ता द्वारा कोड की कुछ पंक्तियों में आसानी से कार्यान्वित किया जा सकता है। लेकिन हम वहां एक गंभीर रूटिंग लाइब्रेरी हैं, आइए बॉक्स में बैटरी शामिल करें :)
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> </>; };
जबकि हमारा राउटर वास्तव में रूट करता है, इसका टैनस्टैक राउटर, रिएक्ट-राउटर, या नेक्स्ट.जेएस राउटर जैसे उत्पादन-तैयार समाधानों से कोई मुकाबला नहीं है। मेरा मतलब है कि यह कोड की केवल ~500 पंक्तियाँ हैं, यह बहुत अधिक नहीं है। लेकिन वास्तव में क्या कमी है?
सबसे पहले, सर्वर साइड रेंडरिंग। आज, सभी ऐप्स को SSR की आवश्यकता नहीं हो सकती है, लेकिन सभी रूटिंग लाइब्रेरीज़ से इसका समर्थन करने की अपेक्षा की जाती है। एक स्ट्रिंग में सर्वर-साइड रेंडरिंग जोड़ने (एसएसआर स्ट्रीमिंग नहीं!) में एक अलग history
बनाना शामिल होगा जो वर्तमान स्थान को मेमोरी में संग्रहीत करेगा (क्योंकि सर्वर पर कोई इतिहास एपीआई नहीं है) और उसे createRouter
फ़ंक्शन में प्लग करें।
मुझे इस बात की जानकारी नहीं है कि स्ट्रीमिंग एसएसआर को लागू करना कितना कठिन होगा, लेकिन मुझे लगता है कि यह सस्पेंस के समर्थन से मजबूती से जुड़ा होगा।
दूसरा, यह राउटर समवर्ती रेंडरिंग के साथ अच्छी तरह से एकीकृत नहीं होता है। अधिकतर हमारे useSyncExternalStore
उपयोग के कारण, क्योंकि यह गैर-अवरुद्ध ट्रांज़िशन के साथ संगत नहीं है। यह फटने से बचने के लिए इस तरह से काम करता है: ऐसी स्थिति जहां यूआई का एक हिस्सा एक विशेष स्टोर वैल्यू के साथ प्रस्तुत किया गया है, लेकिन बाकी यूआई एक अलग के साथ प्रस्तुत किया गया है।
और इस वजह से, राउटर सस्पेंस के साथ अच्छी तरह से एकीकृत नहीं होता है, क्योंकि निलंबित होने वाले प्रत्येक स्थान अपडेट के लिए एक फ़ॉलबैक दिखाया जाएगा। वैसे, मैंने इस लेख में रिएक्ट में समवर्तीता को कवर किया है, और इसमें , मैं सस्पेंस, डेटा फ़ेचिंग और use
हुक के बारे में बात करता हूं।
लेकिन इन कमियों के बावजूद, मुझे आशा है कि आपको यह लेख दिलचस्प लगा होगा और आपने अपना राउटर भी बना लिया होगा :)