Архитектура Поиска Яндекса. Лекция для Малого ШАДа

В этой лекции на примере Яндекса будут рассмотрены базовые компоненты, необходимые для организации интернет-поисковика. Мы поговорим о том, как эти компоненты взаимодействуют и какими особенностями обладают. Вы узнаете также, что такое ранжирование документов и как измеряется качество поиска.

Лекция рассчитана на старшеклассников – студентов Малого ШАДа, но и взрослые могут узнать из нее много нового об устройстве поисковых машин.

Первый компонент нашей поисковой машины – это Паук. Он ходит по интернету и пытается выкачать как можно больше информации. Робот обрабатывает документы таким образом, чтобы по ним было проще искать. По простым html-файлам искать не очень удобно. Они очень большие, там много лишнего. Робот отсекает все лишнее и делает так, чтобы по документам было удобно искать. Ну и непосредственно поиск, который получает запросы и выдает ответы.

image

Паук

Как понять, насколько хорошо работает Паук? Первая метрика – какой процент сайтов мы увидели. Вторая метрика – насколько быстро мы умеем замечать изменения.
Рунет:

  • Качающие сервера: 300;
  • Нагрузка: 20 000 документов в секунду;
  • Трафик: 400 МБайт/с (3200 Мбит/с).

Все вместе:

  • Качающие сервера: 700;
  • Нагрузка: 35 000 документов в секунду;
  • Трафик: 700 МБайт/с (5600 Мбит/с).

Если весь Паук всеми своими серверами начнет скачивать один сайт, получится достаточно мощная DDoS-атака. Для предотвращения таких ситуаций предусмотрен компонент Zora. Он координирует закачки, знает, какие сайты были закэшированы недавно, а на какие стоит сходить в ближайшее время.

image

Паук должен иметь возможность посмотреть на страницу из разных точек, чтобы убедиться, что, например, из России и из США страницы выглядят одинаково. Если же для разных регионов выдаются разные страницы, он должен учесть и обработать эту информацию.

Робот

Скачанные Пауком страницы переходят к роботу, который обрабатывает их и делает пригодными для поиска. В первую очередь удаляется вся html-разметка, определяются важные участки.
Состоит Робот их трех компонентов. Во-первых, это система распределенных вычислений MapReduce, которая позволяет посчитать свойства для каждого выкачанного Пауком документа. Второй компонент – это кластер серверов, занятых сборкой поискового индекса. Ну и третий компонент – это архив, содержащий несколько версий поисковой базы.

image

Мы упомянули свойства документа. Что мы подразумеваем под этим понятием? Допустим, мы скачали html-файл, и нам нужно собрать как можно больше данных о его содержании. MapReduce проводит вычисления и навешивает на документ ярлыки, которые позже будут использоваться в качестве факторов ранжирования: язык документа, тематика, коммерческая направленность и т.п.
Факторы бывают двух типов: быстрые и медленные. Медленные факторы считаются однократно и присваиваются исключительно документу. Быстрые вычисляются для документа вместе с поисковым запросом.
Даже если не брать в расчет сервера MapReduce (они могут использоваться и для других задач), Робот представляет собой более двух тысяч серверов.
Российская база:

  • Кластер вычисления факторов: 650;
  • Варка поисковой базы: 169;
  • Тестовые сервера: 878;
  • Архив: 172.

Мировая база:

  • Кластер вычисления факторов: 301;
  • Варка поисковой базы: 120;
  • Тестовые сервера: ???;
  • Архив: 60.

В базе хранится около 2,5 миллиарда документов (214 ТБ), два раза в неделю она полностью пересчитывается.

Устройство поискового индекса
Допустим, у нас есть три очень простых документа, содержащих которыкие тексты:

  • Мама мыла раму
  • Рамы в Москве купить
  • Москва для мам

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

Мама (1, 3)
Мыть (1)
Рама (1, 2)
Москва (2, 3)
Купить (2)
В (2)
Для (3)

Теперь если нам придет поисковый запрос [мама], у нас уже будет готовый ответ. Нам будет достаточно один раз заглянуть в таблицу, чтобы узнать, в каких документах встречается это слово. Если в запросе больше одного слова (например, [мама Москва]), в той же таблице Робот сможет найти документы, в которых встречаются оба этих слова. В нашем случае это третий документ.
Как мы уже говорили, размер нашего реального поискового индекса – 214 ТБ. Чтобы Поиск работал с достаточной скоростью и эффективностью, все эти данные должны храниться в оперативной памяти. Сейчас у нас стоят сервера, в которых установлено от 24 до 128 ГБ памяти. Для оптимизации мы делим поисковую базу на тиры (от англ. Tier – уровень). С их помощью мы разделяем документы по языкам и другим признакам. Поэтому когда нам приходит запрос на русском языке, мы имеем возможность проводить поиск только по соответствующим документам. Всего таких тиров у нас более десятка. Тиры делятся на шарды по 32 гигабайта: объем данных, который можно разместить в памяти физической машины. Сейчас у нас около 6700 шардов.

