Недавно проделана работа по написанию пользовательского скрипта (браузеры Firefox, Chrome и Opera), в котором понадобилось обращаться к документу XML, находящемуся в старшем домене 2-го уровня из домена 3-го уровня. Работа открыла взгляд на некоторые особенности поведения браузеров, особенно, Оперы, причины которых до конца не выяснены. Но, так как такой скриптинг (чтение и запись в документы XML в наддомене) иногда необходим, хотел бы поделиться практическими результатами и показать открытые вопросы.
Есть страница HTML с не строгим Doctype (transitional), которая должна получить данные из некоторого XML, который, к тому же, мы не можем менять и вписывать в него свои скрипты (как если бы он был XHTML). Страницу HTML тоже не можем менять, внедряя, например, XML, а управляем всеми действиями лишь через пользовательский скрипт, запускаемый в момент onload. Первая мысль — нет ничего проще, классическая задача с XMLHttpRequest и перестройкой DOM по нашим нуждам.
Да, если бы XML был на том же домене, то нет ничего проще. Но домен другой, поэтому AJAX-request тут был бы возможен, но сложно-избыточным образом: 1) загрузить какой-то существующий простой HTML из наддомена в фрейм, 2) вписать скриптом в него скрипт AJAX, 3) прочитать XML по AJAX, 4) прочитать скриптом из поддомена полученное. Зачем делать 2 чтения, если можно обойтись одним чтением в фрейм одного XML? Подгружать код в фрейм не надо, только читать — тоже проще. Поэтому исключаем AJAX как метод, сопровождающий сложный способ и пробуем делать простым.
Чтение JS-данных и DOM-документа в наддомене, как известно, можно делать, указав document.domain в поддомене, равным старшему домену:
Действительно, в FF и Chrome работа по этой схеме происходит без особенностей, и можно было бы об ней не писать, но в Опере возникли странные нерешённые проблемы (может быть, от XML?), которые были просто обойдены, но они экспериментально наблюдаются. О них и некоторых побочных результатах в Firefox — эта статья.
Поскольку скрипты пользовательские, IE в исследовании делать было нечего.
Работающий пример скрипта можно посмотреть в Firefox+GreeaseMonkey, Chrome или Opera, установив скрипт, описанный в статье (последнюю версию 1.3).
Будем говорить только о сути — о той части скрипта, которая относится к вопросу кроссдоменного доступа.
Мы полностью отказались от AJAX на XMLHttp, и теперь надо сделать фрейм, в который подгружается XML.
XML имеет такую структуру:
Затем включаем счётчик периодических попыток чтения структуры XML, потому что событие onload в чужом фрейме ловить не можем. (На самом деле, в Опере это возможно, код и пояснения ниже, но пробы чтения дали ещё худшие результаты, о них позже.)
Тут начинается самое интересное. Во-первых, чтобы не иметь ошибок, приходится, как сапёру, проверять дерево шаг за шагом (конечно, можно и ловить ошибки в try-catch).
Почему во втором операторе if пришлось разделить «Оперную» и не-Оперную части? Потому что в Опере мы смогли записать в документ XML переменную u.
В FF этого сделать не смогли (почему — вопрос открытый), но этого не очень хотелось, потому что есть вторая часть условия: длинное выражение, читающее ноду в теге — это тот же запомненный username, который читался в FF/Chrome без проблем. Что же за проблемы были в Опере?
Проблемы (вопрос второй) были странные и слабообъяснимые. Пытаясь читать логин вторым способом, по нодам, а не записанный заранее, получали существование ноды с логином, getElementsByTagName('login')[0], но отсутствие .childNodes — текста в логине. Т.е. так, как будто бы XML был вида
Не помогали ни задержки, ни танцы типа чтения getElementsByTagName('habrauser')[0].childNodes[1] ([1] — потому что там текстовые ноды на месте переносов строк). <login /> казался пустым с точки зрения Оперы. Почему — второй открытый вопрос. (В фрейме он был непустым и виделся, если не писать ifr.style.display='none'; .)
Несмотря на непонятность поведения первой ноды, пришлось скрипт оставить для Оперы в таком виде — по случайному совпадению работоспособных альтернатив, мы всё равно получили заменитель — переменную u в документе. Но решение в общем виде, если бы <karma> стояла первой, было бы для Оперы невыполнимой задачей (если решать этим путём).
Наконец, третий вопрос и особенность Оперы и FF. В документе «Web Technologies for Opera Web Applications» я подсмотрел хак для подключения onload к фрейму. Что интересно, вид DOM-документа, видимого в момент onload, был ещё хуже. Не виделись ноды не только с логином, но и с кармой. Получается приблизительно такой эффект, как неточный выбор момента onload, но не совсем так — нода login[0].firstChild не видится продолжительное время, если не сказать, что всегда. Что с этим всем делать и как избежать? Может быть, Опера «захлёбывается» в длинной череде проверок нод и надо их делать как-то иначе? Никто не сталкивался с этой ситуацией?
Как будет видеться произвольный документ в Опере через ноды — теоретический вопрос. Пока на него нет желания отвечать, потому что непонятен сам смысл происходящего, поэтому предсказывать поведение и прощупывать результаты некуда.
1. Опера умеет писать в наддомен в фрейме через contentDocument и contentWindow новые переменные. Firefox не умеет, но через contentWindow при этом не выдаёт ошибку — просто undefined. (Вызов фрейма — в ипостаси документа: document.getElementsByName('ifr'), поэтому правильнее было бы обращение через contentDocument, но интересно, что FF не срабатывает, выдавая uncaught error.)
2. Опера умеет делать хак для onload чужого документа, чтобы выполнить код в поддомене после формирования документа, Firefox не умеет. (Хотя пользы для XML у Оперы оказалось мало.)
3. С Оперой или кодом проверки существования ноды, который приводит к эффекту нечитаемости первой текстовой ноды, в то время, когда она существует, надо разобраться — как правильно обращаться к нодам, не есть ли это влияние кроссдоменности, встречали ли другие разработчики подобные эффекты.
Условия задачи.
Есть страница HTML с не строгим Doctype (transitional), которая должна получить данные из некоторого XML, который, к тому же, мы не можем менять и вписывать в него свои скрипты (как если бы он был XHTML). Страницу HTML тоже не можем менять, внедряя, например, XML, а управляем всеми действиями лишь через пользовательский скрипт, запускаемый в момент onload. Первая мысль — нет ничего проще, классическая задача с XMLHttpRequest и перестройкой DOM по нашим нуждам.
Да, если бы XML был на том же домене, то нет ничего проще. Но домен другой, поэтому AJAX-request тут был бы возможен, но сложно-избыточным образом: 1) загрузить какой-то существующий простой HTML из наддомена в фрейм, 2) вписать скриптом в него скрипт AJAX, 3) прочитать XML по AJAX, 4) прочитать скриптом из поддомена полученное. Зачем делать 2 чтения, если можно обойтись одним чтением в фрейм одного XML? Подгружать код в фрейм не надо, только читать — тоже проще. Поэтому исключаем AJAX как метод, сопровождающий сложный способ и пробуем делать простым.
Чтение JS-данных и DOM-документа в наддомене, как известно, можно делать, указав document.domain в поддомене, равным старшему домену:
document.domain = 'сайт.ру';
Действительно, в FF и Chrome работа по этой схеме происходит без особенностей, и можно было бы об ней не писать, но в Опере возникли странные нерешённые проблемы (может быть, от XML?), которые были просто обойдены, но они экспериментально наблюдаются. О них и некоторых побочных результатах в Firefox — эта статья.
Поскольку скрипты пользовательские, IE в исследовании делать было нечего.
Работающий пример скрипта можно посмотреть в Firefox+GreeaseMonkey, Chrome или Opera, установив скрипт, описанный в статье (последнюю версию 1.3).
Процесс работы скрипта.
Будем говорить только о сути — о той части скрипта, которая относится к вопросу кроссдоменного доступа.
Мы полностью отказались от AJAX на XMLHttp, и теперь надо сделать фрейм, в который подгружается XML.
if(!document.getElementsByName('ifr').length){ //создание фрейма
var ifr=document.createElement('iframe');
ifr.setAttribute('name', 'ifr');
ifr.src = 'http://habrahabr.ru/api/profile/'+username+'/';
ifr.style.display='none';
document.body.appendChild(ifr);
}
XML имеет такую структуру:
<?xml version="1.0"?>
<habrauser>
<login>spmbt</login>
<karma>24</karma>
<rating>59.3</rating>
<ratingPosition>1038</ratingPosition>
</habrauser>
Затем включаем счётчик периодических попыток чтения структуры XML, потому что событие onload в чужом фрейме ловить не можем. (На самом деле, в Опере это возможно, код и пояснения ниже, но пробы чтения дали ещё худшие результаты, о них позже.)
win.habrKarmView.ii=20; //число попыток прочитать фрейм
win.habrKarmView.ww = setInterval(showValue, 300);
Тут начинается самое интересное. Во-первых, чтобы не иметь ошибок, приходится, как сапёру, проверять дерево шаг за шагом (конечно, можно и ловить ошибки в try-catch).
var f = document.getElementsByName('ifr');
if(f && f[0] && f[0].contentDocument && f[0].contentDocument.getElementsByTagName('login')
&& f[0].contentDocument.getElementsByTagName('login')[0]
&& f[0].contentDocument.getElementsByTagName('karma')[0]){
if( (f[0].contentDocument.u == username || !f[0].contentDocument.u) && self.opera
|| !self.opera && f[0].contentDocument.getElementsByTagName('login')[0].childNodes[0].nodeValue == username ){
...тело функции showValue - отображаем полученные из XML данные...
}
}
Почему во втором операторе if пришлось разделить «Оперную» и не-Оперную части? Потому что в Опере мы смогли записать в документ XML переменную u.
if(self.opera) document.getElementsByName('ifr')[0].contentDocument.u = username;
В FF этого сделать не смогли (почему — вопрос открытый), но этого не очень хотелось, потому что есть вторая часть условия: длинное выражение, читающее ноду в теге — это тот же запомненный username, который читался в FF/Chrome без проблем. Что же за проблемы были в Опере?
Проблемы (вопрос второй) были странные и слабообъяснимые. Пытаясь читать логин вторым способом, по нодам, а не записанный заранее, получали существование ноды с логином, getElementsByTagName('login')[0], но отсутствие .childNodes — текста в логине. Т.е. так, как будто бы XML был вида
<habrauser>
<login />
<karma>24</karma>
<rating>59.3</rating>
<ratingPosition>1038</ratingPosition>
</habrauser>
Не помогали ни задержки, ни танцы типа чтения getElementsByTagName('habrauser')[0].childNodes[1] ([1] — потому что там текстовые ноды на месте переносов строк). <login /> казался пустым с точки зрения Оперы. Почему — второй открытый вопрос. (В фрейме он был непустым и виделся, если не писать ifr.style.display='none'; .)
Несмотря на непонятность поведения первой ноды, пришлось скрипт оставить для Оперы в таком виде — по случайному совпадению работоспособных альтернатив, мы всё равно получили заменитель — переменную u в документе. Но решение в общем виде, если бы <karma> стояла первой, было бы для Оперы невыполнимой задачей (если решать этим путём).
Наконец, третий вопрос и особенность Оперы и FF. В документе «Web Technologies for Opera Web Applications» я подсмотрел хак для подключения onload к фрейму. Что интересно, вид DOM-документа, видимого в момент onload, был ещё хуже. Не виделись ноды не только с логином, но и с кармой. Получается приблизительно такой эффект, как неточный выбор момента onload, но не совсем так — нода login[0].firstChild не видится продолжительное время, если не сказать, что всегда. Что с этим всем делать и как избежать? Может быть, Опера «захлёбывается» в длинной череде проверок нод и надо их делать как-то иначе? Никто не сталкивался с этой ситуацией?
Как будет видеться произвольный документ в Опере через ноды — теоретический вопрос. Пока на него нет желания отвечать, потому что непонятен сам смысл происходящего, поэтому предсказывать поведение и прощупывать результаты некуда.
Полезные знания и выводы.
1. Опера умеет писать в наддомен в фрейме через contentDocument и contentWindow новые переменные. Firefox не умеет, но через contentWindow при этом не выдаёт ошибку — просто undefined. (Вызов фрейма — в ипостаси документа: document.getElementsByName('ifr'), поэтому правильнее было бы обращение через contentDocument, но интересно, что FF не срабатывает, выдавая uncaught error.)
if(self.opera)
document.getElementsByName('ifr')[0].contentDocument.u = username;
2. Опера умеет делать хак для onload чужого документа, чтобы выполнить код в поддомене после формирования документа, Firefox не умеет. (Хотя пользы для XML у Оперы оказалось мало.)
var ifr=document.createElement('iframe');
ifr.src = 'http://habrahabr.ru/api/profile/'+username+'/';
ifr.style.display='none';
ifr.onload = function(){
...;
}
document.body.appendChild(ifr);
3. С Оперой или кодом проверки существования ноды, который приводит к эффекту нечитаемости первой текстовой ноды, в то время, когда она существует, надо разобраться — как правильно обращаться к нодам, не есть ли это влияние кроссдоменности, встречали ли другие разработчики подобные эффекты.