Русский

Инструменты разработчика · Кодер и декодер URL

escape() против encodeURIComponent: как развивалась кодировка URL JavaScript

· Фон

javascript URL-кодировка история

Три поколения функций кодирования JavaScript URL
Оригинальная векторная иллюстрация ToolAcre

JavaScript имеет три поколения функций кодирования URL, и самое старое из них все еще скрывается в рабочем коде. В этом посте объясняется, что escape() делает неправильно, почему ES3 добавил функции URI и почему они сохраняют ! * ' ( ).

%u20AC в устаревшем журнале — безошибочный отпечаток escape() и вызванный им сбой декодирования

Устаревший файл JavaScript содержит вызов кодировки URL с использованием устаревшей функции escape(). Вывод в файле журнала или сообщении об ошибке включает последовательность %u20AC — безошибочный отпечаток устаревшей функции escape(), которую больше никто не использует. Эта последовательность не соответствует ни одной стандартной кодировке URL, и декодер, построенный на правилах RFC, 3986 или WHATWG, не распознает ее. Данные не могут пройти туда и обратно с помощью современных инструментов. Это распространенный признак кода, который появился раньше ES3 и не обновлялся с 1990-х годов.

Функция escape() была разработана в эпоху Netscape, до того, как JavaScript имел стандарты или формальные правила кодирования URL. Он кодирует большинство символов, отличных от ASCII, с использованием нотации %uXXXX, четырехзначного шестнадцатеричного кода, который больше никто не использует и нигде не определяет ни один стандарт. Это имело смысл для одноразового использования в браузере, но нарушало совместимость со стандартами URL и делало невозможным декодирование данных где-либо еще.

escape() и unescape(): дизайн эпохи Netscape — предположения Latin-1, изобретение %uXXXX и почему оно никогда не соответствовало ни одному стандарту

escape() и unescape() предполагают, что входные данные имеют формат Latin-1 (ISO 8859-1), кодировку символов, предшествовавшую UTF-8 и Unicode. Они преобразуют каждый символ в шестнадцатеричный код, используя %XX для высокобитовых символов Latin-1 и %uXXXX для всего, что находится за пределами Latin-1. Нелатинские символы 1, такие как эмодзи, вообще не могут быть представлены. Функции просты и быстры, но они совершенно не подходят для любого современного варианта использования.

Обе функции были добавлены в JavaScript до появления стандартов. Они были признаны устаревшими сразу после того, как ES3 ввел правильную кодировку URL в 1999. Они остаются в JavaScript для обратной совместимости — их удаление нарушит древний код. Но любой новый код никогда не должен их использовать. Они являются реликвией наследия.

ES3 (1999) добавляет encodeURI и encodeURIComponent — UTF-8 процентное кодирование, согласованное с RFC 2396

ES3 представил две функции: encodeURI и encodeURIComponent. Оба выполняют процентное кодирование UTF-8: преобразуют символы, отличные от ASCII, в UTF-8 байты, а затем записывают каждый байт как %HH. Оба соответствуют RFC 2396, который был текущим на тот момент. RFC 3986 появился позже и не изменил поведение кодирования. Эти функции до сих пор являются стандартом и их следует использовать.

encodeURI предназначен для полного кодирования URI; encodeURIComponent предназначен для кодирования компонента внутри URI, например значения запроса или сегмента пути. Разница абсолютно критична, и ее легко понять неправильно. encodeURI сохраняет структурные символы, такие как: /? # @ = & и ;. encodeURIComponent кодирует все это, оставляя их безопасными для встраивания в более крупный URI.

Почему ! * ' ( ) по-прежнему остаются незакодированными — символы «маркировки» RFC 2396 заморожены в языке после того, как RFC 3986 переместил их

Обе функции оставляют незакодированными следующие символы: буквы, цифры, дефис (-), подчеркивание (_), точку (.), тильду (~) и пять знаков препинания! * ' ( ). Знаки взяты из RFC 2396, где они указаны как незарезервированные символы «знаков». RFC 3986 появился в 2005 и переместил эти пять в другую категорию, но JavaScript уже заморозил encodeURI и encodeURIComponent в 1999. Изменение того, какие символы они оставят в покое, нарушит существующий код, поэтому они остались.

