Деловая неделя

Набирать буквы в выпадающем списке SELECT надо достаточно быстро (не делая больших пауз), тогда список будет прокручиваться до нужного слова. На практике же чаще всего пользователь крутит список мышкой и ищет нужное слово глазами, а это удобнее делать с простым списком, а не с выпадающим.

ЕЖЕНЕДЕЛЬНАЯ РЕКЛАМНАЯ ГАЗЕТА ДЛЯ ПРЕДПРИЯТИЙ «Деловая неделя» (Иркутск)

Автозапчасти в Иркутске: что внутри (PHP, Javascript)

PHP-программист, среди всего прочего, должен решать две базовые технологические задачи: 1) правильно распределить нагрузку между сервером и клиентом (браузером); 2) свести к минимуму их общение (трафик). Можно рассматривать это и как одну задачу, а можно разбить и на более мелкие – это неважно, а важно то, что создатели сайтов обычно вообще такие задачи перед собой не ставят.

По умолчанию в настройках PHP значение параметра _session.cache_limiter__ обычно равно nocache. Это значит, PHP будет со всеми своими скриптами отправлять http-заголовки Cache-Control: no-store, no-cache, Pragma: no-cache и всё в таком духе – сообщающее клиенту (браузеру), что каждую страницу нужно каждый раз скачивать с сервера заново. Иногда это действительно бывает нужно – например, при активной переписке на форуме. Но и то далеко не каждую страницу. Чаще же всего никто просто не обращает на подобные «мелочи» внимания. Если начать обращать внимание (как того требует протокол HTTP), то возникнет целая куча новых проблем. В основном о них здесь и пойдёт речь.

Большие списки (словари)

Каталог автозапчастей достаточно сложен по самой своей сути (многообразие наименований и параметров), поэтому очень показателен, пригоден для отладки клиент-серверных задач. Типичной особенностью всех интернет-каталогов (доски объявлений, каталоги сайтов, предприятий...) являются так называемые словари: списки рубрик, наименований, городов и проч. Эти словари могут использоваться как при добавлении новой информации, так и при поиске. Обычно они присутствуют на странице в виде выпадающих списков (элемент HTML SELECT): – именно в таком виде этот список отображается на страницах нашего каталога автозапчастей.

Длина основного кода этого словаря – 123 символа. Если в словаре будет не 3 слова, а 1300, как в списке наименований запчастей, то его длина будет уже 50 килобайт. Таких списка у нас два (ещё словарь моделей автомашин), и эти 100 Кб будут каждый раз загружаться заново при самом небольшом изменении текста страницы. HTTP протокол тут помочь не может: если мы через него скажем браузеру, что страницу надо кэшировать, браузер не будет видеть текущих изменений (если мы, например, добавим новую запчасть в таблицу Mysql, она не появится для нас в общем списке).

Мы решали проблему словарей в двух направлениях: уменьшая размер кода словарей и, в конце концов, полностью удалив оба больших словаря с главной страницы. Максимально уменьшить размер отправляемого с сервера словаря можно, отправляя список в предельно простом виде: <div id="list">"Амортизаторы;Багажник;Балка;Бампер"</div>, а потом, на стороне клиента (в браузере) с помощью Javascript генерируя из этого сжатого списка элемент SELECT:

<script id="script1" type="text/javascript">
var script1=document.getElementById("script1")
var list=document.getElementById("list")
var larr=list.innerHTML.split(";")
var s=document.createElement("SELECT")
var lcode=""
for (var o in larr) {
 lcode+="<option>"+larr[o]+"</option>"
}
script1.parentNode.appendChild(s);
s.innerHTML=lcode
s.onchange="javascript:"
</script>

(Результат работы скрипта:

)

Удалить словарь с главной страницы можно, только поместив его на какую-то другую страницу. А связать обе страницы между собой можно с помощью элементов FRAME (или IFRAME). Мы поступили самым радикальным, даже, можно сказать, наглым образом – поместили словари Наименований и Моделей в правый и левый фреймы (а не в элементы IFRAME). А файлы словарей, которые грузятся в соответствующие фреймы, – обычные html-файлы (а не скрипты php), поэтому заботиться об отправке правильных HTTP-заголовков с инструкциями кэширования нам не надо (это делает сервер Apache без участия PHP). Конечно, словари могут изменяться при добавлении новой запчасти, поэтому каждый раз после успешного добавления html-файлы словарей генерируются php-скриптом addzap.php заново. Ну, и (мало ли что) у пользователя есть возможность в любой момент самостоятельно сгенерировать новые словари – через ссылку Обновить списки на главной странице.

