URL و URLSearchParams؛ ساخت نشانی بدون چسباندن رشته
در این درس ۵ بخش
در تمرین کاتالوگ قبلی، URLSearchParams برای نگهداری جستوجو پیشنهاد شد. اکنون از «چطور پارامتر بسازیم» عبور میکنیم: اگر یک کلید دو بار آمده باشد چه کنیم، نشانی مطلق چگونه مبدأ پایه را کنار میزند و چرا کدگذاری دوباره نتیجه را خراب میکند؟ این مسائل در پیوند اشتراکگذاری، فیلتر و اتصال یک کلاینت کاربرد دارند. همهٔ نشانیهای example.test در مثالها ساختگیاند و برای اجرا درخواست شبکهای نمیفرستیم. فایلها را با Node.js ۲۴ اجرا کنید و مقدارهای ساختهشده را بررسی کنید؛ نتیجهٔ این درس یک قرارداد قابل آزمون برای نشانی است، نه API تازهای که باید روی اینترنت پیدا کنید.
نشانی را به اجزا تقسیم کنید
URL فقط رشتهای با چند خط مورب نیست. روش ارتباط، میزبان، پورت، مسیر، بخش جستوجو و fragment مسئولیتهای متفاوت دارند. URL میتواند نشانی نسبی را با یک پایه حل کند و اجزا را برای خواندن و تغییر در اختیار بگذارد. #url-query مبنای این کار است؛ مسئلهٔ ما حالا کنترل ابهام ورودی است. Query همان بخش پس از علامت سؤال و پیش از fragment است. fragment معمولاً در درخواست HTTP به سرور فرستاده نمیشود، ولی همچنان میتواند در تاریخچه یا نمایش مرورگر باقی بماند. هیچیک را محل مناسبی برای گذاشتن رمز و توکن واقعی فرض نکنید.
فایل url-build.mjs زیر را اجرا کنید. مقدار q شامل فاصله، علامت سؤال و & است؛ اگر دستی به رشته بچسبد، ممکن است بخشی از آن پارامتر دیگری تعبیر شود. URLSearchParams مقدار خام را میگیرد و هنگام سریالسازی، کدگذاری لازم را انجام میدهد. بررسی اصلی این است که مقدار بازیابیشده دقیقاً همان ورودی باشد. نیازی نیست خروجی طولانی درصدها را حفظ کنید. نشانی ساختهشده هنوز فقط داده است؛ تا زمانی که آن را با fetch یا مرورگر درخواست نکردهاید، ارتباط شبکهای رخ نداده است.
const term = 'سلام & چای؟';
const link = new URL('/messages', 'https://chat.example.test');
link.searchParams.set('q', term);
link.searchParams.set('tag', 'الف+ب');
console.log(link.origin, link.pathname);
// https://chat.example.test /messages
console.log(link.searchParams.get('q') === term); // true
console.log(link.searchParams.get('tag')); // الف+ب
console.log(link.href); // نشانی کدگذاریشده را بررسی کنید.به set مقدار encodeURIComponentشده ندهید؛ در این مسیر مقدار خام لازم است و کدگذاری دوباره میتواند علامت درصد را هم به داده تبدیل کند. همچنین URLSearchParams بهتنهایی تجزیهگر یک URL کامل نیست؛ اگر نشانی کامل دارید ابتدا new URL بسازید و از searchParams آن استفاده کنید. فاصله در نمایش query ممکن است به صورت + سریال شود؛ برابرنبودن ظاهری دو رشته الزاماً به معنی متفاوتبودن مقدارهای بازیابیشده نیست. برعکس، یکسانبودن خروجیِ یک نمایشگر هم قرارداد شما را ثابت نمیکند. مقدار decodeشده و قواعد هر کلید را جدا بررسی کنید.
برای کلید تکراری، تصمیم محصول لازم است
در query داشتن چند مقدار برای یک کلید از نظر این ساختار ممکن است. get نخستین مقدار را میخواند؛ getAll همه را برمیگرداند. اگر یکی از بخشهای سامانه مقدار اول و بخش دیگر آخر را بپذیرد، یک نشانی مبهم دو رفتار ایجاد میکند. برای یک فیلتر چندانتخابی شاید تکرار عمدی باشد، ولی برای page تکمقداری بهتر است سیاست روشن داشته باشید. این درس برخلاف تمرین قبلی که روی ورودی خراب به صفحهٔ یک برمیگشت، قرارداد سختگیرانهٔ مستقلی را آزمایش میکند: page باید دقیقاً یک بار و بین یک تا پنجاه باشد.
function readPage(query) {
const params = new URLSearchParams(query);
const values = params.getAll('page');
if (values.length !== 1 || !/^[1-9]\d?$/.test(values[0])) {
throw new Error('One plain positive page is required');
}
const page = Number(values[0]);
if (page > 50) throw new RangeError('Page must be 1..50');
return page;
}
console.log(readPage('page=2')); // 2
for (const query of ['page=1&page=2', 'page=', 'page=1e1', 'page=51']) {
try { readPage(query); }
catch { console.log('rejected:', query); }
}این کد را مستقل در query-contract.mjs اجرا کنید. #type-conversion فقط مرحلهٔ Number نیست؛ پیش از آن شکل متن و شمار مقدارها بررسی میشود. تابع فقط قانون page را میسنجد و ادعا ندارد همهٔ کلیدهای یک API را کنترل کرده است. اگر قرارداد کامل فقط page را مجاز میداند، باید کلید ناشناخته را هم بررسی کنید. رد تکرار، صفر، متن خالی و نماد علمی را در جدول آزمون بنویسید. درستبودن قالب query، معتبر بودن مقدار تجاری آن را تضمین نمیکند؛ میتوان نشانی کاملاً معتبر با page نامناسب ساخت.
پایهٔ امن، نشانی مطلق را خودکار محدود نمیکند
new URL ورودی مطلق را معتبر میداند و در آن حالت از پایه برای نگهداشتن میزبان استفاده نمیکند. بنابراین اگر کاربر بتواند نشانی بدهد، گذاشتن یک base مورد اعتماد بهتنهایی محدودیت مقصد نیست. فایل url-policy.mjs زیر یک سیاست کوچک را نشان میدهد: فقط مبدأ ثابت، مسیر messages، بدون اطلاعات ورود و بدون fragment مجاز است. این مثال درخواست نمیفرستد و ابزار کامل مقابله با همهٔ حملات شبکهٔ سرور نیست. هدف این است که تشخیص ساختاری را با جستوجوی متنی مانند startsWith روی نشانی اشتباه نگیرید.
function allowedArchiveUrl(raw) {
if (typeof raw !== 'string') throw new TypeError('Expected URL text');
const base = new URL('https://chat.example.test');
const url = new URL(raw, base);
if (url.origin !== base.origin || url.pathname !== '/messages' ||
url.username || url.password || url.hash) {
throw new Error('URL outside this feature policy');
}
return url;
}
console.log(allowedArchiveUrl('/messages?q=سلام').pathname); // /messages
for (const raw of [
'https://outside.example/messages',
'https://chat.example.test@outside.example/messages',
'/messages#secret',
'javascript:alert(1)'
]) {
try { allowedArchiveUrl(raw); }
catch { console.log('blocked'); }
}نام ظاهری قبل از @ میتواند بخش اطلاعات ورود باشد، نه میزبان واقعی. origin و pathname نتیجهٔ تجزیهٔ URL را نشان میدهند؛ به جای حدس از ترتیب حروف از این اجزا استفاده میکنیم. مبدأ شامل روش ارتباط و پورت مؤثر هم هست. در پروژههای دیگر ممکن است پورت یا مسیر متفاوتی مجاز باشد، اما باید همان را صریح تعریف کنید. اگر URL خطای ساخت بدهد، آن را با #try-catch در مرز ورود به پیام قابل فهم تبدیل کنید. موفقیت new URL فقط به معنی قابل تجزیهبودن است، نه تأیید اجازه، اعتماد یا وجود مقصد.
در کلاینت چتِ همراه، نشانی اتصال باید از مبدأ و مسیر تعیینشدهٔ همان برنامه ساخته شود؛ کادر پذیرش سرور دلخواه به رابط اضافه نمیکنیم. مثال allowlist بالا قرار نیست جای تنظیم Origin در سرور WebSocket را بگیرد. اینها دو مرز متفاوتاند: کلاینت مقصد خودش را تعیین میکند و سرور دربارهٔ پذیرفتن اتصال تصمیم میگیرد. همچنین نام نمایشی کاربر در query یا پیام، هویت تأییدشده ایجاد نمیکند. قرارداد دقیق اتصال و پیام را در درس کلاینت و مسیر Node.js دنبال کنید و مسیر فرضی /messages این آزمایش را بهعنوان مسیر واقعی چت وارد برنامه نکنید.
ترتیب سریالسازی را با معنای قرارداد یکی نکنید
برای ساخت کلید کش یا پیوند تکرارپذیر، گاهی ترتیب کلیدها مهم میشود. sort روی URLSearchParams کلیدها را مرتب میکند و ترتیب نسبی مقدارهای دارای کلید یکسان را حفظ میکند. این رفتار، سیاست همارزی همهٔ URLها را تعریف نمیکند. اگر ترتیب دو مقدار تکراری در برنامهٔ شما معنی دارد، مرتبکردن خودسرانهٔ آنها ممکن است درخواست را تغییر دهد. برای امضای درخواست یا کنترل امنیتی هم از قانون مستند همان سامانه پیروی کنید؛ چند خط مرتبسازی، جای قرارداد طرف مقابل را نمیگیرد. مثال زیر فقط سریالسازی ثابت یک ساختار مشخص را بررسی میکند.
const params = new URLSearchParams('b=2&a=first&a=second');
params.sort();
console.log(params.toString()); // a=first&a=second&b=2
console.log(params.getAll('a')); // ['first', 'second']
params.set('a', 'single');
console.log(params.getAll('a')); // ['single']تمرین، پاسخ و ارجاع
یک سازندهٔ پیوند برای جستوجوی پیام بنویسید که q و page را بگیرد، q را خام به searchParams بدهد و page را طبق قرارداد تکمقداری این درس بسازد. ورودی شامل &، + و رقم فارسی را در q امتحان کنید؛ خروجی باید همان مقدار اصلی را از get برگرداند. پاسخ راهنما: URL پایه بسازید، با set هر کلید را یک بار قرار دهید و سپس خروجی را دوباره تجزیه کنید. برای page عمداً «۲» فارسی را بهعنوان متن رد کنید، مگر اینکه قرارداد تازهای برای تبدیل آن نوشته باشید. معیار تحویل، round-trip مقدار و رد نمونههای مبهم است.
اگر صفحهبندی و رفتار فیلتر را فراموش کردهاید، درس قبلی کاتالوگ را ببینید: https://www.darsavand.com/courses/javascript-project-lab?lesson=70875d30-98aa-4b81-8fe3-ef7349b1c555
برای تفاوت شبکه، HTTP و دادهٔ دریافتشده، این درس را مرور کنید: https://www.darsavand.com/courses/javascript-project-lab?lesson=d08ad69c-8701-447f-8f78-d44d9e14b30d
در کاربرد جدید، #validation-contracts شامل شمار مقدارها، مجازبودن مبدأ و سیاست خطا هم میشود. پیوندی که میسازید باید با توضیح رابط و کنترل سمت سرور هماهنگ باشد. یک رشتهٔ ظاهراً مرتب بهتنهایی نتیجهٔ درست نمیسازد؛ انتظار را قبل از اجرای نمونهها بنویسید و هم مقدار موفق و هم شکلهای ردشده را ثبت کنید.
منابع رسمی انگلیسی:
MDN — URL: https://developer.mozilla.org/en-US/docs/Web/API/URL
MDN — URLSearchParams: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
MDN — getAll: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/getAll
MDN — sort: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/sort
تألیف و مثالهای مستقل: تحریریه درسآوند. منابع رسمی برای بررسی رفتار زبان و مطالعهٔ بیشتر معرفی شدهاند.
تألیف و مثالهای اختصاصی: تحریریه درسآوند. فایلهای کامل پروژه و راهنمای اجرا و دیباگ:
https://www.darsavand.com/resources/simple-websocket-chat-project
متن درسهای عمومی را بدون ورود بخوانید.