Решение оставить эти пять знаков незакодированными для обратной совместимости означает, что кодировка JavaScript не полностью соответствует ни стандарту RFC 3986, ни стандарту WHATWG. Он достаточно близок для практического использования, и изменить его сейчас совершенно невозможно. Это урок API стабильности: если вы заморозите поведение, вы не сможете его изменить, даже если стандарт будет развиваться.

Рабочий пример: одна и та же строка через escape, encodeURI и encodeURIComponent — сравнение трёх выходных данных

Возьмем строку «НИОКР (исследования) = кафе». Запустите его через escape(), encodeURI и encodeURIComponent. escape() создает "R%26D%20(research)%20%3D%20caf%E9", смешивая незакодированные круглые скобки и апостроф с амперсандом, закодированным в процентах, и равными. encodeURI создает «R&D%20(research)%20=%20caf%C3%A9's», оставляя амперсанд и знаки равенства в покое, поскольку они являются структурными. encodeURIComponent создает «R%26D%20%28research%29%20%3D%20caf%C3%A9%27s», кодируя все, включая круглые скобки и апостроф.

Вставьте ту же строку в кодировщик и декодер URL и переключайтесь между encodeURI и encodeURIComponent, чтобы увидеть разницу. Затем проверьте, что выдаст escape() (вы можете вызвать его в консоли браузера, хотя он вас предупредит). Вы сразу видите, что три функции дают три совершенно разных результата.

Переход от escape() — сопоставление старых вызовов с правильной современной функцией и обработка сохраненных данных %uXXXX

Старый код, использующий escape(), необходимо обновить. Если escape() использовался для кодирования компонента URI, замените его на encodeURIComponent. Если он использовался для полного кодирования URI, используйте encodeURI. Для сохраненных данных, содержащих последовательности %uXXXX, вам понадобится специальный декодер: преобразуйте каждый %uXXXX в кодовую точку Unicode, а затем соберите кодовые точки в строку. Встроенная функция unescape() JavaScript прочитает %uXXXX, но результат может быть неправильным UTF-8.

После замены escape() протестируйте код со строками, содержащими символы, отличные от ASCII, знаки препинания и специальные символы. Теперь выходные данные должны соответствовать ожиданиям современных инструментов и стандартов. Если ваш код значительно старше ES3, он также может использовать другие устаревшие шаблоны; комплексный аудит того стоит.

Что здесь не распространяется — API URL и URLSearchParams, которые рассматриваются отдельно.

API URL и URLSearchParams, добавленные намного позже, предоставляют интерфейсы более высокого уровня для построения URL и кодирования компонентов. Они обрабатывают все экранирование автоматически и точно соответствуют стандарту WHATWG URL. Это предпочтительный способ программного создания URL-адресов в современном JavaScript.

В этом посте рассматриваются только функции кодирования, а не API более высокого уровня. URL и URLSearchParams анализируют структуру, выбирают правила компонента и сериализуют результат, тогда как encodeURIComponent преобразует одну предоставленную строку, не зная, где она будет размещена. Это различие является границей: перенесите старый вызов escape() в зависимости от того, обрабатывает ли он значение или адрес, а затем рассмотрите возможность замены окружающей ручной конкатенации структурированными API в качестве отдельного рефакторинга.

Вывод: три функции, одна сохранившаяся пара — как кодировщик и декодер URL бок о бок демонстрируют современное поведение encodeURI и encodeURIComponent.

Современная разработка JavaScript должна использовать encodeURI или encodeURIComponent, а не escape(). Функции были стандартизированы в 1999 и с тех пор не менялись. Они кодируют символы, отличные от ASCII, как UTF-8 байты и правильно обрабатывают стандартные зарезервированные символы. Инструмент кодирования и декодера URL реализует обе функции и позволяет вам видеть их поведение одновременно, что упрощает выбор подходящей функции для вашего компонента.

Если вы встретите последовательности %u в старых журналах или сохраненных данных, это выходные данные escape(), и их следует перенести. Миграция становится простой, как только вы определите шаблон. Современный код никогда не должен их создавать.