Pull to refresh

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

Reading time 9 min
Views 7.7K

Вступление


Доброго всем времени суток. Поздравляю вас с праздниками и перехожу к теме.
Когда вы выходите за рамки написания простых фрагментов jQuery и приступаете к разработке более сложных взаимодействий пользователей, ваш код может быстро стать громоздким и трудным для отладки. Эта статья покажет вам начать думать об этих взаимодействий в терминах «частичек поведения» используя паттерн проектирования object literal.

В последние несколько лет, JavaScript библиотеки дали начинающим разработчикам возможность добавлять дополнительные возможности взаимодействия с сайтом. Некоторые из них, например jQuery, имеют синтаксис настолько простой, что люди с нулевым опытом программирования научились быстро добавлять «свистки и колокольчики» на свои страницы. Надо только погуглить плагин, скопировать и вставить несколько десятков строк из пользовательского кода. Результат — клиент впечатлен, и вы добавляете jQuery к вашему резюме.

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

Знакомимся с Object Literal


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

Паттерн программирования Object Literal это такой способ организации кода, который заставляет вас думать в самом начале о том, что ваш код будет делать и какие части должны быть на месте в для того, чтобы сделать это. Это также неплохое средство, для того чтобы ваш код не «загрязнял глобальное пространство имен», что является хорошей практикой для всех проектов (особенно для больших). Object Literal является также способом инкапсуляции поведения.
var myObjectLiteral = {
  myBehavior1 : function() {
    /* Какое-нибудь действие */
  },

  myBehavior2 : function() {
    /* Другое действие */
  }
};


* This source code was highlighted with Source Code Highlighter.

Как искусственно упрощенный пример, приведу кусок jQuery для отображения или скрытия контента при нажатии на элемент списка:
$(document).ready(function() {
  $('#myFeature li')
    .append('<div/>')
    .each(function() {
      $(this).find('div')
        .load('foo.php?item=' + $(this).attr('id'));
    })
  .click(function() {
    $(this).find('div').show();
    $(this).siblings().find('div').hide();
  });
});


* This source code was highlighted with Source Code Highlighter.

Достаточно просто. И все же, даже в этом примере есть несколько вещей, которые вы можете захотеть изменить позже — например, способ установления URL для загрузки контента, куда загружать полученный контент и т.д. В представлении Object Literal функции четко отделяет эти аспекты. Это может выглядеть следующим образом:
var myFeature = {
  config : {
    wrapper : '#myFeature',
    container : 'div',
    urlBase : 'foo.php?item='
  },

  init : function(config) {
    $.extend(myFeature.config, config);
    $(myFeature.config.wrapper).find('li').
      each(function() {
        myFeature.getContent($(this));
      }).
      click(function() {
        myFeature.showContent($(this));
      });
  },

  buildUrl : function($li) {
    return myFeature.config.urlBase + $li.attr('id');
  },

  getContent : function($li) {
    $li.append(myFeature.config.container);
    var url = myFeature.buildUrl($li);
    $li.find(myFeature.config.container).load(url);
  },

  showContent : function($li) {
    $li.find('div').show();
    myFeature.hideContent($li.siblings());
  },

  hideContent : function($elements) {
    $elements.find('div').hide();
  }
};

$(document).ready(function() { myFeature.init(); });


* This source code was highlighted with Source Code Highlighter.

Так как начальный пример невероятно прост, вариант Object Literal больше по размеру. По правде говоря, метод Object Literal не поможет вам сэкономить строки кода, зато поможет вам избежать головной боли. Используя Object Literal, мы разбили наш код на логические части, для того чтобы легче найти то, что мы, возможно, захотим изменить в будущем. Мы сделали наш код расширяемым, предоставляя возможность передавать в переопределять конфигурацию по умолчанию. Мы также сделали некоторого рода самодокументацию — тепер легче «врубиться» в то, что делает наш кусок кода. По мере того как ваши потребности будут вырастать за пределы этого примера, преимущества Object Literal подхода станут все более очевидными (смотрите ниже).

Еще один пример


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

Шаг 1: HTML структура

Написание хорошего семантического HTML является важнейшим условием для написание хорошего JavaScript кода, так что давайте подумаем как может выглядеть HTML для нашей задачи. HTML должен:
  • Быть доступным (и работать) при отключенном JavaScript.
  • Обеспечить предсказуемый DOM чтобы подключить JavaScript.
  • Помочь избежать лишних id и class -ов.

