encodeURI یا encodeURIComponent: کدام را استفاده کنیم؟

encodeURIComponent یازده نویسه را کدگذاری می‌کند که encodeURI دست‌نخورده می‌گذارد، و همین ۱۱ نویسه تعیین می‌کنند یک مقدار داخل پارامتر خودش باقی بماند یا نه.

همین حالا امتحان کنید: کدگذار و کدگشای URL رمزگشای URL

جاوا اسکریپت دو تابع داخلی برای کدگذاری درصدی (percent-encoding) دارد و این دو فقط در یک چیز با هم فرق دارند: چه مقدار از نحو URL را دست‌نخورده می‌گذارند. encodeURI فرض می‌کند یک URL کامل به آن داده شده و نویسه‌هایی را که به URL ساختار می‌دهند، مثل /، ? و &، حفظ می‌کند. encodeURIComponent فرض می‌کند فقط یک تکه داده به آن داده شده و همین نویسه‌ها را هم کدگذاری می‌کند تا داده با ساختار URL اشتباه گرفته نشود. خلاصه ماجرا: برای گذاشتن یک مقدار داخل URL از encodeURIComponent استفاده کنید، یا بهتر از آن، از API URL.

تفاوت دقیق دو تابع

هر دو تابع هر نویسه‌ای را که دست‌نخورده نمی‌گذارند به UTF-8 تبدیل می‌کنند و هر بایت را به شکل % و دو رقم هگز با حروف بزرگ می‌نویسند؛ پس é به %C3%A9 و € به %E2%82%AC تبدیل می‌شود. تنها تفاوت، مجموعه نویسه‌های قابل چاپ ASCII است که هرکدام دست‌نخورده می‌گذارد:

نویسه‌هاencodeURIencodeURIComponent
A-Z a-z 0-9 - _ . ! ~ * ' ( )دست‌نخوردهدست‌نخورده
# $ & + , / : ; = ? @دست‌نخوردهکدگذاری می‌شود
فاصله و " % < > [ \ ] ^ ` { | }کدگذاری می‌شودکدگذاری می‌شود
نویسه‌های کنترلی و هر چیزی خارج از ASCIIبه شکل بایت‌های UTF-8 کدگذاری می‌شودبه شکل بایت‌های UTF-8 کدگذاری می‌شود

ردیف وسط کل ماجراست. این ۱۱ نویسه بخش‌های مختلف URL را از هم جدا می‌کنند، پس اگر داخل یک مقدار دست‌نخورده بمانند، معنای URL عوض می‌شود:

const s = 'Tom & Jerry/2?';
encodeURI(s);           // 'Tom%20&%20Jerry/2?'
encodeURIComponent(s);  // 'Tom%20%26%20Jerry%2F2%3F'

کی از کدام استفاده کنیم

برای هر چیزی که داخل URL قرار می‌دهید از encodeURIComponent استفاده کنید: مقدار یا نام یک پارامتر کوئری، یک بخش از مسیر، بخش fragment، یا یک URL کامل که خودش به‌عنوان پارامتر فرستاده می‌شود، مثل redirect=. ببینید با تابع دیگر چه چیزی خراب می‌شود:

const q = 'rock & roll';

// Wrong: the & starts a new parameter
encodeURI('https://example.com/search?q=' + q);
// 'https://example.com/search?q=rock%20&%20roll'
// The server sees q = 'rock ' and a second parameter ' roll'

// Right
'https://example.com/search?q=' + encodeURIComponent(q);
// 'https://example.com/search?q=rock%20%26%20roll'

encodeURI برای یک حالت محدود ساخته شده است: وقتی از قبل یک آدرس کامل با ساختار درست دارید که فقط فاصله یا نویسه‌های غیر ASCII در آن هست، مثل https://example.com/café?name=Zoë. حتی در این حالت هم دو دام دارد:

  • نویسه % را کدگذاری می‌کند. آدرسی که بخشی از آن از قبل کدگذاری شده، دو بار کدگذاری می‌شود: encodeURI('https://example.com/a%20b') نتیجه https://example.com/a%2520b را برمی‌گرداند.
  • نویسه‌های [ و ] را کدگذاری می‌کند. این نویسه‌ها دور نشانی‌های IPv6 می‌آیند، پس http://[::1]:8080/ به http://%5B::1%5D:8080/ تبدیل می‌شود که دیگر میزبان معتبری نیست.

اگر URL را از چند بخش می‌سازید، new URL() معمولاً ابزار بهتری است، چون هر بخش را بر اساس جایی که در URL قرار می‌گیرد کدگذاری می‌کند:

const url = new URL('https://example.com/search');
url.searchParams.set('q', 'café & thé');
url.href;  // 'https://example.com/search?q=caf%C3%A9+%26+th%C3%A9'

مقایسه با RFC 3986

RFC 3986 دو گروه نویسه تعریف می‌کند. نویسه‌های unreserved یا رزرونشده (بخش ۲٫۳) هیچ‌وقت به کدگذاری نیاز ندارند: حروف، ارقام و - . _ ~. نویسه‌های reserved یا رزروشده (بخش ۲٫۲) نقش نحوی دارند: جداکننده‌های عمومی : / ? # [ ] @ و جداکننده‌های فرعی ! $ & ' ( ) * + , ; =.

هیچ‌کدام از دو تابع جاوا اسکریپت دقیقاً با این تعریف منطبق نیست. encodeURIComponent پنج جداکننده فرعی را کدگذاری نمی‌کند: ! ' ( ) *. مرورگرها و بیشتر سرورها به این اهمیتی نمی‌دهند، اما سیستم‌هایی که روی URL کدگذاری‌شده امضا محاسبه می‌کنند حساس‌اند. امضاهای OAuth 1.0 (RFC 5849) و AWS Signature Version 4 هر دو الزام می‌کنند که همه نویسه‌ها به‌جز نویسه‌های unreserved کدگذاری شوند، پس مقداری که آپاستروف یا پرانتز دارد می‌تواند به ناهمخوانی امضا برسد. راه‌حل رایج این است:

function encodeRFC3986(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g,
    c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
}

encodeRFC3986("it's (fine)!*");  // 'it%27s%20%28fine%29%21%2A'

URLSearchParams و علامت به‌علاوه

URLSearchParams، و در نتیجه url.searchParams، از هیچ‌کدام از این دو تابع استفاده نمی‌کند. این API از قالب application/x-www-form-urlencoded پیروی می‌کند که فرم‌های HTML با آن ارسال می‌شوند و در دو چیز متفاوت است: فاصله به + تبدیل می‌شود و فقط حروف، ارقام و * - . _ دست‌نخورده می‌مانند، پس ! ' ( ) ~ هم کدگذاری می‌شوند.

encodeURIComponent('a b+c');                      // 'a%20b%2Bc'
new URLSearchParams({ q: 'a b+c' }).toString();   // 'q=a+b%2Bc'

هر دو نتیجه در رشته کوئری درست‌اند. دردسر وقتی شروع می‌شود که سمت کدگذاری و سمت کدگشایی با هم هماهنگ نباشند. decodeURIComponent علامت + را به فاصله تبدیل نمی‌کند، پس decodeURIComponent('a+b%2Bc') نتیجه a+b+c را برمی‌گرداند، در حالی که new URLSearchParams('q=a+b%2Bc').get('q') نتیجه a b+c را. رشته‌های کوئری را با URLSearchParams کدگشایی کنید، یا پیش از صدا زدن decodeURIComponent، هر + را با فاصله جایگزین کنید. در بخش مسیر URL اما + فقط یک علامت به‌علاوه است.

تفاوت decodeURI و decodeURIComponent

توابع کدگشایی قرینه توابع کدگذاری‌اند. decodeURIComponent همه دنباله‌های escape را کدگشایی می‌کند. decodeURI دنباله‌های مربوط به نویسه‌هایی را که encodeURI کدگذاری نمی‌کرد، یعنی # $ & + , / : ; = ? @، دست‌نخورده می‌گذارد تا کدگشایی یک URL ساختار آن را تغییر ندهد: decodeURI('%C3%A9%20%2F%3F') نتیجه é %2F%3F را برمی‌گرداند، در حالی که decodeURIComponent نتیجه é /? را.

هر دو تابع خطای URIError: URI malformed می‌دهند (این متن پیام در V8 است که کروم و Node.js از آن استفاده می‌کنند)؛ یکی وقتی بعد از % دو رقم هگز نیامده باشد، مثل 100%، و دیگری وقتی بایت‌ها UTF-8 معتبر نباشند، مثل %E9 از سیستمی که é را با Latin-1 کدگذاری کرده است. توابع کدگذاری هم برای نیمه تنهای یک جفت جانشین (surrogate pair)، مثلاً رشته‌ای که وسط یک ایموجی بریده شده، همین خطا را می‌دهند.

escape() جایگزین مناسبی نیست

escape() تابعی قدیمی است که فقط برای کدهای قدیمی باقی مانده است. این تابع @ * _ + - . / را دست‌نخورده می‌گذارد و از UTF-8 استفاده نمی‌کند: نویسه‌ها تا U+00FF به یک بایت Latin-1 تبدیل می‌شوند و هر چیزی بالاتر از آن به دنباله غیراستاندارد %u. escape('café €') نتیجه caf%E9%20%u20AC را برمی‌گرداند که سرورهایی که انتظار UTF-8 دارند نمی‌توانند کدگشایی‌اش کنند، و decodeURIComponent('%E9') هم خطا می‌دهد. به‌جای آن از encodeURIComponent و به‌جای unescape() از decodeURIComponent استفاده کنید و یادتان باشد داده‌ای که قبلاً با قالب قدیمی ذخیره شده، باید تبدیل شود.

کدگذاری دوباره

هر تابع کدگذاری خود % را به %25 تبدیل می‌کند، پس اجرای دوباره آن دنباله‌هایی مثل %2520 می‌سازد: encodeURIComponent(encodeURIComponent('a b')) نتیجه a%2520b را برمی‌گرداند. مقدارهای خام را دقیقاً یک بار، همان لحظه‌ای که وارد URL می‌شوند، کدگذاری کنید و رشته‌ای را که کدگذاری‌شده به دستتان رسیده هرگز دوباره کدگذاری نکنید. تنها حالت مشروع، URL داخل یک URL دیگر است: کدگذاری https://example.com/?q=a%20b به‌عنوان پارامتر، به‌درستی ...q%3Da%2520b می‌دهد و سمت گیرنده یک بار کدگشایی می‌کند تا URL درونی را دوباره به دست آورد.

برای مقایسه دو حالت روی ورودی خودتان، کدگذار URL یک انتخابگر برای encodeURIComponent و encodeURI دارد. کدگشای URL همزمان با تایپ کدگشایی می‌کند و مثل داده فرم، + را فاصله در نظر می‌گیرد؛ پس مقداری که علامت به‌علاوه واقعی دارد باید آن را به شکل %2B داشته باشد.

سوالات متداول

encodeURIComponent کدام نویسه‌ها را کدگذاری نمی‌کند؟

حروف، ارقام و - _ . ! ~ * ' ( ). هر چیز دیگری، از جمله / ? & = # و فاصله، به شکل بایت‌های UTF-8 کدگذاری درصدی می‌شود.

آیا برای یک URL کامل از encodeURI استفاده کنم؟

فقط وقتی URL از قبل ساختار درستی دارد و تنها فاصله یا نویسه‌های غیر ASCII در آن هست. برای ساختن URL از چند بخش، هر بخش را با encodeURIComponent کدگذاری کنید یا از APIهای URL و URLSearchParams استفاده کنید.

چرا URLSearchParams به‌جای %20 علامت + می‌گذارد؟

این API از قالب application/x-www-form-urlencoded فرم‌های HTML استفاده می‌کند که در آن + به معنای فاصله است. سرورها رشته‌های کوئری را به همین شکل کدگشایی می‌کنند، پس هر دو شکل در رشته کوئری کار می‌کنند، اما decodeURIComponent به‌تنها + را به فاصله برنمی‌گرداند.

آیا escape() همان encodeURIComponent است؟

خیر. escape() تابعی قدیمی است که نویسه‌های غیر ASCII را به‌جای UTF-8 به شکل Latin-1 یا دنباله‌های %uXXXX کدگذاری می‌کند و @ + / را کدگذاری نمی‌کند. به‌جای آن از encodeURIComponent استفاده کنید.
همین حالا امتحان کنید: کدگذار و کدگشای URL رمزگشای URL