পাঠ ১৩ · ৫৮-এর মধ্যে · মডিউল ৩
Home / Courses / Full-Stack Web Frameworks / SPA রাউটিং

সিঙ্গেল-পেজ অ্যাপ্লিকেশনে রাউটিং

Routing in single-page applications
১২ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • SPA-তে ক্লায়েন্ট-সাইড রাউটিং কীভাবে ফুল পেজ রিলোড এড়ায়
  • ডাইনামিক পাথ সেগমেন্ট (:id-স্টাইল) কী এবং কেন প্রয়োজন
  • একটি হাতে-লেখা, non-regex পাথ ম্যাচার বাস্তবে তৈরি করা
  • একাধিক concrete পাথ (একটি ক্যাপচার-করা প্যারামিটারসহ) দিয়ে ম্যাচিং লজিক যাচাই করা

১ · SPA-তে রাউটিং কেন আলাদা

ঐতিহ্যবাহী মাল্টি-পেজ ওয়েবসাইটে প্রতিটি লিংকে ক্লিক করলে ব্রাউজার সার্ভারকে একটি নতুন রিকোয়েস্ট পাঠায় এবং সম্পূর্ণ নতুন HTML পেজ ডাউনলোড করে (L01-L02-এর client-server চক্র)। কিন্তু একটি সিঙ্গেল-পেজ অ্যাপ্লিকেশনSPAএকটি ওয়েব অ্যাপ যেখানে প্রথমবার একটি HTML পেজ লোড হওয়ার পর, পরবর্তী "পেজ পরিবর্তন" জাভাস্ক্রিপ্ট দিয়ে ক্লায়েন্ট-সাইডেই হ্যান্ডেল হয়, সার্ভার থেকে নতুন পুরো পেজ না এনেই।-এ পুরো অ্যাপ একবারই লোড হয় — এরপর ব্যবহারকারী যখন একটি "লিংকে" ক্লিক করেন, জাভাস্ক্রিপ্ট নিজেই ব্রাউজারের URL বদলে দেয় (ব্রাউজারকে সার্ভারে রিকোয়েস্ট পাঠাতে না দিয়ে), তারপর সেই নতুন URL দেখে ঠিক করে কোন কম্পোনেন্ট স্ক্রিনে দেখাতে হবে — এটাই ক্লায়েন্ট-সাইড রাউটিং। এতে পেজ পরিবর্তন অনেক দ্রুত হয় (সম্পূর্ণ HTML/CSS/JS আবার ডাউনলোড করতে হয় না), কিন্তু এর জন্য অ্যাপের নিজেরই একটি রাউটার থাকতে হয় যা URL-কে সঠিক কম্পোনেন্টে ম্যাপ করে।

২ · ডাইনামিক পাথ সেগমেন্ট — :id-স্টাইল ওয়াইল্ডকার্ড

L01-এর Router ক্লাস শুধু হুবহু-মেলা স্ট্যাটিক পাথ (/users, /about) হ্যান্ডেল করতে পারত — প্রতিটি সম্ভাব্য পাথের জন্য আলাদা একটি ডিকশনারি এন্ট্রি লাগত। কিন্তু /users/1, /users/2, /users/42 — এরকম হাজারো ইউজারের জন্য আলাদা রুট রেজিস্টার করা অবাস্তব। সমাধান হলো একটি প্যাটার্ন রেজিস্টার করা — /users/:id — যেখানে :id অংশটি একটি ওয়াইল্ডকার্ড: যেকোনো মানের সাথে মিলবে, আর সেই আসল মানটি (যেমন "42") হ্যান্ডলার ফাংশনে ক্যাপচার-করা প্যারামিটার হিসেবে পাঠানো হবে।

৩ · হাতে-লেখা path matcher

নিচের কোড সেলে re মডিউলের বদলে শুধু str.split('/') ও একটি সরল লুপ দিয়ে একটি সম্পূর্ণ কার্যকর match_route(pattern, path) ফাংশন তৈরি করা হয়েছে — এটি প্যাটার্ন ও পাথকে / দিয়ে সেগমেন্টে ভেঙে, সেগমেন্ট-বাই-সেগমেন্ট তুলনা করে, :-দিয়ে-শুরু-হওয়া সেগমেন্টকে ওয়াইল্ডকার্ড হিসেবে ট্রিট করে মান ক্যাপচার করে। এই matcher-এর উপর ভিত্তি করে একটি সম্পূর্ণ Router তৈরি করা হয়েছে যার navigate() মেথড ফুল পেজ রিলোড ছাড়াই "নেভিগেশন" সিমুলেট করে।