С учетом этих принципов:
<div id="myFeature">
  <ul class="sections">
    <li>
      <h2><a href="/section/1">Section 1</a></h2>
      <ul>
        <li>
          <h3><a href="/section/1/content/1">Section 1 Title 1</a></h3>
          <p>The excerpt content for Content Item 1</p>
        </li>
        <li>
          <h3><a href="/section/1/content/2">Section 1 Title 2</a></h3>
          <p>The excerpt content for Content Item 2</p>
        </li>
        <li>
          <h3><a href="/section/1/content/3">Section 1 Title 3</a></h3>
          <p>The excerpt content for Content Item 3</p>
        </li>
      </ul>
    </li>      

    <li>
      <h2><a href="/section/2">Section 2</a></h2>
      <ul>
        <li>
          <h3><a href="/section/2/content/1">Section 2 Title 1</a></h3>
          <p>The excerpt content for Content Item 1</p>
        </li>
        <li>
          <h3><a href="/section/2/content/2">Section 2 Title 2</a></h3>
          <p>The excerpt content for Content Item 2</p>
        </li>
        <li>
          <h3><a href="/section/2/content/3">Section 2 Title 3</a></h3>
          <p>The excerpt content for Content Item 3</p>
        </li>
      </ul>
    </li>      

    <li>
      <h2><a href="/section/3">Section 3</a></h2>
      <ul>
        <li>
          <h3><a href="/section/3/content/1">Section 3 Title 1</a></h3>
          <p>The excerpt content for Content Item 1</p>
        </li>
        <li>
          <h3><a href="/section/3/content/2">Section 3 Title 2</a></h3>
          <p>The excerpt content for Content Item 2</p>
        </li>
        <li>
          <h3><a href="/section/3/content/3">Section 3 Title 3</a></h3>
          <p>The excerpt content for Content Item 3</p>
        </li>
      </ul>
    </li>      

  </ul>
</div>


* This source code was highlighted with Source Code Highlighter.

Обратите внимание, что мы не включили разметку для отображения навигации по разделу, эти части будут добавлены jQuery, так как они будут работать только с jQuery; пользователи без JavaScript получат просто красивую семантическую разметку. (Если что-нибудь отсюда кажется удивительным или запутанным, то можете погуглить POSH (plain-old semantic HTML) и progressive enhancement).

Шаг 2: Подготовка объекта

Моим первым шагом в создании объекта для задачи будет создание «заглушек» для объекта. Заглушки — это заполнители, которые помогают при планировании структуры кода. Наш объект будет иметь следующие методы:
var myFeature = {
  'config' : { },
  'init' : function() { },
  'buildSectionNav' : function() { },
  'buildItemNav' : function() { },
  'showSection' : function() { },
  'showContentItem' : function() { }
};


* This source code was highlighted with Source Code Highlighter.


Здесь стоит отметить myFeature.config, который хранит в одном месте значения по умолчанию. Мы добавим возможность переопределить значения по умолчанию, когда мы определим метод myFeature.init ().

Шаг 3: Код

После того как мы построили этот скелет, можно приступать к написанию кода. Давайте начнем с создания простого myFeature.config объекта и метода myFeature.init ():
'config' : {
  // контейнер по умолчанию #myFeature
  'container' : $('#myFeature')
},

'init' : function(config) {
  // переопределение конфигурации через init()
  if (config && typeof(config) == 'object') {
    $.extend(myFeature.config, config);
  }

  // создание и/или кеширование некоторых элементов DOM
  // которые мы будем использовать в нашем коде
  myFeature.$container = myFeature.config.container;

  myFeature.$sections = myFeature.$container.
    // выбираем только непосредственных потомков
    find('ul.sections > li');

  myFeature.$section_nav = $('<p/>')
   .attr('id','section_nav')
   .prependTo(myFeature.$container);  

  myFeature.$item_nav = $('<p/>')
   .attr('id','item_nav')
   .insertAfter(myFeature.$section_nav);  

  myFeature.$content = $('<p/>')
   .attr('id','content')
   . insertAfter(myFeature.$item_nav); 

 // строим навигацию по разделам
 // "кликаем" на первый пункт
 myFeature.buildSectionNav(myFeature.$sections); 
 myFeature.$section_nav.find('li:first').click();   

 // скрываем простой HTML
 myFeature.$container.find('ul.sections').hide();   

 // на всякий случай (вдруг пригодится) 
 myFeature.initialized = true
}


* This source code was highlighted with Source Code Highlighter.

Далее создадим метод myFeature.buildSectionNav ():
'buildSectionNav' : function($sections) {

  // пройдемся по разделам
  $sections.each(function() {

    var $section = $(this);  

    // создаем элемент подраздела
    $('<li/>')
     // используем первый h2
     // в разделе для названия раздела
     .text($section.find('h2:first').text())

     // добавляем
     .appendTo(myFeature.$section_nav)

     // используем data() для сохранения ссылки
     .data('section', $section)

     // привяжем обработчик click
     .click(myFeature.showSection);
});


* This source code was highlighted with Source Code Highlighter.

Далее, создадим метод myFeature.buildItemNav():
‘buildItemNav’ : function($items) {
  $items.each(function() {

  var $item = $(this);

  // Создаем элемент навигации
  $('<li>')

   // используем первый h3
     // в разделе для названия подраздела
   .text($item.find('h3:first').text())

   // add the list item to the item navigation
   .appendTo(myFeature.$item.nav)

   // используем data() для сохранения ссылки
   .data('item', $item)

  // привяжем обработчик click
   .click(myFeature.showContentItem);
});

* This source code was highlighted with Source Code Highlighter.

Наконец, мы напишем методы для отображения разделов и элементов контента:
'showSection' : function() {
 var $li = $(this);

 // опустошим подменю и зону контента
 myFeature.$item_nav.empty();
 myFeature.$content.empty();

 // возьмем данные из объекта jQuery
 // которые были сохранены при помощи data() в методе buildSectionNav
 var $section = $li.data('section');

 // пометим активный элемент
 $li.addClass('current')
  .siblings().removeClass('current');

 // найдем все подразделы раздела
 var $items = $section.find('ul li');

 // построим подменю
 myFeature.buildItemNav($items);

 // "кликнем" на первый пункт подменю
 myFeature.$item_nav.find('li:first').click();

},

'showContentItem' : function() {
 var $li = $(this);

 // пометим активный элемент
 $li.addClass('current')
  .siblings().removeClass('current');

// возьмем данные из объекта jQuery
// которые были сохранены при помощи data() в методе buildSectionNav
var $item = $li.data('item');

 // Заполним область контента
 myFeature.$content.html($item.html());
}


* This source code was highlighted with Source Code Highlighter.


Все что осталось сделать это вызвать myFeature.init():
$(document).ready(myFeature.init);

* This source code was highlighted with Source Code Highlighter.

Вы можете посмотреть на картину целиком здесь (+ немного CSS для вида).

Шаг 4: Меняем требования

Ни один проект не обходится без изменений требований в последнюю минуту, не так ли? Object Literal подход помогает, сделать необходимые изменения быстро и безболезненно.

Что делать, если нам необходимо получить содержание пункта выдержки с помощью AJAX, а не из HTML? Если не считать настройки бэкэнда:
var myFeature = {

  'config' : {
    'container' : $('#myFeature'),

    // настраиваемая функции для получения
    // URL для загрузки содержимого
    'getItemURL' : function($item) {
      return $item.find('a:first').attr('href');
    }

  },

  'init' : function(config) {
    // то же самое 
  },

  'buildSectionNav' : function($sections) {
    // то же самое 
  },

  'buildItemNav' : function($items) {
    // то же самое 
  },

  'showSection' : function() {
    // то же самое
  },

  'showContentItem' : function() {

    var $li = $(this);

    $li.addClass('current').
      siblings().removeClass('current');

    var $item = $li.data('item');
    var url = myFeature.config.getItemURL($item);

    // myFeature.$content.html($item.html());
    myFeature.$content.load(url);

  }

};


* This source code was highlighted with Source Code Highlighter.

Вам нужно больше гибкости? Здесь много чего можно настроить (и, следовательно, переопределить). Например, вы можете использовать myFeature.config чтобы указать, как найти и обработать текст заголовка для каждого элемента навигации:
var myFeature = {
  'config' : {
    'container' : $('#myFeature'),

    'itemNavSelector' : 'h3',
  
    'itemNavProcessor' : function($selection) {
      return 'Preview of ' +
        $selection.eq(0).text();
    }
  },

  'init' : function(config) {
    // то же самое 
  },

  'buildSectionNav' : function($sections) {
    // то же самое 
  },

  'buildItemNav' : function($items) {

    $items.each(function() {
      var $item = $(this);

      var myText = myFeature.config.itemNavProcessor(
        $item.find(myFeature.config.itemNavSelector)
      );

      $('<li/>')
     
       .text(myText)
       .appendTo(myFeature.$item_nav)
       .data('item', $item)
       .click(myFeature.showContentItem);    
   }); 
 },  

 'showSection' : function() {   
  // то же самое 
 },  

 'showContentItem' : function() {   
  // то же самое 
 } 

};


* This source code was highlighted with Source Code Highlighter.

После того как вы добавили объект конфигурации по умолчанию, вы можете переопределить его при вызове myFeature.init ()
$(document).ready(function() {
  myFeature.init({ 'itemNavSelector' : 'h2' });
});


* This source code was highlighted with Source Code Highlighter.

Заключение


Если вы просмотрели примеры кода в этой статье, вы должны иметь общее представление об Object Literal, и как можно использовать этот подход для создания более сложных взаимодействий.

Пример взят отсюда.

Дополнительная литература

Tags:
Hubs:
+25
Comments 47
Comments Comments 47

Articles