Работа с запросом

Поток поисковых запросов к Яндексу может достигать 36 000 обращений в секунду. Только получить такой трафик – серьезная задача. Для ее решения предусмотрено несколько уровней балансировки. В качестве первого уровня применяется DNS. За распределение пакетов отвечает L3 balancer, а за обработку их содержимого – http balancer. Первым делом отсекаются запросы от роботов. Затем исправляются опечатки и проводится анализ запроса. В результате получается «дерево запроса», содержащее возможные написания запроса и вероятные смыслы. После всей этой обработки запрос передается на фронтэнд, и начинается непосредственно поиск. Помимо основного поиска во всем документам, содержащимся в базе, происходит еще множество маленьких поисков с определенными параметрами. Например, по картинкам, видео, афише и т.д. Если эти поиски дадут релевантные результаты, они будут подмешаны в основную поисковую выдачу.

image

ссылка на оригинал статьи http://habrahabr.ru/company/yandex/blog/204282/

Дайджест интересных материалов из мира веб-разработки и IT за последнюю неделю № 85 (24 — 30 ноября 2013)


Веб-разработка

CSS

JavaScripts

Браузеры

Веб-инструменты

  • en DevDocs — ресурс с документацией по различным API и библиотекам
  • en swipe — система для создания слайдов в вебе от Markdown
  • en iconmelon — веб-библиотека с svg-иконками с возможностью добавления эффектов
  • en Reportr — панель со статистикой, собираемой по событиям из разных источников
  • en DOM Monster — букмарклет для анализа DOM и других штук на странице
  • en Google Fonts загрузили более 1 триллиона раз
  • en Monocle — open source новостной агрегатор

Новости

Сайты с интересным дизайном и функциональностью

Дизайн

Подборка бесплатных дизайнерских печенек

Занимательное

Просим прощения за возможные опечатки или неработающие/дублирующиеся ссылки. Если вы заметили проблему — напишите пожалуйста в личку, мы стараемся оперативно их исправлять.

Дайджест за прошлую неделю.
Материал подготовили dersmoll и alekskorovin.

ссылка на оригинал статьи http://habrahabr.ru/company/zfort/blog/204300/

DIY-диммер: путеводитель по компонентам

В недавнем прошлом мы поделились с нашими дорогими читателями полным комплектом gerber-файлов. Чтобы все желающие имели возможность заказать себе печатные платы. Так же недорого, как это сделали мы.

Сегодня мы делаем следующий шаг. Мы публикуем полную спецификацию электронных компонентов, используемых в нашем DIY-диммере. Мы постарались, чтобы эта спецификация стала понятна даже самым начинающим электронщикам. Под катом большая подробная таблица компонентов с фотографиями (в перспективе) всех используемых элементов.

Краткий ввод в курс для тех, кто первый раз увидел наш проект

Мы разрабатываем полноценную систему Умного дома. «Первая ласточка» нашей системы — DIY-диммер. Вот его основные характеристики:

  • Работа по радиоканалу 2,4Ггц (свой протокол, без лицензионных ограничений)
  • Защищенное шифрованием соединение
  • Установка без изменения стандартной электропроводки обычной квартиры
  • Низкий расход электроэнергии
  • Привычный внешний вид выключателей
  • Возможность самостоятельного расширения как аппаратного, так и программного функционала
  • Открытый исходный код как программной, так и аппаратной части

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

Хотите быть в курсе всех событий проекта? Это совсем не сложно!

Нужно всего лишь подписаться на обновление нашей компании на Хабре и в группе ВКонтакте.

С ВКонтакте вопросов обычно не возникает. Чтобы подписаться на обновления Хабра, нужно перейти на страницу компании и нажать кнопку «Подписаться» в блоке справа.

Принципиальная схема устройства

Спецификация используемых компонентов

Мы будем добавлять фотографии по мере появления у нас заказанных компонентов. Спецификации некоторых элементов немного отличаются от схемы. Возможные диапазоны пассивных компонентов будут добавлены в таблицу позже. Обновления будут анонсироваться в комментариях.