Python
def match_route(pattern, path):
    """pattern (যেমন '/users/:id') এর সাথে path (যেমন '/users/42') মেলে কি না দেখে।
    মিললে ক্যাপচার-করা প্যারামিটারের dict রিটার্ন করে (কোনো ডাইনামিক অংশ না থাকলে খালি dict),
    না মিললে None রিটার্ন করে। কোনো regex ব্যবহার করা হয়নি -- শুধু split ও তুলনা।"""
    pattern_parts = [seg for seg in pattern.split('/') if seg != '']
    path_parts = [seg for seg in path.split('/') if seg != '']

    if len(pattern_parts) != len(path_parts):
        return None  # সেগমেন্ট সংখ্যা না মিললে কখনোই মিলবে না

    params = {}
    for pattern_seg, path_seg in zip(pattern_parts, path_parts):
        if pattern_seg.startswith(':'):
            param_name = pattern_seg[1:]      # ':id' -> 'id'
            params[param_name] = path_seg      # আসল মান ক্যাপচার করা হলো
        elif pattern_seg != path_seg:
            return None                        # স্ট্যাটিক অংশ হুবহু না মিললে ব্যর্থ

    return params


class Router:
    """L01-এর Router-এর মতোই decorator-ভিত্তিক, কিন্তু ডাইনামিক সেগমেন্ট সমর্থন করে"""

    def __init__(self):
        self.routes = []  # (pattern, handler) -- ইনসার্শন-অর্ডারে রাখা হয়

    def route(self, pattern):
        def decorator(handler_fn):
            self.routes.append((pattern, handler_fn))
            return handler_fn
        return decorator

    def navigate(self, path):
        """'নেভিগেশন' সিমুলেট করে -- কোনো প্রকৃত পেজ রিলোড হয় না, শুধু মিলে যাওয়া হ্যান্ডলার কল হয়"""
        for pattern, handler in self.routes:
            params = match_route(pattern, path)
            if params is not None:
                print(f"navigate('{path}') -> প্যাটার্ন '{pattern}' মিলেছে, params={params}")
                return handler(**params)
        print(f"navigate('{path}') -> কোনো রুট মেলেনি (404)")
        return None


app = Router()

@app.route('/')
def home():
    return "হোমপেজ কম্পোনেন্ট রেন্ডার হলো"

@app.route('/users')
def user_list():
    return "সব ইউজারের লিস্ট কম্পোনেন্ট রেন্ডার হলো"

@app.route('/users/:id')
def user_detail(id):
    return f"ইউজার #{id}-এর প্রোফাইল কম্পোনেন্ট রেন্ডার হলো"

@app.route('/posts/:postId/comments/:commentId')
def comment_detail(postId, commentId):
    return f"পোস্ট #{postId}-এর কমেন্ট #{commentId} কম্পোনেন্ট রেন্ডার হলো"


test_paths = ['/', '/users', '/users/42', '/posts/7/comments/3', '/unknown']

for path in test_paths:
    rendered = app.navigate(path)
    print(f"  রেন্ডার-করা আউটপুট: {rendered}\n")

    

