Стиль в подключаемых модулях jQuery, и почему он имеет значение

"Ковбой" Бен Алман (Ben Alman) | 14 мая 2010 г.

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

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

Данная статья разделяется на три основных раздела.

  1. Несколько профессиональных советов
  2. Правила хорошего тона
  3. Элементы стиля
  4. Заключение

1. Несколько профессиональных советов

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

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

Принцип DRY = Don't repeat yourself (не повторять себя)

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

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

Кроме того, поскольку вы можете не осознавать, что повторяете сами себя, пока не вернетесь назад и не взглянете на свой код, рекомендуется периодически это делать. Вернитесь назад, взгляните на код и максимально исключите дублирование кода!

// Плохо: много повторов, не во всем коде соблюдается принцип DRY.
 
$('body')
  .bind( 'click', function(e){ console.log( 'click: ', e.target ); })
  .bind( 'dblclick', function(e){ console.log( 'dblclick: ', e.target ); })
  .bind( 'keydown', function(e){ console.log( 'keydown: ', e.target ); })
  .bind( 'keypress', function(e){ console.log( 'keypress: ', e.target ); })
  .bind( 'keyup', function(e){ console.log( 'keyup: ', e.target ); });
 
 
// Хорошо: в коде больше соблюдается принцип DRY, это значительное улучшение,
// но на первый взгляд не так очевидно, что делается.
 
function myBind( name ) {
  $('body').bind( name, function(e){ console.log( name + ': ', e.target ); })
};
 
myBind( 'click' );
myBind( 'dblclick' );
myBind( 'keydown' );
myBind( 'keypress' );
myBind( 'keyup' );
 
 
// Лучше: обработчик обобщается и использует свойство event.type,
// и совершенно очевидно, что делается, даже на первый взгляд.
 
function myHandler( e ) {
  console.log( e.type + ': ', e.target );
};
 
$('body')
  .bind( 'click', myHandler )
  .bind( 'dblclick', myHandler )
  .bind( 'keydown', myHandler )
  .bind( 'keypress', myHandler )
  .bind( 'keyup', myHandler );
 
 
// Отлично: действительно понятно, как работа API jQuery может уменьшить
// сложность кода и сделать его гораздо более удобочитаемым.
 
$('body').bind( 'click dblclick keydown keypress keyup', function(e){
  console.log( e.type + ': ', e.target );
});

Использование API jQuery

Как можно видеть в предыдущем примере, ничто не может заменить понимание, как лучше всего использовать встроенные методы jQuery. Прочитайте документацию по API и примеры, и в любом случае изучите исходный код jQuery, поскольку это источник других подключаемых модулей.

Чем лучше вы будете знать и использовать API jQuery API, тем более понятным и удобочитаемым будет ваш код. Кроме того, благодаря использованию встроенных методов jQuery часто будет возможно исключать части собственного кода, что в результате уменьшает необходимый объем обслуживания.

Избегайте преждевременных оптимизаций

Хотя оптимизация может быть очень важной, она не так важна, как период создания рабочего кода. Наибольшая проблема оптимизации часто состоит в том, что оптимизированный код сложнее для чтения и понимания, чем код перед оптимизацией.

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

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

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

Избегайте излишне уклоняться от преждевременных оптимизаций

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

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

// Плохо: очень медленно, и нет даже признаков DRY.
 
$('#foo').appendTo( 'body' );
$('#foo').addClass( 'test' );
$('#foo').show();
 
// Хорошо: ссылка на объект jQuery "кэшируется" в переменной elem.
 
var elem = $('#foo')
 
elem.appendTo( 'body' );
elem.addClass( 'test' );
elem.show();
 
// Еще лучше: методы jQuery соединяются в цепочку.
 
$('#foo')
  .appendTo( 'body' )
  .addClass( 'test' )
  .show();
 
// Можно даже объединять кэширование с кэшированием, что может оказаться
// особенно удобно в условных выражениях.
 
var elem = $('#foo').appendTo( 'body' );
 
if ( some_condition ) {
  elem.addClass( 'test' );
} else {
  elem.show();
}

2. Правила хорошего тона

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

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

Не изменяйте объекты, которые вам не принадлежат

Я не собираюсь слишком глубоко погружаться в эту тему, поскольку это уже сделал Николас Закас (Nicholas Zakas), поэтому прочитайте его статью о том, что не следует изменять объекты, которые вам не принадлежат. Эта статья имеет прямое отношение к утверждению "Если люди пытаются использовать ваш подключаемый модуль, и он плохо работает с другими частями их кода, они больше не захотят его использовать".

Объявляйте свои переменные

Всегда объявляйте переменные перед их использованием с помощью ключевого слова var. Необъявленные переменные не являются локальными, как вы могли бы ожидать, фактически они создаются в глобальной области. Эти "неявные глобальные переменные" могут конфликтовать с другим кодом, что плохо — см. предыдущий пункт.

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

Используйте замкнутые выражения

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

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