Обозначение Наименование Варианты замены Корпус Фото
U1 PBS1.27 10pin x2 Выводной
10-пиновый разъем для радиомодуля с шагом выводов 1.27мм, 2 шт.
U2 LNK304D LNK305D, LNK306D SO-8
Преобразователь питания
U3 MOC3052SM SDIP6
Оптосиммистор
VO1 6N139S SDIP8
Оптопара
VD1, VD2 TB10S TDI
Диодный мост
T1 T835-600G T1635-600G D2PAK
Симмистор
D1 LED 0805
Светодиод вашего любимого цвета
D2 STTH1R06A ES1J SMA
Диод быстрый
D3 GS1J S1J SMA
Диод выпрямительный
C1 3,3uF x 450V 2,2uF x 450V Выводной
Конденсатор электролитический
C2, C4, C5, C6 100n (0.1uF) 0603
Конденсатор керамический SMD
C3 10n (0.01uF) 0603
Конденсатор керамический SMD
C7 10uF x 10V 10uF x 16V Выводной
Конденсатор электролитический
C8 470uF x 10V 220uF x 10V LOWESR / 220 x 16 Выводной
Конденсатор электролитический
R1, R2 470k 0,25W Выводной
Резистор выводной
R3, R4 47R 1W Выводной
Резистор выводной взрывозащитный (flameproof)
R5, R6 10k 5% 0603
Резистор SMD
R7 200R 5% 0603
Резистор SMD
R8, R10 1,6k 1% 0603
Резистор SMD
R9 2k 1% 0603
Резистор SMD
R11 390R 1W Выводной
Резистор выводной
R12 1k 5% 0603
Резистор SMD
L1 3,3uH 1210
Дроссель SMD
L2 1000uH Выводной
Дроссель выводной силовой
F1 250V 2A Выводной
Предохранитель
LINE Разъем 2 пин Выводной
Силовой разъем

Обратите внимание, в продуктовой плате мы постарались минимизировать размеры корпусов элементов и по-максимуму использовать поверхностный монтаж. Все используемые микросхемы — в SMD-корпусах. Выглядят они вот так:

Внимательный читатель заметил на заглавном фото поста изображение нового радиомодуля. Именно такой модуль мы применяем в итоговой компоновке DIY-диммера. Вопросам приобретения этого модуля и совместимых с ним готовых китайских комплектующих будет посвящена отдельная статья.

Кто может собрать сам?

Любой отважный первопроходец в держании паяльника. Паяльник должен быть с очень тонким жалом. Лучше использовать паяльную станцию. Настройки схема не требует.

Где купить компоненты?

Максимальную экономию можно получить, заказывая компоненты из Китая через одного из посредников национального аукциона TaoBao. Такая покупка имеет свои особенности и некоторые оверхеды как по длительным срокам, так и по возможности получения в итоге не того, что заказывал. Плюс следует учитывать, что заказывать из Китая деталей на 1-10 устройств скорее всего будет совсем не выгодно.

Если не гнаться за максимальной экономией и желать какого-то уровня комфорта, все необходимые детали в обозримом будущем можно будет приобрести у нас. В виде наборов различных комплектаций. Возможным комплектациям посвящен опрос в конце этого поста. Не проходите мимо, проголосуйте, нам важно ваше мнение!

Ориентировочная стоимость набора необходимых деталей (только детали, без платы и корпуса) при штучной продаже составит примерно 800 рублей. Ценник будет значительно уменьшаться в зависимости от количества приобретаемых наборов.

Анонс следующих постов:

  • Первые фото первого корпуса нашего DIY-диммера + 3D-модель для самостоятельной печати корпуса на 3D-принтере.
  • Модуль подключения к компьютеру по USB: описание, схематика, спецификация компонентов и традиционный комплект gerber-файлов.
  • Где приобрести подходящие радиомодули и что можно еще купить в Китае для использования в самодельном умном доме?
  • Первые шаги по программированию NRF24LE1: мигаем светодиодом
  • Прошивка NRF24LE1 с помощью подручных средств: Raspberry PI & TI MSP430 LaunchPad

Подписывайтесь на наши обновления здесь и во ВКонтакте, участвуйте в обсуждениях, влияйте на ход проекта.

Какую комплектацию DIY-диммера вам было бы интересно приобрести?

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Никто ещё не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи http://habrahabr.ru/company/coolrf/blog/202968/

Магия data-driven design

Примечание от переводчика

Из-за отсутствия лаконичного перевода некоторых терминов пришедших к нам из английского языка я предпочту оставить их на языке оригинала. Думаю что у интересующихся данной отраслью это не вызовет раздражения.

Игры состоят из двух частей: логики и данных. Бесполезных по-отдельности, но оживляющих игру при соединении их вместе. Логика определяет основные правила и алгоритмы игрового движка, в то время как данные описывают подробные сведения об игровом содержании и его поведение. Магия в том, что обе эти составляющие отделены друг от друга и могут развиваться отдельно.

Мысль 1: Основы

Прежде всего необходима система, которая может считывать текстовые файлы по запросу (а не только при начальной загрузке программы). Она необходима для следования парадигме data-driven, положенной в основу всего процесса. В каждой игре требуется способ чтения данных из файлов, и несмотря на то, что в конечном итоге игра должна работать с бинарными файлами, во время процесса разработки гораздо удобнее работать с текстовыми. Ведь они просты до невозможного! А также позволяют организовать работу для всей вашей команды эффективнее — не меняя ни одной строки кода, любой член команды (включая тестировщиков и гейм-дизайнеров) может пробовать новое и экспериментировать с различными вариантами существующего. Таким образом, легко реализуемая функция быстро становится незаменимым инструментом в работе.

