Статьи

Распутывающий код спагетти: как написать поддерживаемый JavaScript

Эта статья была рецензирована Томом Греко , Дэном Принсом и Яфи Берхану . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

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

Мне пришлось взять на себя и очистить много проектов. Там было не много я начал с нуля. На самом деле, я сейчас делаю именно это. Я многому научился в отношении JavaScript, организовывая кодовую базу и, самое главное, не злюсь на предыдущего разработчика. В этой статье я хочу показать вам свои шаги и рассказать о моем опыте.

Проанализируйте проект

Самый первый шаг — получить представление о том, что происходит. Если это веб-сайт, выберите все функции: открывайте модалы, отправляйте формы и так далее. При этом откройте Инструменты разработчика, чтобы увидеть, появляются ли какие-либо ошибки или что-то регистрируется. Если это проект Node.js, откройте интерфейс командной строки и пройдите через API. В лучшем случае проект имеет точку входа (например, main.jsindex.jsapp.js

Узнайте, какие инструменты используются. JQuery ? Реагировать ? Экспресс ? Составьте список всего, что важно знать. Допустим, проект написан на Angular 2, и вы с этим не работали, перейдите прямо к документации и получите общее представление. Поиск лучших практик.

Понять проект на более высоком уровне

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

Создать базовый уровень

Это все о создании последовательности . Теперь, когда у вас есть вся информация о наборе инструментов проектов, вы знаете структуру и то, как логика связана, пришло время создать базовую линию. Я рекомендую добавить файл .editorconfig для обеспечения согласованности руководств по стилю кодирования между различными редакторами, IDE и разработчиками.

Когерентный отступ

Знаменитый вопрос (это скорее война ) о том, следует ли использовать пробелы или табуляции, не имеет значения. Кодовая база написана в пробелах? Продолжайте с пробелами. С вкладками? Используй их. Только когда кодовая база имеет смешанные отступы, необходимо решить, какой использовать. Мнения хорошие, но хороший проект гарантирует, что все разработчики могут работать без хлопот.

Почему это даже важно? У каждого свой способ использования редактора или IDE. Например, я большой поклонник свертывания кода . Без этой функции я буквально теряюсь в файле. Когда отступ не согласован, эта функция не работает. Поэтому каждый раз, когда я открываю файл, мне нужно исправить отступ, прежде чем я смогу начать работать. Это огромная трата времени.

 // While this is valid JavaScript, the block can't
// be properly folded due to its mixed indentation.
 function foo (data) {
  let property = String(data);

if (property === 'bar') {
   property = doSomething(property);
  }
  //... more logic.
 }

// Correct indentation makes the code block foldable,
// enabling a better experience and clean codebase.
function foo (data) {
 let property = String(data);

 if (property === 'bar') {
  property = doSomething(property);
 }
 //... more logic.
}

Именование

Убедитесь, что соглашение об именах, используемое в проекте, соблюдается. CamelCase обычно используется в коде JavaScript, но я часто видел смешанные соглашения. Например, проекты jQuery часто имеют смешанные имена переменных объекта jQuery и других переменных.

 // Inconsistent naming makes it harder
// to scan and understand the code. It can also
// lead to false expectations.
const $element = $('.element');

function _privateMethod () {
  const self = $(this);
  const _internalElement = $('.internal-element');
  let $data = element.data('foo');
  //... more logic.
}

// This is much easier and faster to understand.
const $element = $('.element');

function _privateMethod () {
  const $this = $(this);
  const $internalElement = $('.internal-element');
  let elementData = $element.data('foo');
  //... more logic.
}

Linting все

В то время как предыдущие шаги были более косметическими и в основном помогали быстрее сканировать код, здесь мы представляем и обеспечиваем общие рекомендации, а также качество кода. ESLint , JSLint и JSHint — самые популярные JavaScript-линтеры в наши дни. Лично я много работал с JSHint, но ESLint стал моим любимым, в основном из-за его пользовательских правил и ранней поддержки ES2015.

Когда вы начнете работать, если всплывет много ошибок, исправьте их! Не продолжайте ни с чем, пока ваш линтер не станет счастливым!

Обновление зависимостей

Обновление зависимостей должно быть сделано осторожно. Легко вводить больше ошибок, если не обращать внимания на изменения, через которые прошли ваши зависимости. Некоторые проекты могут работать с фиксированными версиями (например, v1.12.5v1.12.x Если вам нужно быстрое обновление, номер версии создается следующим образом: MAJOR.MINOR.PATCH Если вы не знакомы с тем, как работает семантическое версионирование , я рекомендую прочитать эту статью Тима Оксли.

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

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

Изображение npm устарело

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

Давайте грязные руки

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

Установить модульные тесты

Наличие модульных тестов гарантирует, что вы понимаете, как код предназначен для работы, и вы случайно ничего не нарушаете. Модульное тестирование JavaScript стоит своих статей, поэтому я не буду здесь вдаваться в подробности. Широко используются кармы , карма , жасмин , мокко или ава . Если вы также хотите протестировать свой пользовательский интерфейс, Nightwatch.js и DalekJS являются рекомендованными инструментами автоматизации браузера.

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

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

Ребекка Мерфи написала отличную статью о написании модульных тестов для существующего JavaScript .

Архитектура

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

Прежде всего, вы должны выяснить, какие шаблоны проектирования уже используются в вашем проекте. Читайте о шаблоне и убедитесь, что он соответствует. Один из ключей к масштабируемости — придерживаться шаблона, а не смешивать методологии. Конечно, в вашем проекте могут быть разные шаблоны проектирования для разных целей (например, использование шаблона Singleton для структур данных или коротких вспомогательных функций с пространством имен и шаблона Observer для ваших модулей), но никогда не следует писать один модуль с одним шаблоном, а другой один с другим рисунком.

Если в вашем проекте действительно нет какой-либо архитектуры (возможно, все в одном огромном app.js Не делай все сразу, но по частям. Опять же, нет общего способа сделать что-то, и каждый проект настройки отличается. Структура папок варьируется между проектами, в зависимости от размера и сложности. Обычно — на самом базовом уровне — структура разделяется на сторонние библиотеки, модули, данные и точку входа (например, index.jsmain.js

Это приводит меня к модульности .

Модульная все?

Модуляризация — далеко не ответ на вопрос о масштабируемости JavaScript. Он добавляет еще один уровень API, с которым разработчики должны ознакомиться. Это может стоить хлопот, хотя. Принцип разделения всей вашей функциональности на крошечные модули. Делая это, легче решать проблемы в своем коде и работать в команде на одной кодовой базе. Каждый модуль должен иметь ровно одну цель и задачу. Модуль не знает о внешней логике вашего приложения и может быть повторно использован в разных местах и ​​ситуациях.

Как вы разделяете большую функцию с большим количеством тесно связанных логики? Давайте сделаем это вместе.

 // This example uses the Fetch API to request an API. Let's assume
// that it returns a JSON file with some basic content. We then create a
// new element, count all characters from some fictional content
// and insert it somewhere in your UI.
fetch('https://api.somewebsite.io/post/61454e0126ebb8a2e85d', { method: 'GET' })
  .then(response => {
    if (response.status === 200) {
      return response.json();
    }
  })
  .then(json => {
    if (json) {
      Object.keys(json).forEach(key => {
        const item = json[key];
        const count = item.content.trim().replace(/\s+/gi, '').length;
        const el = `
          <div class="foo-${item.className}">
            <p>Total characters: ${count}</p>
          </div>
        `;
        const wrapper = document.querySelector('.info-element');

        wrapper.innerHTML = el;
      });
    }
  })
  .catch(error => console.error(error));

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

Давайте выделим различные обязанности:

 // In the previous example we had a function that counted
// the characters of a string. Let's turn that into a module.
function countCharacters (text) {
  const removeWhitespace = /\s+/gi;
  return text.trim().replace(removeWhitespace, '').length;
}

// The part where we had a string with some markup in it,
// is also a proper module now. We use the DOM API to create
// the HTML, instead of inserting it with a string.
function createWrapperElement (cssClass, content) {
  const className = cssClass || 'default';
  const wrapperElement = document.createElement('div');
  const textElement = document.createElement('p');
  const textNode = document.createTextNode(`Total characters: ${content}`);

  wrapperElement.classList.add(className);
  textElement.appendChild(textNode);
  wrapperElement.appendChild(textElement);

  return wrapperElement;
}

// The anonymous function from the .forEach() method,
// should also be its own module.
function appendCharacterCount (config) {
  const wordCount = countCharacters(config.content);
  const wrapperElement = createWrapperElement(config.className, wordCount);
  const infoElement = document.querySelector('.info-element');

  infoElement.appendChild(wrapperElement);
}

Хорошо, теперь у нас есть три новых модуля. Давайте посмотрим на рефакторированный вызов fetch

 fetch('https://api.somewebsite.io/post/61454e0126ebb8a2e85d', { method: 'GET' })
  .then(response => {
    if (response.status === 200) {
      return response.json();
    }
  })
  .then(json => {
    if (json) {
      Object.keys(json).forEach(key => appendCharacterCount(json[key]))
    }
  })
  .catch(error => console.error(error));

Мы могли бы также взять логику из методов .then()

Если !modularization

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

Документируйте свой код

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

JSDoc — это генератор документации API для JavaScript. Обычно он доступен как плагин для всех известных редакторов и IDE. Давайте рассмотрим пример:

 function properties (name, obj = {}) {
  if (!name) return;
  const arr = [];

  Object.keys(obj).forEach(key => {
    if (arr.indexOf(obj[key][name]) <= -1) {
      arr.push(obj[key][name]);
    }
  });

  return arr;
}

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

 /**
 * Iterates over an object, pushes all properties matching 'name' into
 * a new array, but only once per occurance.
 * @param  {String}  propertyName - Name of the property you want
 * @param  {Object}  obj          - The object you want to iterate over
 * @return {Array}
 */
function getArrayOfProperties (propertyName, obj = {}) {
  if (!propertyName) return;
  const properties = [];
  Object.keys(obj).forEach(child => {
    if (properties.indexOf(obj[child][propertyName]) <= -1) {
      properties.push(obj[child][propertyName]);
    }
  });
  return properties;
}

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

Организовать рабочий процесс коммитов

Рефакторинг — огромная миссия сама по себе. Чтобы всегда иметь возможность откатить ваши изменения (в случае, если вы что-то сломаете и заметите только позже), я рекомендую фиксировать каждое сделанное вами обновление. Переписал метод? git commitsvn commit Переименовали пространство имен, папку или несколько изображений? git commit Вы поняли идею. Это может быть утомительно для некоторых людей, но это действительно помогает вам правильно вымыться и собраться.

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

В случае, если вам нужно кратко обновить, как все это работает, есть интересное руководство от GitHub по их рабочему процессу контроля версий.

Как не сойти с ума

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

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

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

Завершение

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

  1. Проанализируйте проект
  • Отложите свою шапку разработчика на мгновение и станьте пользователем, чтобы увидеть, о чем все это.
  • Пройдите через кодовую базу и составьте список используемых инструментов.
  • Ознакомьтесь с документацией и рекомендациями по использованию инструментов.
  • Пройдите юнит-тесты, чтобы почувствовать проект на более высоком уровне.
  1. Создать базовый уровень
  • .editorconfig чтобы обеспечить согласованность руководств по стилю кодирования между различными IDE.
  • Сделайте отступы последовательными; табуляции или пробелы, не имеет значения.
  • Обеспечить соблюдение правил именования.
  • Если его еще нет, добавьте линтер , такой как ESLint , JSLint или JSHint .
  • Обновите зависимости, но делайте это с умом и следите за тем, что именно было обновлено.
  1. Убираться
  • Создайте модульные тесты и автоматизируйте браузер с помощью таких инструментов, как Karma , Jasmine или Nightwatch.js .
  • Убедитесь, что архитектура и шаблон дизайна соответствуют друг другу.
  • Не смешивайте шаблоны дизайна , придерживайтесь уже существующих.
  • Решите, хотите ли вы разбить свою кодовую базу на модули. Каждый должен иметь только одну цель и не знать об остальной логике вашей кодовой базы.
  • Если вы не хотите этого делать, сосредоточьтесь больше на тестируемом коде и разбейте его на более простые блоки.
  • Документируйте свои функции и код в сбалансированном виде с правильно названными функциями.
  • Используйте JSDoc для создания документации для вашего JavaScript.
  • Совершайте регулярно и после важных изменений. Если что-то сломается, проще вернуться.
  1. Не сходи с ума
  • Не сердитесь на предыдущего разработчика; негативность приведет только к ненужному рефакторингу и потере времени.
  • Были причины, по которым код пишется так, как он есть. Имейте в виду, что мы все были там.

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