Создаем прокрутку списка записей по типу Твиттера без jQuery
jQuery - удивительный инструмент. Но когда его не стоит использовать? В данном уроке мы рассмотрим вопрос, как создать интересную прокрутку содержания, а затем пересмотрим наш проект на предмет улучшения посредством потенциального отказа от jQuery там, где будет возможно.
Введение.
Вероятно, вы ожидаете, данный урок будет чем-то из серии "Кое-что удивительное, что может jQuery". Нет. Если вы хотите построить крутой, но вместе с тем, вероятно, бесполезный плагин по данному уроку, то читать дальше не стоит.
Целью нашего материала является показать, что на jQuery можно смотреть как на обычный JavaScript, в котором нет ничего магического. Иногда, ваш код может обладать большей "магией, если из него исключить немного великолепия jQuery. Очень может быть, что после усвоения данного урока вы будете понимать JavaScript немного лучше.
Если выше сказанное звучит для вас несколько абстрактно, то рассматривайте данный материал как урок по производительности и перестройке кода ... и как шаг за пределы зоны комфорта разработчика.
Шаг 1. Проект
Вот что мы будем строить. Твиттер для Mac показал отличную идею для реализации. Если запустить приложение и перейти на чью-нибудь страницу, то при прокрутке вниз станет видно, что иконка аватара присутствует не у всех записей, а "следует" за прокруткой страницы. Если встречаются следующие подряд несколько записей, то аватара выводится только для одной, расположенной на самом верху. При изменении автора записи изменяется и аватар.
Именно такой функционал мы и реализуем. С использованием jQuery выполнить задачу будет достаточно просто.
Шаг 2. HTML и CSS
Прежде чем начать работать над кодом, нужно построить разметку. Для нашего урока она будет выглядеть следующим образом:
<!DOCTYPE HTML> <html lang="ru"> <head> <meta charset="UTF-8"> <title>Прокрутка списка в стиле Twitter | Демонстрация для сайта RUSELLER.COM</title> <link rel="stylesheet" href="/style.css" /> </head> <body> <section> <article> <img class="avatar" src="/images/one.jpg" /> <p> Запись, сделанная пользователем.</p> </article> <article> <img class="avatar" src="/images/two.jpg" /> <p> Запись, сделанная пользователем.</p> </article> <article> <img class="avatar" src="/images/one.jpg" /> <p> Запись, сделанная пользователем. Просто текст для демонстрации.</p> </article> <article> <img class="avatar" src="/images/one.jpg" /> <p> Запись, сделанная пользователем.</p> </article> <!-- Здесь добавляем еще записей!--> </section> <script src="/jquery.js"></script> <script src="/twitter-scrolling.js"></script> <script> $("section").twitter_scroll(".avatar"); </script> </body> </html>
Да, код получился длинным.
Теперь добавим CSS.
body { font:13px/1.5 "helvetica neue", helvetica, arial, san-serif; } article { display:block; background: #ececec; width:380px; padding:10px; margin:10px; overflow:hidden; } article img { float:left; } article p { margin:0; padding-left:60px; }
Здесь только то, что действительно нужно для нашего урока.
Шаг 3. JavaScript. Раунд 1.
Надо сказать, что задача выходит за рамки создания виджета JavaScript средней сложности. Вот несколько моментов, которые надо принять во внимание при разработке:
- Нужно скрывать каждое изображение, если оно одинаковое с предыдущей записью.
- При прокрутке страницы надо определять, какая запись находится наиболее близко к верху страницы.
- Если запись является первой в серии, сделанной одной персоной, то необходимо зафиксировать аватар, чтобы он не прокручивался с остальным содержанием страницы.
- Когда запись является последней в серии одного автора, нужно оставить аватар на соответствующей строке.
- Все выше описанные моменты должны работать при прокрутке и вверх и вниз.
- Так как все выполняется каждый раз, при генерации события scroll, то код должен работать быстро.
В начале разработки нужно беспокоиться о работоспособности кода, а оптимизацию выполнять позже. Первая версия будет игнорировать несколько лучших методов работы с jQuery. Версия два предназначена именно для оптимизации кода jQuery.
Будем писать код в виде плагина jQuery:
$(wrapper_element).twitter_scroll(entries_element, unscrollable_element);
Объект jQuery, для которого вызывается плагин, содержит записи. Первый параметр плагина является селектором элементов, которые будут прокручиваться: записей. Второй параметр - селектор элементов, которые остаются на месте при необходимости (в плагине предполагается, что это будут изображения, но для других элементов плагин легко перенастроить). Итак, для кода HTML, который используется в нашей демонстрации, вызов плагина будет выглядеть следующим образом:
$("section").twitter_scroll("article", ".avatar"); // или $("section").twitter_scroll(".avatar");
Первый параметр будет опциональным: если он опущен, то мы считаем, что задан селектор элементов, которые не прокручиваются (не прокручиваются - плохой термин, но в рамках урока вполне понятный), а первый селектор будет определяться как непосредственный родитель.
Оболочка плагина:
(function ($) { jQuery.fn.twitter_scroll = function (entries, unscrollable) { }; }(jQuery));
Установки плагина
Начнем с кода установок. Нужно сделать несколько действий, прежде чем определять обработчик события scroll.
if (!unscrollable) { unscrollable = $(entries); entries = unscrollable.parent(); } else { unscrollable = $(unscrollable); entries = $(entries); }
Первый параметр: если unscrollable
имеет значение false, то мы с помощью jQuery и селектора entries
мы устанавливаем значение unscrollable
, а самой переменной entries
присваиваем значение родителя unscrollable
. Иначе, с помощью jQuery устанавливаются оба параметра. Теперь (если пользователь правильно установил разметку) у нас есть два объекта jQuery, которые соответствуют друг другу и имеют одинаковый индекс: unscrollable[i]
является потомком entries[i]
. Это пригодиться в дальнейшем. Если предполагать, что пользователь разметил свой документ не правильно, или что используются селекторы, которые выбирают элементы вне нужных нам областей, то можно использовать this
в качестве параметра контекста или метод find
для this
.
Далее, определим несколько переменных:
var parent_from_top = this.offset().top, entries_from_top = entries.offset().top, img_offset = unscrollable.offset(), prev_item = null, starter = false, margin = 2, anti_repeater = null, win = $(window), scroll_top = win.scrollTop();
Смещения элементов, с которыми мы работаем, очень важны. Метод jQuery offset
возвращает объект со свойствами top
и left
. Для родительского элемента (this
внутри плагина) и для самого элемента entries
нам нужны только смещения сверху. Для непрокручиваемых элементов нам нужно использовать смещения сверху и слева.
Переменные prev_item
, starter
, и anti_repeater
будут использоваться далее в обработчика события scroll, где нужны будут значения, существующие независимо от вызовов функций. Переменная win
будет использована в нескольких местах. А переменная scroll_top
содержит расстояние панели прокрутки от верха экрана. Мы будем использовать ее для определения направления прокрутки.
Далее мы определим, какие элементы являются первым и последним в полосе записей. Существует несколько способов выполнить задачу. Мы будем использовать атрибут HTML5 data для соответствующих элементов.
entries.each(function (i) { var img = unscrollable[i]; if ($.contains(this, img)) { if (img.src === prev_item) { img.style.visibility = "hidden"; if (starter) { entries[i-1].setAttribute("data-8088-starter", "true"); starter = false; } if (!entries[i+1]) { entries[i].setAttribute("data-8088-ender", "true"); } } else { prev_item = img.src; starter = true; if (entries[i-1] && unscrollable[i-1].style.visibility === "hidden") { entries[i-1].setAttribute("data-8088-ender", "true"); } } } }); prev_item = null;
Мы используем метод jQuery each
для объекта entries
. Помните, что внутри функции this
указывает на текущий элемент из entries
. Также у нас есть параметр индекс, который мы используем. Мы получаем соответствующий элемент из объекта unscrollable
и сохраняем его в img
. Затем, если наш элемент содержит img
(так должно быть, но нам нужно проверить), мы проверяем, что источник изображения тот же, что и prev_item
. Если это верно, то мы знаем, что изображение такое же, как и у предыдущей записи. Следовательно, надо скрыть изображение. Использовать свойство display нельзя, так как такое действие будет вызывать удаление изображения из потока документа, а нам не нужно перемещение других элементов на пустое место.
Затем, если значение starter
true, мы устанавливаем предыдущему элементу атрибут data-8088-starter
. Все атрибуты HTML5 data должны начинаться с “data-“. А использование собственного суффикса позволяет избежать конфликта с кодом других разработчиков (в данном случае используется код 8088). Атрибут HTML5 data должен иметь строковое значение, но в нашем случае мы используем его только как маркер элемента. Затем устанавливаем значение starter
false. И если это последний элемент, то маркируем соответствующим образом.
Если источник изображения не совпадает с предыдущим элементом, то изменяем значение prev_item
. Затем устанавливаем значение starter
true. Таким образом, если следующее изображение будет иметь такой же источник, мы промаркируем данный элемент как стартовый. Далее, если есть элемент перед текущим, и его изображение скрыто, то мы знаем, что он является последним в серии с одинаковым изображением (потому что текущий элемент имеет другое изображение). Следовательно, нужно установить соответствующий атрибут маркер.
По завершению обработки установим значение prev_item
null
, так как вскоре будем использовать его снова.
Теперь, если взглянуть на разметку после работы функции (например, с помощью Firebug), можно увидеть следующую структуру:
Обработчик события scroll
Теперь напишем обработчик события scroll. Здесь есть две задачи, которые надо решить. Первая - нужно найти самый близкий к верху страницы элемент. Вторая - сделать соответствующие манипуляции с изображением.
$(document).bind("scroll", function (e) { var temp_scroll = win.scrollTop(), down = ((scroll_top - temp_scroll) < 0) ? true : false, top_item = null, child = null; scroll_top = temp_scroll; // Здесь будет вставляться код });
Это оболочка нашего обработчика. Создаем несколько переменных. Переменная down
определяет направление прокрутки. Переменная scroll_top
содержит расстояние от верха, на которое произошла прокрутка.
Теперь присвоим переменной top_item
самый близкий к верху страницы элемент:
top_item = entries.filter(function (i) { var distance = $(this).offset().top - scroll_top; return ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ); });
Мы просто используем метод filter
для определения элемента, который должен попасть в перменную top_item
. Сначала определяем значение distance
как разность уже прокрученного расстояния и смещения элемента сверху. Затем, возвращаем true, если значение distance
находится между parent_from_top + margin
и parent_from_top - margin
. Нам необходимо учесть смещение контейнера сверху. Кроме того, событие scroll может генерироваться при некотором смещении от точного значения положения верха. Поэтому надо учесть поле, чтобы использовать диапазон, а не определённую точку.
Теперь у нас есть верхний элемент и можно работать дальше.
if (top_item) { if (top_item.attr("data-8088-starter")) { if (!anti_repeater) { child = top_item.children(unscrollable.selector); anti_repeater = child.clone().appendTo(document.body); child.css("visibility", "hidden"); anti_repeater.css({ 'position' : 'fixed', 'top' : img_offset.top + 'px', 'left' : img_offset.left + "px" }); } } else if (top_item.attr("data-8088-ender")) { top_item.children(unscrollable.selector).css("visibility", "visible"); if (anti_repeater) { anti_repeater.remove(); } anti_repeater = null; } if (!down) { if (top_item.attr("data-8088-starter")) { top_item.children(unscrollable.selector).css("visibility", "visible"); if (anti_repeater) { anti_repeater.remove(); } anti_repeater = null; } else if (top_item.attr("data-8088-ender")) { child = top_item.children(unscrollable.selector); anti_repeater = child.clone().appendTo(document.body); child.css("visibility", "hidden"); anti_repeater.css({ 'position' : 'fixed', 'top' : img_offset.top + 'px', 'left' : img_offset.left + "px" }); } } }
Вы наверняка обратили внимание, что выше приведенный код является почти точным повторением дважды одного и того же куска. Это неоптимизированный вариант. При работе над кодом сначала создается версия для прокрутки вниз, затем вариант для прокрутки наверх. Вскоре мы оптимизируем код полностью.
Если верхний элемент имеет атрибут data-8088-starter
, то поверяем значение переменной anti_repeater
. Данная переменная указывает на элемент изображения, который фиксируется при прокрутке. Если значение anti_repeater
еще не установлено, то получаем наследника нашего элемента top_item
, который имеет такой же селектор, как и unscrollable
(далее код будет оптимизирован). Затем мы клонируем его и добавляем к body. Затем скрываем оригинал, а клон размещаем туда, куда нужно.
Если элемент не имеет атрибута data-8088-starter
, то проверяем наличие атрибута data-8088-ender
. Если данный атрибут присутствует, находим потомка и делаем его видимым, а затем устанавливаем значение переменной anti_repeater
null
.
При прокрутке наверх нужно изменить два атрибута. А если top_item
не имеет никаких атрибутов, то мы находимся посредине набора и ничего менять не нужно.
Проверка производительности
Итак, наш код делает то, что нужно. Но при проверке видно, что прокрутка осуществляется очень медленно. Можно добавить строки console.profile("scroll")
и console.profileEnd()
к первой и последней строкам функции обработки события scroll. Обработка может занять от 2.5ms до 4ms, и будет сделано 166 – 170 вызовов функции.
Очень медленная обработка. Некоторые функции вызываются 30-31 раз. В нашем тестовом списке 30 элементов. Поэтому, чем больше элементов мы будем иметь в списке, тем медленнее будет работать прокрутка! Нужно искать пути оптимизации процесса.
Шаг 4. JavaScript. Раунд 2
Если вы подозреваете, что jQuery является главным виновником данного безобразия, то окажетесь правы. Библиотеки, подобные jQuery очень удобны и облегчают работу с DOM, но плата за удобство - производительность. В нашем случае нужно использовать более продуктивный код. Откажемся от части кода jQuery в пользу непосредственной работы со структурой DOM, что, на самом деле, окажется не таким уж сложным процессом.
Обработчик события Scroll
Разберемся с очевидной частью, с тем, что мы делаем с top_item,
как только находим его. В текущий момент top_item
является объектом jQuery. Однако, все что мы делаем с помощью jQuery с top_item
тривиально сделать без использования библиотеки.
Вот что можно изменить для ускорения процесса:
- Перестроить выражение if для исключения повторений (это, скорее, касается структуры кода, а не производительности).
- Можно использовать метод JavaScript
getAttribute
вместо метода jQueryattr
. - Можно получать элемент из
unscrollable,
который соответствует элементуtop_item
, вместо использованияunscrollable.selector
. - Можно использовать методы JavaScript
clodeNode
иappendChild
вместо версий jQuery. - Можно использовать свойство
style
вместо метода jQuerycss
. - можно использовать метод JavaScript
removeNode
вместо метода jQueryremove
.
Применив все идеи, получим следующий код:
if (top_item) { if ( (down && top_item.getAttribute("data-8088-starter")) || ( !down && top_item.getAttribute("data-8088-ender") ) ) { if (!anti_repeater) { child = unscrollable[ entries.indexOf(top_item) ]; anti_repeater = child.cloneNode(false); document.body.appendChild(anti_repeater); child.style.visibility = "hidden"; style = anti_repeater.style; style.position = 'fixed'; style.top = img_offset.top + 'px'; style.left= img_offset.left + 'px'; } } if ( (down && top_item.getAttribute("data-8088-ender")) || (!down && top_item.getAttribute("data-8088-starter")) ) { unscrollable[ entries.indexOf(top_item) ].style.visibility = "visible"; if (anti_repeater) { anti_repeater.parentNode.removeChild(anti_repeater); } anti_repeater = null; } }
Данный код существенно лучше не только потому, что выброшены повторения, но и не используется jQuery.
Также оптимизации можно подвергнуть процесс получения top_item
. В текущий момент мы используем метод jQuery filter
. Если вы хорошо подумаете, то распознаете ущербность такого способа. Мы знаем, что будем получать только один элемент с помощью фильтра, но метод filter
об этом не знает и продолжает работать после того, как нужный элемент найден. В тестовом списке 30 элементов, так что перебор занимает достаточное количество времени. Сделаем вот так:
for (i = 0; entries[i]; i++) { distance = $(entries[i]).offset().top - scroll_top; if ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ) { top_item = entries[i]; break; } }
(В качестве альтернативы можно использовать цикл while с условием !top_item
. Разницы, практически, никакой нет.)
Таким образом, как только мы находим top_item
, поиск прекращается. Однако можно еще улучшить алгоритм. Так как прокрутка линейна, то можно предположить, какой элемент окажется ближе всего к верху страницы. Если прокрутка была вниз, то поиск нужно вести с последнего самого близкого элемента вниз по списку. А при прокрутке вверх - с последнего самого близкого элемента вверх по списку. Однако в текущий момент поиск всегда ведется от самого верхнего элемента на странице.
Как реализовать задуманные изменения? Начнем с сохранения top_item
от предыдущего запуска обработчика события scroll:
Присвоение надо поместить в самый конец функции обработчика. Теперь можно использовать имеющееся значение:
if (prev_item) { prev_item = $(prev_item); height = height || prev_item.outerHeight(); if ( down && prev_item.offset().top - scroll_top + height < margin) { top_item = entries[ entries.indexOf(prev_item[0]) + 1]; } else if ( !down && (prev_item.offset().top - scroll_top - height) > (margin + parent_from_top)) { top_item = entries[ entries.indexOf(prev_item[0]) - 1]; } else { top_item = prev_item[0]; } } else { for (i = 0; entries[i]; i++) { distance = $(entries[i]).offset().top - scroll_top; if ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ) { top_item = entries[i]; break; } } }
Если переменная prev_item
имеет значение, значит, мы можем использовать ее для поиска следующего элемента top_item
. Здесь используется небольшой трюк:
- Если предыдущий элемент находится полностью над верхом страницы, берем следующий элемент (можно использовать индекс
prev_item
+ 1 для получения нужного элемента вentries
). - Если прокрутка происходит вверх, и предыдущий элемент достаточно далеко под страницей, берем предшествующий ему элемент.
- В противном случае используем тот же элемент, что и в последний раз.
- Если нет значения в
top_item
, то используем цикл для поиска.
Есть еще несколько моментов. Что такое переменная height
? Если все элементы имеют одинаковую высоту, то мы можем исключить вычисление высоты при каждом вызове обработчика события scroll, перенеся вычисления в установки плагина. Добавляем в раздел установок:
height = entries.outerHeight(); if (entries.length !== entries.filter(function () { return $(this).outerHeight() === height; }).length) { height = null; }
Метод jQuery outerHeight
возвращает высоту элемента, включая отступы и поля. Если все элементы имеют одинаковую высоту с первым элементом, то height
будет установлена, а в противном случае значение height
будет null. При определении top_item
можно использовать height,
если она имеет значение.
Есть еще один момент. Можно подумать, что вычисление prev_item.offset().top
выполняется дважды. В действительности оно выполняется только один раз, так как второе выражение if выполняется только в случае, если значение down
является false. Так как мы используем логический оператор AND, то вторая часть двух выражений if никогда не будет вызываться в одно и то же время. Конечно же, вы можете использовать перменную, если считаете нужным в данном случае, но она никак не повлияет на производительность.
Однако есть еще над чем поработать. Безусловно, наш обработчик стал быстрее, но мы можем сделать дополнительные улучшения. Мы используем jQuery только для двух процессов: получаем outerHeight
для prev_item,
и получаем смещение от верха для prev_item
. Для таких мелочей слишком большое расточительство, использовать объекты jQuery. Но моментального решения для замещения jQuery в данном случае нет. Нужно погрузиться в код jQuery, чтобы точно определить, что делает библиотека. Таким образом, мы сможем заменить небольшую часть кода без использования избыточного веса остального jQuery.
Начнем со смещения от верха. Вот код jQuery для метода offset
:
function (options) { var elem = this[0]; if (!elem || !elem.ownerDocument) { return null; } if (options) { return this.each(function (i) { jQuery.offset.setOffset(this, options, i); }); } if (elem === elem.ownerDocument.body) { return jQuery.offset.bodyOffset(elem); } var box = elem.getBoundingClientRect(), doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement, clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, top = box.top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop) - clientTop, left = box.left + (self.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft) - clientLeft; return { top: top, left: left }; }
Выглядит сурово. Однако не все так плохо. Нам нужно только смещение сверху. Поэтому можно смело выкинуть часть кода, которая касается смещения слева и сделать свою функцию:
function get_top_offset(elem) { var doc = elem.ownerDocument, body = doc.body, docElem = doc.documentElement; return elem.getBoundingClientRect().top + (self.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop) - docElem.clientTop || body.clientTop || 0; }
Все, что нужно сделать - это передать в нее элемент DOM. И мы получим смещение! Теперь можно заменить все вызовы offset().top
нашей функцией.
А как на счет outerHeight
? Здесь будет небольшой трюк, так как в данном случае используется внутренняя функция jQuery.
function (margin) { return this[0] ? jQuery.css(this[0], type, false, margin ? "margin" : "border") : null; }
В действительности здесь происходит только вызов функции css
с параметрами: элемент, “margin”, “border”, и false
. Вот как она выглядит:
function (elem, name, force, extra) { if (name === "width" || name === "height") { var val, props = cssShow, which = name === "width" ? cssWidth : cssHeight; function getWH() { val = name === "width" ? elem.offsetWidth : elem.offsetHeight; if (extra === "border") { return; } jQuery.each(which, function () { if (!extra) { val -= parseFloat(jQuery.curCSS(elem, "padding" + this, true)) || 0; } if (extra === "margin") { val += parseFloat(jQuery.curCSS(elem, "margin" + this, true)) || 0; } else { val -= parseFloat(jQuery.curCSS(elem, "border" + this + "Width", true)) || 0; } }); } if (elem.offsetWidth !== 0) { getWH(); } else { jQuery.swap(elem, props, getWH); } return Math.max(0, Math.round(val)); } return jQuery.curCSS(elem, name, force); }
В данной функции можно легко потеряться, но распутать логику стоит... Потому что решение удивительно! Все, что нам нужно - использовать свойство элемента offsetHeight
!
Таким образом, наш код превращается в:
if (prev_item) { height = height || prev_item.offsetHeight; if ( down && get_top_offset(prev_item) - scroll_top + height < margin) { top_item = entries[ entries.indexOf(prev_item) + 1]; } else if ( !down && (get_top_offset(prev_item) - scroll_top - height) > (margin + parent_from_top)) { top_item = entries[ entries.indexOf(prev_item) - 1]; } else { top_item = prev_item; } } else { for (i = 0; entries[i]; i++) { distance = get_top_offset(entries[i]) - scroll_top; if ( distance < (parent_from_top + margin) && distance > (parent_from_top - margin) ) { top_item = entries[i]; break; } } }
Теперь нет необходимости в объекте jQuery! И если запустить проверку производительности опять, то результат будет существенно лучше и иметь только пару вызовов функций.
Полезный инструмент
В таких случаях очень полезным инструментом является проект jQuery Source Viewer, с помощью которого можно исследовать внутренности jQuery.
Заключение
В данном уроке мы рассмотрели процесс создания прокрутки списка записей. Но главное заключается в другом:
- Факт, что ваш код выполняет то, что задумывалось, не означает, что он ясный, или быстрый, или его нельзя улучшить
- jQuery — или другая ваша любимая библиотека — это простой JavaScript. Она тоже может быть медленной и иногда от нее лучше отказаться.
- Лучший способ для развития разработчика - выйти из зоны комфорта.
Источник: http://feedproxy.google.com/~r/ruseller/CdHX/~3/F64BkzIs_-s/lessons.php
Дайджест новых статей по интернет-маркетингу на ваш email
Новые статьи и публикации
- 2024-11-26 » Капитан грузового судна, или Как начать использовать Docker в своих проектах
- 2024-11-26 » Обеспечение безопасности ваших веб-приложений с помощью PHP OOP и PDO
- 2024-11-22 » Ошибки в Яндекс Вебмастере: как найти и исправить
- 2024-11-22 » Ошибки в Яндекс Вебмастере: как найти и исправить
- 2024-11-15 » Перенос сайта на WordPress с одного домена на другой
- 2024-11-08 » OSPanel 6: быстрый старт
- 2024-11-08 » Как установить PhpMyAdmin в Open Server Panel
- 2024-09-30 » Как быстро запустить Laravel на Windows
- 2024-09-25 » Next.js
- 2024-09-05 » OpenAI рассказал, как запретить ChatGPT использовать содержимое сайта для обучения
- 2024-08-28 » Чек-лист: как увеличить конверсию интернет-магазина на примере спортпита
- 2024-08-01 » WebSocket
- 2024-07-26 » Интеграция с Яндекс Еда
- 2024-07-26 » Интеграция с Эквайринг
- 2024-07-26 » Интеграция с СДЕК
- 2024-07-26 » Интеграция с Битрикс-24
- 2024-07-26 » Интеграция с Travelline
- 2024-07-26 » Интеграция с Iiko
- 2024-07-26 » Интеграция с Delivery Club
- 2024-07-26 » Интеграция с CRM
- 2024-07-26 » Интеграция с 1C-Бухгалтерия
- 2024-07-24 » Что такое сторителлинг: техники и примеры
- 2024-07-17 » Ошибка 404: что это такое и как ее использовать для бизнеса
- 2024-07-03 » Размещайте прайс-листы на FarPost.ru и продавайте товары быстро и выгодно
- 2024-07-01 » Профилирование кода в PHP
- 2024-06-28 » Изучаем ABC/XYZ-анализ: что это такое и какие решения с помощью него принимают
- 2024-06-17 » Зачем вам знать потребности клиента
- 2024-06-11 » Что нового в работе Яндекс Метрики: полный обзор обновления
- 2024-06-11 » Поведенческие факторы ранжирования в Яндексе
- 2024-06-11 » Скорость загрузки сайта: почему это важно и как влияет на ранжирование
Всегда храни верность своему начальнику - следующий, может быть еще хуже... |
Мы создаем сайты, которые работают! Профессионально обслуживаем и продвигаем их , а также по всей России и ближнему зарубежью с 2006 года!
Как мы работаем
Заявка
Позвоните или оставьте заявку на сайте.
Консультация
Обсуждаем что именно Вам нужно и помогаем определить как это лучше сделать!
Договор
Заключаем договор на оказание услуг, в котором прописаны условия и обязанности обеих сторон.
Выполнение работ
Непосредственно оказание требующихся услуг и работ по вашему заданию.
Поддержка
Сдача выполненых работ, последующие корректировки и поддержка при необходимости.