Мысль 2: Необходимый Минимум

Не надо жестко задавать константы в коде. Сложите их все в текстовый файл (или файлы), чтобы их можно было легко изменить без повторной компиляции кода. Например, основной функционал, поведение камеры и т.п. можно полностью описать таким образом. И если будет так, то дизайнер игр, продюсер, мальчишка с соседнего подъезда — все будут в состоянии изменить поведение игры, просто отредактировав нужный файл в блокноте. Да и не все члены команды могут быть близки к программированию и хорошо с ним ладить, поэтому важно помнить об этом, ведь, играясь со значениями постоянных, продюсеры и дизайнеры игр могут добиться нужного им поведения в игре, при этом не тревожа программистов.

Мысль 3: Ничего Не Ограничивайте

Предположим, что измениться может что угодно (скорее всего так и будет). Если игра требует режим разделенного экрана, не надо жестко указывать ровно два. Напишите свою игру, которая может поддерживать любое количество экранов со своей логикой для каждой камеры. Количество работы от этого, при правильном подходе к делу, не сильно увеличится. С помощью магии текстовых файлов вы определите сколько экранов должно быть в игре — один, два, четыре или больше. Файлы также зададут начальные данные для камер: положение, направление, угол охвата и наклон. Самое приятное в том, что ваши гейм-дизайнеры тоже обладают прямым доступом к изменению этих параметров.
Игра развивается в полную силу, при гибком проектировании кода. Процесс абстрагирования и вынесения ядра игры сильно поможет в ее проектировании и разработке. Вместо проектирования игры под одну конкретную цель, вы можете проектировать каждый её компонент и его общую функциональность. Эффект от такого подхода будет в том, что вы лучше поймете что вам действительно нужно делать, а не будете слепо следовать описанному поведению в проектной документации к игре.
Например, если игра требует только четыре видов оружия, вы можете запрограммировать совершенную систему, которая описывает все их по отдельности. Однако, если вы абстрагировались от функциональности каждого из видов оружия, используя данные, определяющие поведение, вы в дальнейшем сможете добавить бесчисленное количество видов оружия, чтобы экспериментировать с новыми идеями и динамикой игрового процесса. Такое мышление, в конечном итоге, позволяет игре улучшаться и развиваться.
Вы поверили мне, когда я сказал «Ничего»?
Правда в том, что игры должны быть настраиваемыми, а большие игры должны развиваться от оригинальной версии, следуя сюжету. Ваша игра должна уметь работать с меняющимися правилами, персонажами, расами, оружием, уровнями, схемами управления и объектами. Без этой гибкости любое изменение в игре становится дорогостоящим, и даже незначительное изменение предполагает участие программиста — что по сути просто трата ресурсов. Если же изменения окажутся сложными, то это может привести в итоге к незначительному улучшению оригинального проекта игры, и игра так и не не доживет до раскрытия всех своих возможностей.

Мысль 4: Основной Управляющий Сценарий

Сценарий — это просто способ задания поведения за пределами программного кода. Сценарии прекрасно подходят для определения последовательности шагов, которые должны произойти в игре, или для игровых событий, которые должны быть вызваны. Например, сценарий для игровой сцены описывающий простую причинно-следственную логику, вроде «что будет при условии завершения квеста» или «какой триггер сработает для данной окружающей среды». Все эти примеры относятся к парадигме data-driven design.
При проектировании сценариев нужно определиться с логикой ветвлений и тем, как она будет организована. Выделают два подхода. Первый — продолжать использовать переменные внутри сценария и сравнивать их операторами сравнения: равенство (=), меньше чем (<) и т.д. Второй подход заключается в непосредственных вызовах оценивающей функции, которая сравнивает переменные, существующие внутри кода, например isLifeBelowPercentage(50). Вы можете использовать и комбинацию этих методов, но при этом постарайтесь сохранить сценарии простыми. Гейм-дизайнеру будет гораздо проще работать с оценивающими функциями, чем с объявленными переменными, их обновлением и сравнением. Также второй подход упростит отладку.
К сожалению, для описания сценариев требуется соответствующий язык. Это означает, что вы должны создать целый синтаксис для определения поведения игры. Язык сценариев включает в себя создание парсера сценариев и, возможно, компилятора для преобразования сценариев в двоичный файл для более быстрого выполнения в дальнейшем. Другой вариант заключается в использовании существующего языка, например Java, но в таком случае может потребоваться большое количество дополнительных компонентов. Чтобы не потратить на язык сценариев много времени, нужно выиграть проектируя системы проще. В целом, наблюдается тенденция придания излишней мощности языку сценариев. Следующая мысль объясняет некоторые подводные камни сложного языка сценариев.

Мысль 5: Когда Хорошие Сценарии Становятся Плохими

