HTML5 History.pushState управляет URL-адресом, содержащим символы с кодировкой процента, не являющейся Ascii (Unicode)
В веб-приложении OSS у нас есть JS-код, который выполняет некоторое обновление Ajax (использует jQuery, не имеет значения). После обновления страницы вызов выполняется в интерфейсе истории html5 History.pushState
в следующем коде:
var updateHistory = function(url) {
var context = { state:1, rand:Math.random() };
/* -----> bedfore the problem call <------- */
History.pushState( context, "Questions", url );
/* -----> after the problem call <------- */
setTimeout(function (){
/* HACK: For some weird reson, sometimes something overrides the above pushState so we re-aplly it
This might be caused by some other JS plugin.
The delay of 10msec allows the other plugin to override the URL.
*/
History.replaceState( context, "Questions", url );
}, 10);
};
[ Обратите внимание:: полный контекст кода предоставляется для контекста, часть HACK не является проблемой этого вопроса]
Приложение i18n'ed и использует URL-кодированные Unicode-сегменты в URL-адресах, поэтому непосредственно перед помеченным вызовом проблемы в приведенном выше коде аргумент URL содержит (как проверено в Firebug):
"/%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9/scope:all/sort:activity-desc/page:1/"
Закодированный сегмент является utf-8 в процентном кодировании. URL-адрес в окне браузера: (просто для полноты, не имеет значения)
http://<base-url>/%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9/
После вызова URL, отображаемый в окне браузера, изменится на:
http://<base-url>/%C3%98%C2%A7%C3%99%C2%84%C3%98%C2%A3%C3%98%C2%B3%C3%98%C2%A6%C3%99%C2%84%C3%98%C2%A9/scope:all/sort:activity-desc/page:1/
Сегмент, кодированный URL-адресом, - это просто mojibake, результат использования неправильной кодировки на некотором уровне. Правильный URL-адрес:
http://<base-url>/%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9/scope:all/sort:activity-desc/page:1/
Это поведение было протестировано как для FF, так и для Chrome.
Интерфейс истории specs ничего не упоминает о кодированных URL-адресах, но я предполагаю стандарт по умолчанию для формирования URL-адреса (utf-8 и процентное кодирование и т.д.) будут применяться при использовании URL-адреса в вызовах функций для интерфейса.
Любая идея о том, что происходит здесь.
Изменить:
Я не обращал внимания на верхний регистр H в истории - этот код фактически использует обертку History.js для интерфейса истории. Я заменил прямой вызов History.pushState
(обратите внимание на нижний регистр h), не пройдя через обертку, и код работает как ожидалось, насколько я могу судить. Проблема с исходным кодом все еще стоит - так что проблема с библиотекой History.js кажется.
Ответы
Ответ 1
Update
Как Doug S объясняет в комментариях ниже, последняя версия History.js включает исправление для этого поведения. Он также нашел что мое решение вызвало двойное кодирование при использовании в браузерах (например, IE 9 и ниже), для которых требуется хеш-резерв, поэтому Я рекомендую вместо использования исправления, подробно описанного ниже, просто загрузить последнюю версию.
Я сохранил свой первоначальный ответ ниже, так как он объясняет, что происходит более подробно.
Базель нашел решение своего рода, но есть еще некоторое замешательство в том, что происходит под капотом. Этот ответ подробно рассказывает о проблеме и предлагает лучшее исправление. (Если хотите, вы можете перейти к исправлению.)
Проблема
Сначала откройте консоль JS вашего браузера и запустите это:
window.encodeURI(window.unescape('%D8%A7%D9%84%D8%A3%D8%B3%D8%A6%D9%84%D8%A9'))
Это выглядит знакомо? Должно быть, что ваш URL-адрес искалечен. Проблема заключается в реализации History.unescapeString
, а именно в этой строке:
tmp = window.unescape(result);
window.unescape
- это функция DOM Level 0, то есть нестандартная реликвия из седых дней Netscape 2. Это использует правила экранирования, определенные в RFC 2396, в соответствии с которыми кодируются символы за пределами безоговорочного диапазона (буквенно-цифровые символы и небольшой набор символов пунктуации) как октеты.
Это отлично подходит для диапазона US-ASCII, но не все (действительно, подавляющее большинство) символов в UTF-8 могут быть представлены в одном байте. Поскольку URI не имеют встроенного способа представления используемого набора символов, window.unescape
просто предполагает, что каждый символ сопоставляется с одним октетом и blithely искажает любые, которые этого не делают.
В этом примере первая буква вашего URL-адреса - арабская буква alef (ا), представленная двумя байтами: 0xD8 0xA7
, window.unescape
интерпретирует их как два отдельных символа: 0x00 0xD8
(Ø-capital O с инсультом) и 0x00 0xA7
(знак §-секции).
Это известная проблема с History.js.
Исправление
Как уже отмечалось выше, вопрос может быть обойден, используя собственную реализацию API истории, а не обложку History.js, т.е. history.pushState
вместо history.pushState
.
Это работает для браузеров, поддерживающих API истории, но теряет преимущество polyfill для тех, кто этого не делает. К счастью, есть лучшее исправление. Откройте источник History.js, на который вы ссылаетесь, и найдите эту строку (~ 1059 в моей копии):
tmp = window.unescape(result);
Замените его:
tmp = window.unescape(encodeURIComponent(result));
Или, если вы используете сжатый источник, замените a.unescape(c)
на a.unescape(encodeURIComponent(c))
.
Чтобы протестировать это изменение, я запустил тестовый набор test.js HTML5 jQuery на локальном веб-сервере внутри каталога с арабским именем. Перед тем, как внести изменения, тест 14 терпит неудачу; после изменения все тесты прошли.
Кредит
Хотя я нашел проблему и решение самостоятельно, Damien Antipa заслуживает признания за то, что нашел его первым и сделал запрос на удаление с исправлением.
Ответ 2
Я все еще могу воспроизвести это в следующем случае:
History.pushState(null, null, "?" + some_Unicode_String_Or_A_String_With_Whitespace);
document.location.hash += "&someStuff";
В этом случае параметр _suid удаляется и также некоторый элемент. Если строка не является юникодом или не имеет пробелов (так что нет символов%) - этого не происходит.
Этот обходной путь работал у меня:
History.pushState(null, null, "?" + some_Unicode_String_Or_A_String_With_Whitespace + "&someStuff");