После всех этих успешных манипуляций и установления в их результате правильного кэширования большей части информации, можно подумать и о вариантах её представления пользователю. В самом деле – ну почему именно выпадающий список? Почему не просто – список? С одной стороны, это вопрос структуры страницы, общего дизайна: выпадающий список, например, помогает сэкономить место. С другой стороны, надо представлять, как именно люди пользуются этим выпадающим списком. В идеале, нужно ткнуть в него мышкой, чтобы активировать, а затем набирать буквы искомого слова. Набирать надо достаточно быстро (не делая больших пауз), тогда список будет прокручиваться до нужного слова. Это идеальный случай (если брать его за основу, лучше использовать динамические списки, описанные нами в статье Пятый элемент). На практике же чаще всего пользователь крутит список мышкой и ищет нужное слово глазами, а это удобнее делать как раз с простым списком, а не с выпадающим.

Далее, простой список не теряет преимущества «прокручивания при наборе букв», если пользоваться правильным браузером Firefox – в нём существует такая опция в меню НастройкиДополнительноОбщие: «Искать текст на странице по мере набора». Впрочем, почему бы и не добавить в начало словаря окно для фильтрации видимой части списка: пользователь набирает текст в этом окне (элемент INPUT), и в списке по мере набора остаются только слова с набранным пользователем буквами.

Мы и добавили. И выяснили, что хвалёный Firefox оказался в этом случае не таким уж правильным: при анализе (с помощью Javascript) списка из 1300 строк FirefoxSeamonkey – видимо, вообще Gecko) работает на порядок медленнее, чем Опера и даже ИЕ. То есть своим внутренним механизмом он ищет слова на странице (по мере набора букв) очень быстро, а пользовательский скрипт, по сути, «вешает» (на короткое время) компьютер. Gecko (и Seamonkey в особенности) вообще являются довольно опасными пожирателями памяти при работе под Windows XP. Нет худа без добра – это заставило нас максимально оптимизировать Javascript-код «фильтрации списков по буквам».

Динамический список у клиента («Отбор по буквам»)

Задача, на первый взгляд, достаточно очевидная: при вводе букв в поле (озаглавленное в наших списках «Отбор по буквам») назначить функцию анализа текста событию keyup – «отпускание клавиши». Функция у нас называется pickup(obj). Точнее, там две разных функции для двух разных списков (полный текст доступен в файле http://irkutsk.ir2.ru/avto/edit.js). В начало функции добавляем несколько ставших для наших программ стандартными ограничений:

1) начинать поиск следует после ввода пользователем не одной, а нескольких букв; при обращении к базе данных обычно это число у нас "3", здесь, при обработке на стороне клиента, – "2" (условие if (obj.value.length>1));

2) если функция уже (ещё!:-) работает, не надо запускать новый экземпляр (условие if (fworking) return) – кажется, эта хрень фактически не работает (иначе бы мы сталкивались с ситуацией, когда при набранных, например, четырёх буквах поиск вёлся бы только по трём или двум), но что-то в этом роде делать надо;

3) событие keyup происходит, например, и просто при перемещении курсора стрелками – а в этом случае начинать работу не надо; для проверки введённого значения заводим глобальную переменную filterval, при работе функции приравниваем её значению поля поиска и потом при попытке очередного вызова функции проверяем: if (filterval==obj.value) return.

Далее, нужно получить список строк для поиска. Например, так: blist=getElementsByTagName("B") – мы знаем, что в наших списках все значимые элементы заключены в теги <b></b> (мы ведь сами генерируем эти списки скриптом listname.inc.php). Потом каждый элемент полученного массива blist сличаем с введённым пользователем значением: if (0==val.indexOf(q)), если поиск по началу слова, или if (val.indexOf(qr)>-1), если поиск букв в любой части слова. Мы учитываем обе возможности: если пользователь просто набирает буквы, поиск ведётся от начала слов (по умолчанию), если пользователь добавит в начало условный символ (пробел), учитывается любое вхождение набранных букв в искомой строке.