Использование сценариев для описания поведения в data-driven design — естественное следствие данной методологии. Однако, не нужно забывать про здравый смысл и помнить ключевую идею: отделение логики от данных. Сложная логика отправляется в программный код, а данные остаются снаружи.
Проблема возникает, когда желание основываться на данные заходит слишком далеко. В какой-то момент, вам захочется описать сложную логику внутри сценария. Когда сценарий начинает содержать состояние чего-либо и требует ветвлений, он становится подобен конечному автомату. С увеличением его состояний, невинному писателю сценариев (а также некоторым бедным гейм-дизайнерам) придется работать программистом. Если сценарии становятся слишком сложными, работа возвращается к программисту, который должен описать все сложности на своем строго ограниченном языке. Сценарии должны делать работу людей проще, а не сложнее.
Почему важно хранить сложную логику в коде? Вопрос функциональности и отладки. Поскольку сценарии находятся вне кода, в них дублируются многие понятия, которые существуют в языках программирования. Естественная тенденция давать все больше и больше функциональности сценариям, пока они не станут настоящими языками программирования. Появляются более сложные сценарии, и требуется отслеживать больше информации при отладке, что усложняет изначально простую идею работы со сценариями.
Как вы, наверное, догадались, в сценариях может быть нетривиальная логика, и тогда могут уйти месяцы работы на написание парсера скриптов, компилятора и отладчика. Такое происходило бы, если программисты не понимали, что перед ними уже достаточно хороший компилятор.
Размытая граница
Нет никаких сомнений, что граница между кодом и сценариями размыта. В общем, плохая идея помещать поведение искусственного интеллекта (AI) в сценарии, в то время как хорошей идеей будет размещение в сценариях системных триггеров для придания миру интерактивности. Получается следующее правило: если логика сложная, то она должна быть в коде. Языки сценариев должны оставаться простыми, чтобы не поглотить собой вашу игру (и все ваши программистские ресурсы).
Однако, некоторые игры проектировались с возможностью написания игроками собственных AI. Зачастую, это стрелялки от первого лица, позволяющие добавлять ботов. Когда цель именно такая, подобие языка сценариев настоящим языкам программирования просто неизбежно. В качестве примера можно рассмотреть Quake C. Поскольку создание ботов заложено в саму игру, ресурсы и энергия были потрачены на создание языка сценариев на столько удобного, как и язык C. Язык сценариев такого масштаба довольно трудоемкий и не стоит относиться к нему легкомысленно.
Прежде всего, помните, что вы не хотите, чтобы гейм-дизайнеры и сценаристы занимались программированием игры. Иногда программисты пытаются уйти от ответственности, создавая языки сценариев, чтобы заманить гейм-дизайнеров в программирование игры. В идеальном случае, программисты должны решать серьезные задачи и создавать значительную часть управляющей логики. Иначе за что еще им платят такие большие деньги?!

Мысль 6: Как Избежать Синдром Повторения Данных

Распространенная программистская практика — никогда не дублировать код. Если вам нужно некоторое поведение (например, общая функциональность) в двух разных местах, оно должно существовать только в одном месте. Идея применима и к описанию данных с помощью ссылок на глобальные блоки данных. Кроме того, используя ссылки на наборы данных и изменяя некоторые из их значений, вы, в конечном счёте, приближаетесь к концепции наследования.
Наследование — это громадная идея, которую нужно применить к вашим данным. Представьте, что ваша игра населена гоблинами, живущими в подземельях. Ваши данные определяют где стоит каждый гоблин со всеми его свойствами в каждом подземелье. Правильно будет инкапсулировать данные глобального определения в каждый экземпляр гоблина. Для того, чтобы каждый гоблин стал уникальным, ссылка может сопровождаться списком переопределяемых свойств. Такой подход позволит каждому гоблину обладать собственным поведением, при этом не дублируя данные.
Идея может применяться на нескольких уровнях, позволяя каждому набору данных иметь ссылку. Используя эту технику, вы можете сделать глобальное определение гоблина, которое наследуется от базового типа гоблина. Внутри определения для каждого подземелья могут быть указаны обычные или быстрые гоблины. На рисунке продемонстрирована эта концепция наследования с помощью ссылок и переопределения значений.

Мысль 7: Сделайте Инструмент Для Работы с Данными

В любой большой игре текстовые файлы в конечном итоге становятся неуправляемыми и с ними становится тяжело работать. Решение этой проблемы — создание инструмента для работы с ними. Назовем этот инструмент редактором игры, редактором уровней, или редактором сценариев, он позволит ускорить разработку игры. Обладание инструментом не меняет методологию data-driven, а лишь делает ее более надежной и эффективной. Время, сохраненное с помощью таких инструментов, оправдывает затраты на их разработку и внедрение.

Заключение

Начать пользоваться методологией data-driven design легко, но довольно трудно вообразить удивительные возможности, появляющиеся при таком подходе.
Игра Total Annihilation — хороший пример такого подхода. Дизайнер Крис Тейлор заложил две расы: Arm и Core. Хоть вся игра и сосредоточена на двух расах, они не были жестко запрограммированы в игру. Теоретически, можно было бы добавить данные для добавления третьей расы, даже после выпуска игры. Несмотря на то, что эту возможность не использовали, Total Annihilation остаётся полностью настраиваемая в этом плане. Так как все юниты определяются данными, новые юниты публиковались еженедельно на сайте игры. На самом деле, многие люди создавали собственных юнитов с функциональностью, которая потрясла самих разработчиков игры.
Data-driven design помог Total Annihilation поддерживать верных фанатов игры в и без того переполненном жанре. Идея, реализованная в Total Annihilation, нашла свое дальнейшее применение и в других играх, например в The Sims, которые также распространяют дополнительный игровой контент через веб сайт. Без приверженности разработчиков к философии data-driven design, такие расширения были бы невозможны.

ссылка на оригинал статьи http://habrahabr.ru/post/204298/

Принципиально новая загрузка CommonJS модулей в браузер

Однажды, сидя за компьютером и обдумывая свою очередную никчемную затею, я внезапно понял, что мне нужен способ использовать один и тот же код на стороне браузера и на стороне сервера. Я почти сразу же догадался, что наверняка я не первый такой умный, и все давно придумано за меня — и не ошибся.

Действительно, под мои требования замечательно подходил, например, RequireJS с его адаптером для Node.js, которые какое-то время с успехом удовлетворяли мои прихоти, пока меня опять не осенила гениальная мысль: «Почему я вынужден использовать кашу из двух совершенно различных форматов модулей в одном проекте? Нужно все унифицировать!».

И опять ответ не заставил себя долго ждать, нашелся миллион браузерных реализаций CommonJS модулей: и всевозможные склейщики скриптов, и серверные препроцессоры, и синхронные загрузчики, и асинхронные — все, что душе угодно. Но все они оказались с одним очень важным недостатком. Они так или иначе изменяли исходный код скриптов и делали очень неудобным процесс их отладки в браузерных инспекторах.

Думаем

Согласно спецификации глобальное пространство имен каждого модуля должно быть глобально только в границах самого модуля. За его пределы распространяться оно не должно. Если такой модуль загрузить в браузер в неизменном виде, его код исполнится в глобальном пространстве имен и будет делить его вместе со всеми остальными модулями. Чтобы этого избежать, код каждого модуля обычно оборачивают в функцию, и пространство имен модуля таким образом становится локальным.

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

Мы же пойдем другим путем. Суть нашего метода состоит в том, что мы будем загружать каждый модуль в отдельный ифрейм, тем самым изолируя его от остальных модулей. В пространстве имен каждого такого ифрейма предварительно будут определены функция require и объекты exports и module.exports, как того требует спецификация CommonJS.

Данный способ загрузки скриптов, к сожалению, оказался не без недостатков. Первое, с чем я столкнулся, это неудобство работы с DOM родительского окна и прочими глобальными объектами. Для доступа к ним нужно использовать громоздкую конструкцию window.parent.window, которая, к тому же, будет излишней, если в дальнейшем мы захотим склеить наши модули для продакшена. Решением данной проблемы, в некотором роде, станет создние в каждом ифрейме объекта global, который будет являться ссылкой на window родительского окна. Посредством этого объекта мы сможем получать доступ из наших модулей к таким вещам, как непосредственно сам window, document, navigator, history и так далее, а также при необходимости использовать глобальные переменные.

Вторым, не столь очевидным на первый взгляд недостатком оказалась неидентичность глобальных функций-конструкторов (классов) Function, Date, String и т.д. в контекстах разных модулей. Это не позволит нам, например, проверить принадлежность объекта какому-либо встроенному классу, если он был создан в другом модуле.

var doSomething = require("./someModule").doSomething; // doSomething - это функция, определенная в модуле someModule console.log(doSomething instanceof Function); // false, потому что Function текущего модуля и Function модуля someModule (которая является конструктором функции someFunction) - это разные объекты 

Прозрачно решить эту проблему нельзя, поэтому придется принять соглашение не использовать в коде конструкции, подобные указанной выше. Либо использовать их аккуратно. Конкретно в этом примере проверку на принадлежность функции классу Function можно заменить, например, таким способом:

console.log(typeof doSomething === "function"); // true 

Еще одним нюансом, затрудняющим жизнь желающим загрузить свои CommonJS модули в браузер, является синхронная природа CommonJS’овской функции require. Наиболее распространенным способом решения этой проблемы является загрузка нужного модуля синхронным AJAX-запросом и последующий eval загруженного кода, либо создание анонимной функции с помощью new Function(). Нам этот способ не подходит, так как отладчик в этом случае перестанет указывать на строки кода в оригинальном файле. Мы опять пойдем другим путем, который позволит нам без проблем бегать дебаггером по нетронутому беспощадным евалом коду.

Функция require по сути всего лишь возвращает закешированный объект module.exports, который экспортируется загруженным модулем. Код самого модуля исполняется только один раз при первой попытке загрузить модуль.

Имея в виду вышесказанное, пойдем на небольшую хитрость — будем загружать наши модули заранее, до того, как начнет исполняться код, который эти самые модули будет использовать. При этом будем кешировать экспорты всех модулей куда-нибудь, откуда потом наша абсолютно синхронная функция require будет их возвращать.

Конечно же, данный способ тоже не лишен недостатков. Для того, чтобы заранее загрузить все модули, нам нужно совершенно точно знать их идентификаторы (имена). А это значит, что используя наш способ, мы не сможем предварительно загрузить те модули, идентификаторы которых вычисляются во время выполнения приложения. То есть мы не сможем сделать так:

var a = someRandomValue(); require("./module" + a); 

Тем не менее, чтобы решить эту проблему, можно воспользоваться для таких случаев обычной AJAX-загрузкой модуля и eval’ом со всеми вытекающими отсюда последствиями.

Еще есть проблемка, заключающаяся в том, что порядок исполнения кода в модулях будет отличаться от такового, например, в условиях Node.js. Рассмотрим два маленьких модуля:

// Модуль "a" exports.result = doSomeExternalOperation(); 

// Модуль "b" prepareDataForSomeExternalOperation(); var a = require("./a"); 

В Node.js, очевидно, вызов функции prepareDataForSomeExternalOperation произойдет раньше, чем вызов doSomeExternalOperation (но только в том случае, если до этого не было других вызовов require("./a")). В нашем же случае все будет наоборот, так как модуль a загрузится и выполнится раньше модуля b. С этим недостатком нам, к сожалению, тоже придется мириться. Но справедливости ради стоит сказать, что при правильном проектировании модулей таких ситуаций возникать не должно. Выполнять в основном коде модуля какие-то внешние действия (например в файловой системе или какой-нибудь базе данных), которые неявно могут повлиять на работу других модулей — нехорошо.

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

Но если вам вдруг нечем сейчас заняться, или просто любопытно — добро пожаловать в описание деталей моей реализации!

Кодим

Исходники лежат в свободном доступе на Github’е.

Я решил оформить данную утилиту в виде класса, каждый экземпляр которого будет представлять собой отдельное приложение со своей собственной конфигурацией (правда, пока вся конфигурация состоит из единственной строчки — пути, где лежат все наши скрипты), и кешем модулей:

function Comeon(path) { 	var self = this; 	self.path = path; 	self.modules = {}; } 

У класса есть единственная публичная асинхронная функция require, которая запускает выполнение какого-либо модуля со всеми его зависимостями, и опционально принимает функцию обратного вызова, которая вызовется после завершения работы главного модуля и получит параметром его экспорт.

Comeon.prototype.require = function require(moduleRequest, callback) { 	var self = this; 	loadNextModule.bind(self)(enqueueModule.bind(self)(getModuleId("", moduleRequest)), callback); } 

Прежде, чем рассмотреть две основные и самые интересные функции enqueueModule и loadNextModule, рассмотрим несколько вспомогательных.

Функция searchRequires принимает параметром URL файла модуля, загружает его синхронным XHR-запросом, и ищет в нем вхождения вызовов функции require. Хочу обратить внимание, что мы не исполняем загруженный код, а всего лишь ищем зависимости модуля с помощью этой функции. Файл модуля во время этой загрузки закешируется браузером, что в дальнейшем нам пригодится при подключении этого модуля.

var requirePattern = /(?:^|\s|=|;)require\(("|')([\w-\/\.]*)\1\)/g; function searchRequires(url) { 	var requires = []; 	var xhr = new XMLHttpRequest(); 	xhr.open("GET", url, false); 	xhr.onreadystatechange = function () { 		if ((xhr.readyState === 4) && (xhr.status === 200)) { 			var match; 			while ((match = requirePattern.exec(xhr.responseText)) !== null) { 				requires.push(match[1]); 			} 		} 	}; 	xhr.send(); 	return requires; } 

Функции getModuleId и getModuleContext предназначены для получения идентификатора и пути к модулю соответственно.

function getModuleId(moduleContext, moduleRequest) { 	var moduleId = []; 	(/^\.\.?\//.test(moduleRequest) ? (moduleContext + moduleRequest) : moduleRequest).replace(/\.(?:js|node)$/, "").split("/").forEach(function (value) { 		if (value === ".") { 		} else if (value === "..") { 			moduleId.pop(); 		} else if (/[\w\-\.]+/.test(value)) { 			moduleId.push(value); 		} 	}); 	return moduleId.join("/"); } 

function getModuleContext(moduleId) { 	return moduleId.slice(0, moduleId.lastIndexOf("/") + 1); } 

Функция require — та самая функция, которая в контексте модулей будет возвращать запрашиваемые закешированные экспорты. Эту функцию, предварительно прибив к контексту экземпляра нашего приложения и передав первым параметром путь текущего модуля, мы будем класть в window каждого ифрейма.

function require(moduleContext, moduleRequest) { 	var self = this; 	var moduleId = getModuleId(moduleContext, moduleRequest); 	if (self.modules[moduleId] && self.modules[moduleId].exports) { 		return self.modules[moduleId].exports; 	} else { 		throw Error("Module not found."); 	} } 

Ну и, наконец, рассмотрим две функции, выполняющие всю основную работу.

Рекурсивня функция enqueueModule добавляет переданный в качестве параметра модуль в очередь, а также, вызывая сама себя для каждой из зависимостей, добавляет и их. В итоге мы получим очередь загрузки модулей, в самом конце которой будет главный модуль — точка входа в приложение. Благодаря этой очереди каждый загружаемый модуль уже будет иметь в распоряжении все закешированные модули, от которых он зависит.

function enqueueModule(moduleId) { 	var self = this; 	var moduleQueue = []; 	if (!self.modules[moduleId]) { 		self.modules[moduleId] = { 			url: self.path + moduleId + ".js?ts=" + (new Date()).valueOf() 		}; 		moduleQueue.push(moduleId); 		searchRequires(self.modules[moduleId].url).forEach(function (value) { 			Array.prototype.push.apply(moduleQueue, enqueueModule.bind(self)(getModuleId(getModuleContext(moduleId), value))); 		}); 	} 	return moduleQueue; } 

Функция loadNextModule проходит по возвращенной функцией enqueueModule очереди и по порядку загружает в браузер наши модули (файлы браузер при этом будет брать из своего кеша, так как мы их уже загружали для поиска зависимостей). Для подключения каждого модуля, как мы договорились выше, используется отдельный ифрейм, в котором мы создаем переменные global, exports и module.exports, а также функцию require. Каждый следующий ифрейм загружается только после полной загрузки предыдущего скрипта. Когда очередь загрузки подходит к концу, мы вызываем переданную в самом начале функцию обратного вызова, если таковая имеется, и передаем в нее экспорт последнего модуля.

function loadNextModule(moduleQueue, callback) { 	var self = this; 	if (moduleQueue.length) { 		var iframe = document.createElement("iframe"); 		iframe.src = "about:blank"; 		iframe.style.display = "none"; 		iframe.onload = function () { 			var moduleId = moduleQueue.pop(); 			var iframeWindow = this.contentWindow; 			var iframeDocument = this.contentDocument; 			iframeWindow.global = window; 			iframeWindow.require = require.bind(self, getModuleContext(moduleId)); 			iframeWindow.module = { 				exports: {} 			} 			iframeWindow.exports = iframeWindow.module.exports; 			var script = iframeDocument.createElement("script"); 			script.src = self.modules[moduleId].url; 			script.onload = function () { 				self.modules[moduleId].exports = iframeWindow.module.exports; 				if (moduleQueue.length) { 					loadNextModule.bind(self)(moduleQueue, callback); 				} else if (typeof callback === "function") { 					callback(self.modules[moduleId].exports); 				} 			}; 			iframeDocument.head.appendChild(script); 		}; 		document.body.appendChild(iframe); 	} else if (typeof callback === "function") { 		callback(); 	} } 

В качестве бонуса добавим функционал, позволяющий запустить наше приложение сразу после загрузки скрипта comeon.js.

var script = Array.prototype.slice.call(document.getElementsByTagName("script"), -1)[0]; var main = script.getAttribute("data-main"); if (main) { 	window.addEventListener("load", function () { 		var comeon = new Comeon(script.getAttribute("data-path") || "/"); 		comeon.require(main); 	}); } 

Вот и все. Теперь мы можем использовать написанные в формате CommonJS модули на стороне браузера и дебажить их в свое удовольствие. Для этого нам нужно всего лишь подключить comeon.js с указанием пути к скриптам и имени главного модуля в data-атрибутах:

<script src="http://rawgithub.com/avgaltsev/comeon/master/comeon.js" data-path="scripts/" data-main="main"></script> 

Либо, если в процессе выполнения нужно подключать несколько независимых друг от друга модулей, или если у приложения несколько точек входа, можно использовать более многословный синтаксис:

<script src="http://rawgithub.com/avgaltsev/comeon/master/comeon.js"></script> <script> 	window.onload = function () { 		var comeon = new Comeon("scripts/"); 		// Точка входа 		comeon.require("main"); 		// Другая точка входа 		comeon.require("another_main", function (exports) { 			console.log(exports); 		}); 	}; </script> 

ссылка на оригинал статьи http://habrahabr.ru/post/201428/