কোড Run করার আগে হাতে ট্রেস করে যাচাই করা যাক প্রতিটি পাথ কী রেজাল্ট দেবে:

  • '/' — pattern_parts ও path_parts দুটোই খালি লিস্ট ([]), দৈর্ঘ্য সমান (০), তুলনা করার কিছু নেই — {} রিটার্ন, home() কল হয়।
  • '/users' — ['users'] বনাম ['users'], স্ট্যাটিক অংশ হুবহু মিলে — {} রিটার্ন, user_list() কল হয়।
  • '/users/42' — প্যাটার্ন ['users', ':id'] বনাম পাথ ['users', '42']: প্রথম সেগমেন্ট স্ট্যাটিক মিল, দ্বিতীয়টি :id ওয়াইল্ডকার্ড তাই params['id'] = '42' — user_detail(id='42') কল হয়।
  • '/posts/7/comments/3' — প্যাটার্ন ['posts', ':postId', 'comments', ':commentId'] বনাম পাথ ['posts', '7', 'comments', '3']: উভয় ডাইনামিক সেগমেন্ট ক্যাপচার হয় — comment_detail(postId='7', commentId='3') কল হয়।
  • '/unknown' — একটি মাত্র সেগমেন্ট (['unknown']); বাকি সব রেজিস্টার্ড প্যাটার্নের সেগমেন্ট-সংখ্যা হয় ০, ১ (কিন্তু স্ট্যাটিক 'users' মেলে না), ২, বা ৪ — কোনোটাই মেলে না, তাই "কোনো রুট মেলেনি (404)" প্রিন্ট হয়।
লক্ষ্য করুন handler(**params) লাইনে ক্যাপচার-করা প্যারামিটার dict-টিকে কী-ওয়ার্ড আর্গুমেন্ট হিসেবে হ্যান্ডলার ফাংশনে পাঠানো হয়েছে — তাই user_detail(id) ফাংশনের প্যারামিটারের নাম (id) অবশ্যই প্যাটার্নের :id-এর নামের সাথে মিলতে হবে। এটাই দেখায় কেন /users/:id-এর মতো নামকরণ শুধু ডকুমেন্টেশন নয় — এটি সরাসরি হ্যান্ডলার ফাংশনের প্যারামিটার নামের সাথে যুক্ত।
মূল কথা · Key takeaway

ক্লায়েন্ট-সাইড রাউটিং মানে জাভাস্ক্রিপ্ট নিজেই URL দেখে সঠিক কম্পোনেন্ট নির্বাচন করে, সার্ভারে নতুন পেজের জন্য রিকোয়েস্ট না পাঠিয়ে। ডাইনামিক পাথ সেগমেন্ট (:id-স্টাইল) এই রাউটারকে হাজারো সম্ভাব্য URL (প্রতিটি ইউজার/পোস্টের জন্য) মাত্র একটি প্যাটার্ন দিয়ে হ্যান্ডেল করতে দেয় — সেগমেন্ট-বাই-সেগমেন্ট তুলনা করে কোন অংশ স্ট্যাটিক আর কোনটি ওয়াইল্ডকার্ড তা আলাদা করেই এটি সম্ভব হয়, কোনো regex ছাড়াই।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ match_route-এ সবার প্রথমেই কেন len(pattern_parts) != len(path_parts) চেক করা হয়েছে — এটা বাদ দিলে কী সমস্যা হতে পারত?

এই চেক ছাড়া zip(pattern_parts, path_parts) শুধু ছোট লিস্টটির দৈর্ঘ্য পর্যন্ত তুলনা করত, বাকি অতিরিক্ত সেগমেন্ট উপেক্ষা করত। ফলে /users প্যাটার্ন ভুলভাবে /users/42/extra/junk-এর মতো পাথের সাথেও "মিলে যেত" (শুধু প্রথম সেগমেন্ট 'users' তুলনা করেই), যা ভুল। দৈর্ঘ্য আগে থেকে সমান কি না নিশ্চিত করে এই ভুল-পজিটিভ ম্যাচ প্রতিরোধ করা হয়েছে।

প্র ০২ রাউটে /users এবং /users/:id — দুটোই রেজিস্টার করা আছে। এই দুটোর ক্রম (কোনটা আগে রেজিস্টার হয়েছে) কি ফলাফলে কোনো পার্থক্য তৈরি করে?

এই নির্দিষ্ট ক্ষেত্রে না, কারণ দুটো প্যাটার্নের সেগমেন্ট-সংখ্যা আলাদা (/users-এর ১টি, /users/:id-এর ২টি) — len() চেকের কারণে একটি পাথ কখনোই দুটোর সাথেই মিলবে না, তাই কোনো দ্ব্যর্থতা নেই। কিন্তু যদি দুটি প্যাটার্নের সেগমেন্ট-সংখ্যা একই হতো (যেমন /users/:id ও /users/new, দুটোই ২ সেগমেন্ট), তাহলে ক্রম গুরুত্বপূর্ণ হয়ে যেত — যেটা তালিকায় আগে থাকে, navigate()-এর লুপ সেটাই আগে চেক করে, তাই /users/:id আগে থাকলে সেটাই "জিতে" যেত, এমনকি /users/new-এর জন্যও।