Как сделать видимыми найденные слова, а все остальные невидимыми? Казалось бы, чего проще: если условие 0==val.indexOf(q) выполняется, назначаем у текущего анализируемого элемента blist[s] свойство style.display="block"; если условие не выполняется – blist[s].style.display="none". Но мы ведь заранее не знаем, найдутся слова с заданными буквами или нет, и может получиться так, что все элементы списка станут невидимыми. В этом случае (флаг успешного поиска пуст – fflag=false) надо вернуть всё, как было: опять запустить цикл перебора всех элементов blist и назначить им всем свойство style.display="block". Вот тут-то и вылезают все «рога» хвалёного FF: он может трудиться над нашей функцией по 5-7 секунд (пожалуй, это можно считать багом)! При том, что даже ИЕ, сволочь, делает всё практически мгновенно (не говоря уже об Опере).

Поэтому мы не анализируем текущий набор элементов, а при загрузке страницы создаём копию этого набора (ex2=ex1.cloneNode(true); document.body.appendChild(ex2)) и анализируем элементы из копии; в случае успешного поиска основной набор делается невидимым через назначение style.display="none" родительскому элементу списка – <div id="ex1"> (именно в этот элемент вложены все элементы B с текстами наименований или моделей); в случае провала операции родительскому элементу основного списка назначаем style.display="block" (а элементы копии списка и так все невидимы, раз ничего не найдено – ведь прямо в основном цикле поиска каждому из них назначается style.display="none").

Ну, ещё всякие мелочи типа красного цвета букв, если поиск не имел успеха, – чтоб пользователь сразу видел, что это не просто большой список результатов поиска, а первоначальный список – «безрезультатный».