// Шаблон подключаемого модуля jQuery верхнего уровня.
(function($){
  
  var myPrivateProperty = 1;
  
  // Вызов этого общего метода в виде $.myMethod();
  $.myMethod = function(){
    // Здесь находится код метода jQuery не для конкретного элемента.
  };
  
  // Вызов этого общего метода в виде $(elem).myMethod();
  $.fn.myMethod = function(){
    return this.each(function(){
      // Здесь находится код сцепляемого метода "объект jQuery".
    });
  };
  
  function myPrivateMethod(){
    // Прочий код.
  };
  
})(jQuery);

Используйте пространства имен при привязке обработчиков событий

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

// Плохо: метод будет отменять привязку всех остальных обработчиков нажатия 'body' в подключаемом модуле!
$('body').bind( 'click', handler ); // Привязка.
$('body').unbind( 'click' );        // Отмена привязки.
 
// Хорошо: отменяется привязка только обработчиков нажатия 'body' в 
// пространстве имен 'yourNamespace'!
$('body').bind( 'click.yourNamespace', handler ); // Привязка.
$('body').unbind( 'click.yourNamespace' );        // Отмена привязки.
 
// Тоже хорошо: отменяется привязка только обработчиков нажатия 'body', которые ссылаются на 
// функцию 'handler' (заметьте, что раз для этого метода требуется ссылка на 
// функцию, он не будет работать для событий, привязанных с помощью
// встроенной анонимной функции)
$('body').bind( 'click', handler );   // Привязка.
$('body').unbind( 'click', handler ); // Отмена привязки.

Используйте уникальные имена данных

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

// Плохо: не очень уникальное имя, может конфликтовать с другими данными кода.
$('#foo').data( 'text', 'hello world' );
 
// Хорошо: довольно уникальное имя, маловероятен конфликт с другими
// данными кода.
$('#foo').data( 'yourPluginName', 'hello world' );

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

function set_data() {
  var data = {
    text: 'hello world',
    awesome: false
  };
  
  // Сохранение объектов данных одновременно.
  $('#foo').data( 'yourPluginName', data );
  
  // Обновление свойств data.xyz будет также обновлять хранилище данных.
  data.awesome = true;
  data.super_awesome = true;
};
 
function get_data() {
  var data = $('#foo').data( 'yourPluginName' );
  alert( data.super_awesome );
}
 
set_data();
get_data(); // Оповещения истинны.

3. Элементы стиля

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

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

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

Также см. статью Дугласа Крокфорда (Douglas Crockford), посвященнуюправилам кодирования для JavaScript, на которой построены многие из предложений в этой статье.

Длина строк

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

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

Вероятно, вам приходит мысль. Горизонтальная прокрутка в текстовых редакторах и примерах кода — это нечто ужасное. Убейте двух зайцев одним выстрелом, сохраняйте приемлемую длину строк, и все будут довольны.

Табуляция в сравнении с пробелами

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

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

Какой вариант более удобен для чтения?

// Табуляция (представлена отступами в 8 пробелов)
function inArray( elem, array ) {
         if ( array.indexOf ) {
                 return array.indexOf( elem );
         }
         
         for ( var i = 0, length = array.length; i < length; i++ ) {
                 if ( array[ i ] === elem ) {
                         return i;
                 }
         }
         
         return -1;
};
 
// Отступы в 2 пробела
function inArray( elem, array ) {
   if ( array.indexOf ) {
     return array.indexOf( elem );
   }
   
   for ( var i = 0, length = array.length; i < length; i++ ) {
     if ( array[ i ] === elem ) {
       return i;
     }
   }
   
   return -1;
};

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

Объединение аргументов или блоки кода

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

Какой из фрагментов более удобен для чтения?

// Объединено.
function inArray(elem,array) {
   if (array.indexOf) {
     return array.indexOf(elem);
   }
   for (var i=0,length=array.length;i<length;i++) {
     if (array[i]===elem) {
       return i;
     }
   }
   return -1;
};
 
// О... немного места для передышки.
function inArray( elem, array ) {
   if ( array.indexOf ) {
     return array.indexOf( elem );
   }
   
   for ( var i = 0, length = array.length; i < length; i++ ) {
     if ( array[ i ] === elem ) {
       return i;
     }
   }
   
   return -1;
};

Комментарии

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

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

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

Фигурные скобки

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

Кроме того, в JavaScript место размещения открывающей фигурной скобки может изменить поведение кода, поэтому необходимо форматировать операторы return соответствующим образом, указывая открывающую фигурную скобку в одной строке с оператором return.

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

// Не слишком хорошо: этот пример относительно очевиден.
if ( a === 1 )
  b = 2;
 
// Плохо: этот пример кое в чем неоднозначен..
if ( a === 1 )
  b = 2;
  c = 3;
 
// Ой! Это то, как предыдущий пример фактически интерпретирован!
if ( a === 1 ) {
  b = 2;
}
c = 3;
 
// Хорошо: этот пример полностью очевиден.
if ( a === 1 ) {
  b = 2;
}
 
// Хорошо: этот пример также полностью очевиден.
if ( a === 1 ) {
  b = 2;
  c = 3;
}
 
// Плохо: см. выше ссылку на то, как расположение открывающих фигурных 
// скобок может изменять поведение кода.
function test()
{
  return
  {
    property: true
  };
};
 
// Хорошо: не только функция возвращает объект как ожидается, но и 
// последовательный стиль скобок { } используется.
function test() {
  return {
    property: true
  };
};

4. Заключение

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

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