encodeURI أم encodeURIComponent: أيهما تستخدم؟

ترمّز encodeURIComponent أحد عشر محرفًا تتركها encodeURI كما هي، وهذه المحارف الأحد عشر هي ما يقرّر إن كانت القيمة ستبقى داخل معاملها.

في JavaScript دالتان مدمجتان للترميز بالنسبة المئوية، ولا تختلفان إلا في أمر واحد: مقدار ما تتركانه دون مساس من بنية الرابط. تفترض encodeURI أنها تتلقى رابطًا كاملًا، فتُبقي المحارف التي تمنح الرابط بنيته، مثل / و? و&. أما encodeURIComponent فتفترض أنها تتلقى قطعة واحدة من البيانات، فترمّز هذه المحارف أيضًا حتى لا تُحسب البيانات جزءًا من البنية. الخلاصة: لوضع قيمة داخل رابط استخدم encodeURIComponent، أو الأفضل منها واجهة URL.

الفرق بالضبط

تحوّل الدالتان كل محرف لا تتركانه إلى UTF-8، وتكتبان كل بايت على شكل % يتبعها رقمان ست عشريان بأحرف كبيرة، فيصبح é هو %C3%A9، ويصبح € هو %E2%82%AC. والفرق الوحيد بينهما هو مجموعة محارف ASCII القابلة للطباعة التي تتركها كل دالة دون ترميز:

المحارفencodeURIencodeURIComponent
A-Z a-z 0-9 - _ . ! ~ * ' ( )تبقى كما هيتبقى كما هي
# $ & + , / : ; = ? @تبقى كما هيتُرمَّز
المسافة و" % < > [ \ ] ^ ` { | }تُرمَّزتُرمَّز
محارف التحكم وكل ما هو خارج ASCIIتُرمَّز كبايتات UTF-8تُرمَّز كبايتات UTF-8

الصف الأوسط هو القصة كلها. فهذه المحارف الأحد عشر تفصل أجزاء الرابط بعضها عن بعض، وتركها دون ترميز داخل قيمة ما يغيّر معنى الرابط:

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

متى تستخدم كل دالة

استخدم encodeURIComponent لكل ما تُدخله في رابط: قيمة معامل استعلام أو اسمه، أو جزء واحد من المسار، أو معرّف الجزء (fragment)، أو رابط كامل يُمرَّر كمعامل مثل 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/، وهذا لم يعد اسم مضيف صالحًا.

وإن كنت تجمّع رابطًا من أجزائه، فإن new 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 مجموعتين من المحارف. المحارف غير المحجوزة (القسم 2.3) لا تحتاج إلى ترميز أبدًا: الحروف والأرقام و- . _ ~. أما المحارف المحجوزة (القسم 2.2) فلها دور نحوي في بنية الرابط: الفواصل العامة : / ? # [ ] @ والفواصل الفرعية ! $ & ' ( ) * + , ; =.

ولا تطابق أيٌّ من دالتَي JavaScript هذا التقسيم تمامًا. فـ encodeURIComponent تترك خمسة من الفواصل الفرعية دون ترميز: ! ' ( ) *. المتصفحات ومعظم الخوادم لا تكترث لذلك، لكن الأنظمة التي تحسب تواقيع على الرابط المرمَّز تكترث. فتواقيع OAuth 1.0 (RFC 5849) وAWS Signature Version 4 تشترط كلتاهما ترميز كل محرف عدا المحارف غير المحجوزة، لذا قد تتسبّب قيمة فيها فاصلة عليا أو أقواس في عدم تطابق التوقيع. والحل المعتاد:

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، أيًّا من الدالتين. بل تتبع صيغة 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. أما داخل المسار فـ + مجرد علامة زائد لا أكثر.

decodeURI أم decodeURIComponent

دالتا فك الترميز صورة معكوسة لدالتي الترميز. تفكّ decodeURIComponent كل تسلسل هروب. أما decodeURI فتترك تسلسلات الهروب الخاصة بالمحارف التي ما كانت encodeURI لترمّزها، أي # $ & + , / : ; = ? @، حتى لا يغيّر فك ترميز الرابط بنيته: تعيد decodeURI('%C3%A9%20%2F%3F') القيمة é %2F%3F، بينما تعيد decodeURIComponent القيمة é /?.

وترمي الدالتان الخطأ URIError: URI malformed (وهي صياغة V8 المستخدمة في Chrome و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. رمّز القيم الخام مرة واحدة بالضبط، لحظة دخولها إلى الرابط، ولا ترمّز أبدًا نصًا وصلك مرمَّزًا. والحالة المشروعة الوحيدة هي رابط داخل رابط آخر: ترميز https://example.com/?q=a%20b كمعامل يعطي بشكل صحيح ...q%3Da%2520b، ويفكّه الطرف المستقبِل مرة واحدة ليستعيد الرابط الداخلي.

ولمقارنة الوضعين على مدخلاتك أنت، يضم مُرمِّز الروابط محدّدًا للاختيار بين encodeURIComponent وencodeURI. أما أداة فك ترميز الروابط فتفكّ الترميز أثناء الكتابة وتعامل + كمسافة، كما تفعل بيانات النماذج، لذا يجب أن تحمل القيمة التي تحتوي علامة زائد حقيقية هذه العلامة على شكل %2B.

الأسئلة الشائعة

ما المحارف التي لا ترمّزها encodeURIComponent؟

الحروف والأرقام و- _ . ! ~ * ' ( ). وكل ما عداها يُرمَّز بالنسبة المئوية كبايتات UTF-8، بما في ذلك / ? & = # والمسافة.

هل أستخدم encodeURI مع رابط كامل؟

فقط حين يكون الرابط مبنيًا بشكل صحيح ولا يحتوي إلا مسافات أو محارف خارج ASCII. ولبناء رابط من أجزائه، رمّز كل جزء بـ encodeURIComponent أو استخدم واجهتي URL وURLSearchParams.

لماذا تُنتج URLSearchParams العلامة + بدل %20؟

لأنها تستخدم صيغة application/x-www-form-urlencoded الخاصة بنماذج HTML، وفيها تمثّل + مسافة. والخوادم تفكّ سلاسل الاستعلام بهذه الطريقة، لذا يعمل الشكلان داخل سلسلة الاستعلام، لكن decodeURIComponent وحدها لا تعيد + إلى مسافة.

هل escape() مثل encodeURIComponent؟

لا. escape() دالة قديمة ترمّز المحارف خارج ASCII كتسلسلات Latin-1 أو %uXXXX بدل UTF-8، وتترك @ + / دون ترميز. استخدم encodeURIComponent بدلًا منها.