প্র ০৩ এই পুরো সিমুলেশনে কোথাও প্রকৃত ব্রাউজার পেজ রিলোড ঘটেনি — কোন লাইনটি সেটা নিশ্চিত করে?

navigate() মেথডটি শুধু একটি সাধারণ Python ফাংশন কল (handler(**params)) করে ও তার রিটার্ন-ভ্যালু প্রিন্ট করে — এটি কোনো নতুন HTTP রিকোয়েস্ট পাঠায় না, কোনো নতুন HTML ফাইল লোড করে না। বাস্তব ব্রাউজারে এর সমতুল্য হলো জাভাস্ক্রিপ্টের History API (pushState) ব্যবহার করে URL বদলানো আর একটি রাউটার-ফাংশন কল করা — যা ব্রাউজারকে সার্ভারে নতুন রিকোয়েস্ট পাঠাতে বাধা দেয়, ঠিক এই সিমুলেশনের navigate()-এর মতোই শুধু একটি ফাংশন-কল দিয়ে "পেজ পরিবর্তন" ঘটায়।

অনুশীলন

  1. চিন্তা করুন: /search?q=python-এর মতো একটি URL-এ ?q=python অংশটিকে (কোয়েরি স্ট্রিং) এই match_route ফাংশন কীভাবে হ্যান্ডেল করবে — এটা কি স্বয়ংক্রিয়ভাবে কাজ করবে, নাকি কোড পরিবর্তন লাগবে?

    বর্তমান কোড কাজ করবে না ঠিকমতো — path.split('/') দিয়ে ?q=python অংশটাও শেষ সেগমেন্টের ভেতরে থেকে যাবে (যেমন 'search?q=python'), যা কোনো রেজিস্টার্ড প্যাটার্নের সাথে হুবহু মিলবে না। বাস্তব রাউটারে সাধারণত পাথ ও কোয়েরি স্ট্রিং আলাদা করার জন্য প্রথমে path.split('?') দিয়ে ভাগ করা হয় — পাথ অংশটি match_route-এ যায়, আর কোয়েরি স্ট্রিং আলাদাভাবে পার্স করে হ্যান্ডলারে অতিরিক্ত আর্গুমেন্ট হিসেবে পাঠানো হয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন রুট যোগ করুন — @app.route('/users/:id/posts/:postId') দিয়ে একটি user_post(id, postId) ফাংশন লিখুন যা f"ইউজার #{id}-এর পোস্ট #{postId} রেন্ডার হলো" রিটার্ন করে, তারপর test_paths-এ '/users/5/posts/9' যোগ করে Run চেপে দেখুন সঠিকভাবে দুটো প্যারামিটারই ক্যাপচার হয় কি না।

    নতুন প্যাটার্নের সেগমেন্ট ['users', ':id', 'posts', ':postId'] — চারটি সেগমেন্ট। '/users/5/posts/9' পাথও চার সেগমেন্টে ভাগ হয়: ['users', '5', 'posts', '9']। সেগমেন্ট-বাই-সেগমেন্ট তুলনায় 'users'='users' ও 'posts'='posts' স্ট্যাটিক মিল, আর :id→'5', :postId→'9' ক্যাপচার হয়। ফলে params={'id': '5', 'postId': '9'} আর user_post(id='5', postId='9') কল হয়ে "ইউজার #5-এর পোস্ট #9 রেন্ডার হলো" প্রিন্ট হবে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • পরের পাঠ L14 কোন স্টেট একটি কম্পোনেন্টের নিজস্ব থাকা উচিত, আর কখন তা গ্লোবালি শেয়ার করা প্রয়োজন — মডিউল ৪-এর শুরু।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
  • Python Programming কোর্স সহোদর কোর্স স্ট্রিং মেথড, লিস্ট কম্প্রিহেনশন ও **kwargs-এর ভাষাগত ভিত্তি সেই কোর্সেই তৈরি হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।
আগের পাঠ
React বনাম Vue বনাম Angular — একটি তুলনা