Для списка моделей нужна отдельная функция (pickupm(obj), потому что он имеет более сложную структуру – в нём значимые элементы с наименованиями моделей машин вложены не в основной элемент div (id="ex1"), а в элементы div каждой марки (Audi, Daihatsu, Ford...), а мы создаём список элементов для поиска всё-таки не через getElementsByTagName("B"), а через получение списка «детей» клонированного div id="ex2" (blist=ex2.childNodes). Правильнее, наверное, сделать одну функцию для двух списков с разными параметрами и дополнительными условными конструкциями. Но мы сделали как проще, а не как правильнее. Как всегда. Это у нас, можно сказать, такой принцип: программа должна быть простой! В смысле, не должна быть запутанной. Наименее запутанным (более унифицированным) было бы получать список всегда одинаково – через getElementsByTagName, а потом проверять «родителя» каждого элемента B, но тогда длина списка поиска удвоится (одинаковые B есть и в ex1, и в ex2), и браузеры gecko опять затрещат по швам (вернее, заставят трещать вентилятор процессора).

После набора букв (например, "дв") список Наименований уменьшится примерно до трёх слов: "Дверь", "Дверь пятая", "Двигатель". Что дальше? Отчасти пользователю даётся подсказка при возюкании мышкой по окну браузера (обычно пользователи любят елозить мышкой): если курсор мышки случайно наедет на какое-то слово из списка, он превратится в "default" (стрелка) и слово станет подчёркнутым, как ссылка – типа надо по нему щёлкнуть.

Лучше, наверное, было бы делать курсор "указатель", как на ссылках, но как будет "указатель" по-английски? ИЕ, как самый умный, считает, что "pointer"; gecko, Opera и весь W3 консорциум переводят более конкретно – "hand". На этот раз мы для разнообразия поленились распознавать useragent'ов, и оставили общий для всех "default". В общем, указатель мыши "стрелка" и подчёркивание должны надоумить пользователя щёлкнуть по слову.

В мире интересного

Списки Наименований запчастей и Моделей машин – периферия нашей программы (даже зрительно – узкие правый и левый фреймы). Как они взаимодействуют с центральной областью?

Если сохранить себе на диск один из наших словарей (например, http://irkutsk.ir2.ru/avto/listname.htm), то можно увидеть, что внутри него написан очень простой код, в котором нет почти ничего, кроме списка наименований, каждое из которых заключено в тэги <b>Дверь</b>. Но чтобы при наведении мыши на слово появлялось подчёркивание, а при щелчке по слову начинались какие-то действия, нужен более сложный код, нечто вроде: <b onmouseover="underline(this)" onclick="insert(this)">Дверь</b>. Идею, как обойтись без этого и сэкономить килобайты текста, мы подсмотрели на http://www.mozilla.org/editor/midasdemo/colors.html – там после загрузки таблицы с обычными ячейками запускается javascript, который обходит список элементов, полученный через getElementsByTagName("TD"), и задаёт каждому элементу нужные свойства.

Но мы решили переплюнуть Мозиллу (тем более что она так плохо обрабатывает javascript'ом большие массивы) и пойти дальше – не обходить список элементов в цикле (это медленно, если элементов много), а назначить событиям click и mouseover функции сразу для всего документа. А потом проверять, куда щёлкнул пользователь, – и если в подходящее место, запускать сценарий на исполнение. Сложность этого способа в том, что в нормальных браузерах (работающих по спецификациям W3C) и в ИЕ задача исполняется по-разному: if (1==agent) document.onclick=insname (если браузер порядочный, для объекта "документ" событию click назначается функция insname), else document.body.onclick=insnameie (если браузер ИЕ, для объекта body событию click назначается функция insnameie).

В самих функциях проверяется, является ли объект, по которому щёлкнули, элементом (nodeType==1?), подходящий ли это элемент (tagName=="B"?); затем извлекается текст элемента (firstChild.nodeValue) и вставляется в форму поиска центрального фрейма. Заслуживает внимания то, как именно мы находим объект – форму в соседнем фрейме: запускается функция initlistvar(), обязательно с try – catch (потому что мы никогда не можем быть уверены, что соседний фрейм уже загружен в браузер – есть такая проблема на странице с фреймами) и пишем: addform=parent.main.document.getElementById("addform").

Ядро (поиск по базе)

Как мы уже говорили, таблица автозапчастей по самой своей сути не может быть простой (много разных параметров), поэтому не совсем ясно, какой в ней должен быть поиск «по умолчанию». Недолго думая, мы переложили эту проблему на пользователя самым незамысловатым способом: поиск ведётся по образцу фильтра в Excel'е; то есть пользователь сам выбирает, по какому полю вести поиск. Вверху, под заголовком каждого поля (столбца) мы поместили элемент INPUT; если ввести в него буквы, а потом нажать кнопку Найти (рядом с заголовком таблицы) или просто Enter, HTML-форма отправит на сервер GET-запрос с заданными пользователем параметрами.

Правый и левый фреймы со словарями Наименований и Моделей, а также кнопки L, R, F, B, U, D мы добавили всего лишь для того, чтобы пользователю меньше нужно было печатать текста, а больше указаний давать программе поиска обычными тычками мыши (пользователи обычно очень не любят набирать текст, особенно латиницей).

Мы не знаем заранее, сколько полей для поиска заполнит пользователь, поэтому sql-запрос формируется в цикле foreach($GET as $key=>$val). Это обязывает нас назвать все элементы поисковой формы в соответствии с полями таблицы запчастей. Чтобы если пользователь, например, пришлёт запрос main.php?name=airbag, при анализе массива GET оказалось, что name – это $key, а airbag – $val, и что в таблице значение airbag хранится именно в поле по имени name. А у нас много полей, и в принципе их количество может меняться. Поэтому «рисовать» заголовок таблицы тоже нужно в цикле, извлекая имена полей из sql-запроса:

$sql="select SQL_CALC_FOUND_ROWS * from `{$maintab}` {$where} order by `datupd` desc {$limit}";
$r = mysql_query($sql);
...
$f=mysql_num_fields ($r)-3; //3 последних поля пользователю не нужны
...
for ($i=1; $i<$f; $i++) {
 $name=mysql_field_name($r, $i);
 ...
 echo "<th>{$fieldsru["$name"]}<br>{$input}";

Отдельного пояснения требует последняя строчка, в которой с помощью выражения $fieldsru["$name"] выводятся русские наименования столбцов таблицы. Массив формируется в файле config.php (который пока не настолько хорош, чтобы предлагать его целиком вниманию читателей, особенно программистов) следующим кодом:

$fieldsru=array();
$r=mysql_query("select * from `fieldsru`");
$n=mysql_num_rows($r);
if ($n) {
 while ($line = mysql_fetch_array($r, MYSQL_ASSOC)) {
  $f=$line["name"];
  $fr=$line["ru"];
  if (trim($fr)) $fieldsru["$f"]=$fr;
 }
}

Из него видно, что у нас в базе данных есть таблица fieldsru, в которой записаны русские соответствия для латинских наименований полей (русская часть таблицы заполняется вручную, администратором).

А код основного скрипта автозапчастей, кажется, не так плох (и в нём всего 170 строк!), поэтому даём к нему общий доступ всем любопытным: main.php.

© 2009, «Деловая неделя», Михаил Гутентог

Читать все комментарии (0)

Добавить комментарий:

*Автор:
E-Mail:
*Текст: