Чиним Steam

Устаревшая фигня! Этот пост содержит неактуальное старье и оставлен для истории и лулзов.

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

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

В общем, вот что вышло:

Демка

А теперь про проблемы сайта Стима и как я их решал, по полочкам:

Проблема
Урезанная мобильная версия

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

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

Решение
Адаптивная верстка

Делаем адаптивную демку. Адаптивная верстка увеличивает время и сложность разработки, зато весь функционал сайта будет доступен сразу на любом устройстве, а поддержка и добавление фич упростится. В особо сложных ситуациях можно использовать комбинированный подход: генерировать на сервере часть страницы по-разному в зависимости от устройства. Например, можно отдавать на разные устройства картинки разных размеров.

Демка сделана по принципу mobile first, то есть базовые стили для маленьких экранов, на которые с ростом размера экрана постепенно наращиваются дополнительные стили.

«Адаптируем» галерею

Галерея со скриншотами должна работать на любых устройствах, с любым типом тач-эвентов. Меняем ее на Peppermint, добавляем превьюшки, дописываем скроллер превьюшек, взяв за основу код работы с событиями из пепперминта (который я, кстати, выделил в отдельный скрипт). Теперь и скришноты, и превьюшки можно проматывать как мышкой, так и тачем. Связываем слайдер и превьюшки, добавляем стрелочки:

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

Оформляем все в виде jQuery-плагина:

$.fn.steamGallery = function() {
$(this).each(function() {
var body = $('body'),
slidesBlock = $(this).find('.slides'),
thumbsBlock = $(this).find('.thumbs'),
thumbs = $(this).find('.thumb'),
arrPrev = $(this).find('.arrow-prev'),
arrNext = $(this).find('.arrow-next'),
slidesNumber,
currentSlide;

/* init the thumb scroller and save its API */
var scroller = Slime(thumbsBlock[0]);

/* init the slider and save its API */
var gallery = Peppermint(this, {
slidesContainer: slidesBlock[0],
dots: true,
mouseDrag: true,
slideshow: true,
slideshowInterval: 5000,
stopSlideshowAfterInteraction: true,
onSlideChange: function(n) {
/* activate appropriate thumb */
thumbs.removeClass('active');

thumbs.eq(n).addClass('active');

/* move active thumb to the viewport, if it's not there */
scroller.moveElementToViewport(thumbs[n], 24);

/* see if an arrow should be disabled */
arrPrev.removeClass('disabled');
arrNext.removeClass('disabled');

if (n == gallery.getSlidesNumber()-1) {
arrNext.addClass('disabled');
}
else if (n == 0) {
arrPrev.addClass('disabled');
}

/* save current slide number */
currentSlide = n;
}
});

/* get total number of slides */
slidesNumber = gallery.getSlidesNumber();

/* bind click & enter handlers to thumbs */
for (var i = thumbs.length - 1; i >= 0; i--) {
$(thumbs[i]).on('click keyup', function(n) {
return function(event) {
if (
scroller.getClicksAllowed() &&
(
event.type == 'click' ||
event.keyCode == 13
)
) {
gallery.slideTo(n);
gallery.stop();
}
};
}(i));
};

// bind event handlers to arrows
arrPrev.on('click keyup', function(event) {
if (event.type == 'keyup' && event.keyCode !== 13) return;

prev();

/* prevent zooming and clicking after touch */
event.preventDefault();
event.stopPropagation();
});

arrNext.on('click keyup', function(event) {
if (event.type == 'keyup' && event.keyCode !== 13) return;

next();

/* prevent zooming and clicking after touch */
event.preventDefault();
event.stopPropagation();
});

function prev() {
if (currentSlide == 0) return;

gallery.prev();
gallery.stop();
}

function next() {
if (currentSlide == slidesNumber - 1) return;

gallery.next();
gallery.stop();
}
});

return this;
};

Фон

Делаем фон на всю страницу. Чтобы мобильные устройства не расстраивались от большой картинки, отдаем им картинку поменьше. Сравните полный и мобильный варианты фона.

Так как у каждой страницы в магазине фон разный, кладем стиль прямо в шапку страницы, не забыв учесть старые ИЕ, не понимающие media queries:

<!--[if lt IE 9]>
<style>
body {
background-color: #1e231f;
background-image: url(i/page.bg.jpg);
}
</style>
<![endif]-->

<!--[if gt IE 8]><!-->
<style>
body {
background-image: url(i/page.bg.mob.jpg);
}
@media all and (min-width: 75em) {
body {
background-color: #1e231f;
background-image: url(i/page.bg.jpg);
}
}
</style>
<!--<![endif]-->

Чтобы мобильники еще больше нас любили, убираем для них почти все тени, полупрозрачные фоны заменяем на непрозрачные.

Проблема
Контент подчиняется дизайну

Вот так, например, сейчас выглядит блок про DLC на сайте Стима:

Что будет, если фразу удлинить в 2 раза? Что получится, если потом перевести эту фразу на язык, в котором она станет еще длиннее? Вот что:

Блок про DLC порвало :-(

У блока фиксированная высота и ширина (ширина непонятно зачем, ведь у родительского блока точно такая же ширина), а на фоне картинка. Даже в то время, когда не было классных CSS3-свойств, можно было сделать подобный блок резиновым. С костылями, но без особых проблем.

Решение
Подчиняем дизайн контенту

Downloadable Content

Requires the base game Dishonored on Steam in order to play.

Все удовольствие одним блоком. Заголовок, параграф и пачка стилей:

<div class="steam-demo game-dlc-notice">
<h3>Downloadable Content</h3>
<p class="small">
Requires the base game <a href="#">Dishonored</a>
on Steam in order to play.
</p>
</div>
.game-dlc-notice {
position: relative;
padding: 16px; /* fallback */
padding: 1rem;
border: 1px solid;
border-color: #6b2269 #6b2269 #6b2250 #6b2269;
border-radius: 0.3em;
background: #411541; /* fallback */
background-image: -webkit-linear-gradient(#411541, #290e23);
background-image: -moz-linear-gradient(#411541, #290e23);
background-image: linear-gradient(#411541, #290e23);
}

.game-dlc-notice:before {
content: '';
position: absolute;
display: block;
top: -14px;
left: 10%;
width: 0;
border: 14px solid;
border-color: transparent transparent #6b2269 transparent;
border-top-width: 0;
}

.game-dlc-notice:after {
content: '';
position: absolute;
display: block;
top: -12px;
left: 10%;
width: 0;
border: 14px solid;
border-color: transparent transparent #411541 transparent;
border-top-width: 0;
}

.game-dlc-notice > h3,
.game-dlc-notice > *:last-child
{
margin: 0;
}

Старые браузеры получат блок без градиента и круглых уголков, не велика беда.

Проблема с недостаточно гибким дизайном не заканчивается одним блоком. Долгое время блоки с ценами, рассчитанные на доллары, разваливались в русском магазине. Сейчас проблема по большей части решена, но блоки с фиксированной шириной все еще попадаются:

Стим не выдержит еще одного обвала рубля :-)

По соседству живет другой такой же по виду блок, который, однако, прекрасно чувствует себя в нестандартных условиях:

Это наталкивает нас на еще одну проблему:

Проблема
Неуниверсальный код

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

Решение
Делаем код универсальным

Делаем универсальный блок с ценой:

-1% £3.00 £2.97
-33% $49.99 $32.99
-600% 100 000 рублей -500 000 рублей
-66% ¥ 999 ¥ 333

Для изменения размера блока достаточно изменить размер шрифта. Все метрики выставлены в em’ах и изменяются пропорционально шрифту. Значения завернуты в дополнительные спаны, чтобы можно было выставить для них размер шрифта не побив метрики родительского блока:

<div class="price-area">
<span class="discount">
<span>-33%</span>
</span>

<span class="price">
<del class="original-price">
<span>$49.99</span>
</del>

<span class="final-price">
<span>$32.99</span>
</span>
</span>
</div>
.price-area {
display: inline-block;
overflow: hidden; /* clearfix */
vertical-align: top;
margin-bottom: 0.5em;
color: white;
}

.price-area .discount {
float: left;
height: 2.5em;
line-height: 2.5em;
padding: 0 0.5em;
background: #94de35;
}

.price-area .discount > span {
font-size: 1.8em;
}

.price-area .price {
float: left;
overflow: hidden; /* clearfix */
height: 2.5em;
line-height: 2.5em;
padding: 0 0.5em;
margin-right: 0.5em;
background: #111;
}

.price-area .price .original-price {
display: block;
line-height: 1.1em;
text-decoration: line-through;
color: #999;
text-align: left;
}

.price-area .price .original-price > span {
font-size: 0.8em;
}

.price-area .price .final-price {
display: block;
line-height: 1.2em;
text-align: left;
}

Кончились скидки? Выставляем цену без скидок. Убираем все лишнее из верстки:

<div class="price-area">
<span class="price">
$5.99
</span>
</div>

И все работает.

¥ 999
599 руб.
$5.99

Та же история с любыми повторяющимися блоками. Например, блок с юзерпиком и именем пользователя:

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

  • Базовые стили — основной шрифт, стили базовых элементов, отступы и размеры шрифтов в параграфах, заголовках, списках и т. п.
  • Вспомогательные классы — модификаторы размера кегля (побольше, поменьше), цвет информационных сообщений, ошибок, предупреждений, другие универсальные утилитарные классы.
  • Разметка — базовые блоки (лэйаут) страницы.
  • Сетка (грид). Я не люблю строгие сетки. В демке сетка используется как вспомогательный набор классов, чтобы не повторять одно и то же много раз. В любой момент можно забить на сетку и написать кастомных стилей для блока, чем я и пользуюсь.
  • Модули — это как раз отдельные повторяющиеся блоки, базовые стили которых не должны зависеть от контекста (но могут быть изменены стилями контекста, см. далее). Модули могут вкладываться друг в друга.
  • Стили страницы — стили специфичных для страницы блоков. Это как раз то место, где можно модифицировать стили модулей, расположенных в конкретных блоках страницы.

Проблема
«Навязчивый» яваскрипт

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

На сайте Стима присутствуют все классический ошибки, собранные мной в посте про ссылки. Вот, например, ссылка «View all screenshots», которая и не ссылка вовсе, так как никуда не ведет:

<a class="linkbar" href="javascript:screenshot_popup('http://store.steampowered.com/screenshot/view/205100/0?snr=1_5_9__400', 800, 635, 0, 0);">...</a>

А вот кнопка предыдущего спотлайта на главной, сделанная из элемента a:

<a href="javascript:PrevSpotlight( 2 );"><img src="http://cdn4.store.steampowered.com/public/images/v5/ico_navArrow_left.gif"> Prev</a>

А еще есть вот такие посты в центре сообщества:

Их код выглядит так:

<div class="apphub_Card interactable" style="float: left; width: 468px; height: 345px;" onclick="ShowModalContent( 'http://steamcommunity.com/app/205100/discussions/0/648813728349716360/?insideModal=1', 'Read at http://steamcommunity.com/app/205100/discussions/0/648813728349716360/', 'http://steamcommunity.com/app/205100/discussions/0/648813728349716360/' );">

...

</div>

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

Решение
Делаем яваскрипт ненавязчивым

Весь блок можно сделать ссылкой и открывать попап (если ну прям очень хочется попап) только по нажатию левой кнопки.

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

Кроме всего перечисленного, «навязчивый» яваскрипт напрямую ведет к еще одной проблеме:

Проблема
Низкая отказоустойчивость

Что произойдет, если упадет CDN-сервер со скриптами? Если один из скриптов выполнится с ошибкой? Правильно, половина функционала сайта просто перестанет работать. А могла бы работать, хоть и не так хорошо, как со скриптами.

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

Решение
Используем грамотные фоллбеки

Кладем картинки из галереи в блок с горизонтальным скроллом, который после инициализации превратится в нормальную галерею. Так как элементы управления без яваскрипта бесполезны, их не стоит показывать до инициализации:

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

Для реализации такого подхода достаточно выдать галерее класс inactive, который сменится на active во время инициализации, и написать два набора стилей для обоих состояний.

Та же история с кнопкой добавления в избранное, кнопками голосования и т. п. — можно обернуть их в форму и перехватывать submit скриптом. Без яваскрипта будет отправляться форма, а сервер может редиректить пользователя обратно на страницу, на которой он нажал кнопку.

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

Еще по мелочам

Доступность использования

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

Исправляется выдачей аттрибута tabindex="0" активным элементам и завязкой нажатия энтера на обработчик клика.

Скорость загрузки

При загрузке страницы игры в Стиме происходит 120 обращений к серверу, из них 92 картинки, 18 скриптов и 8 стилей. Причем все скрипты грузятся в шапке сайта, что сильно затормаживает отрисовку страницы.

Объединяем стили и скрипты в один файл, загружаем стили в шапке, а скрипты перед закрывающим тегом </html> (кроме Модернайзера, его кладем в шапку, так как он влияет на стили страницы). Объединяем картинки в спрайт. Там, где можно, используем CSS3-фичи вместо картинок.

Все это резко снижает количество обращений к серверу и время начала отрисовки страницы. В моей демке 25 обращений к серверу, из которых 21 картинка, 2 скрипта и 1 стиль. В оформлении используется два png-спрайта — один для обычный дисплеев и один для high density (в старых браузерах загрузится три фоллбек-картинки для полупрозрачного фона и градиентов). Сначала я вообще использовал один svg-спрайт, но, к сожалению, он очень сильно затормаживает некоторые мобильные браузеры, а в мобильном IE сильно «блюрит», поэтому пока приходится использовать png или иконочные шрифты (у которых есть свои проблемы).

Естественно, на боевом сервере количество загружаемых ресурсов может возрасти, однако разница в количестве запросов очевидна.

Интерфейс и навигация

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

Так как я на дизайнера интерфейсов слабо тяну, останавливаться на этом моменте не буду.

Итого

Что сделано: адаптивная демка, соответствующая принципам прогрессивного улучшения и ненавязчивого яваскрипта, с повышенной отказоустойчивостью. Работает в ИЕ8+ и практически на всех мобильных девайсах и браузерах.

Что не сделано: шапка, футер, HTML5-видео с фоллбеком на флеш (для трейлеров игрушек), пропущена пара блоков, присутствующих на оригинальной странице.

Разминка объявляется законченной.

Бонус-пак

Код одного из пунктов главного меню Стима:

<a class="menuitem supernav" href="http://store.steampowered.com/" data-tooltip-content="
&lt;a class=&quot;submenuitem&quot; href=&quot;http://store.steampowered.com/&quot;&gt;Featured&lt;/a&gt;
&lt;a class=&quot;submenuitem&quot; href=&quot;http://store.steampowered.com/news/&quot;&gt;News&lt;/a&gt;
&lt;a class=&quot;submenuitem&quot; href=&quot;http://store.steampowered.com/recommended/&quot;&gt;Recommended&lt;/a&gt;
&lt;a class=&quot;submenuitem&quot; href=&quot;http://steamcommunity.com/my/wishlist/&quot;&gt;Wishlist&lt;/a&gt;
&lt;a class=&quot;submenuitem&quot; href=&quot;http://store.steampowered.com/stats/&quot;&gt;STATS&lt;/a&gt;
"
>

STORE </a>