[Перевод] Улучшаем код JavaScript на примере StarWars API

imageПривет, меня зовут Рэймонд, и я пишу плохой код. Ну, не совсем плохой, но я точно не следую всем «лучшим практикам». Однако давайте я расскажу вам, как один проект помог мне начать писать код, которым я могу гордиться.

Как-то в выходной я решил отказаться от использования компьютера. Но ничего не вышло. Я наткнулся на Star Wars API. Этот простой интерфейс основан на REST, и с его помощью можно запрашивать информацию о персонажах, фильмах, космических кораблях и других вещах из вселенной SW. Поиска нет, но сервис свободный.И я быстренько налабал библиотеку на JS для работы с API. В простейшем случае можно запросить все ресурсы одного типа:

// получить все корабли swapiModule.getStarships (function (data) { console.log («Результат getStarships», data); }); Или получить один предмет:

// получить один корабль, если 2 — это допустимый номер swapiModule.getStarship (2, function (data) { console.log («Результат getStarship/2», data); }); Сам код находится в одном файле, я сделал к нему test.html и залил на GitHub: github.com/cfjedimaster/SWAPI-Wrapper/tree/v1.0. (Это изначальный проект. Окончательная версия лежит тут).

Но затем меня стали одолевать сомнения. Не могу ли я что-нибудь улучшить в коде? Не нужно ли написать модульные тесты? Добавить уменьшенную версию?

И я начал постепенно составлять список того, что можно сделать для улучшения проектов.

— при написании кода некоторые его части повторялись, и это требовало оптимизации. Я проигнорировал эти случаи и сосредоточился на том, чтобы код заработал, чтобы не заниматься преждевременной оптимизацией. Теперь мне хочется вернуться назад и заняться оптимизацией— очевидно, нужно сделать модульные тесты. Хотя система работает с удалённым интерфейсом и тесты сделать в этом случае довольно трудно, но даже тест, предполагающий, что удалённый сервис работает на 100% — лучше, чем вообще без тестов. И потом, написав тесты, я могу удостовериться, что мои последующие изменения кода не сломают программу— я большой фанат JSHint, и хотел бы прогнать его по моему коду— хотелось бы сделать уменьшенную версию библиотеки — думаю, для этого подошла бы какая-нибудь утилита командной строки— наконец, уверен, что смогу выполнять модульные тесты, проверку JSHint и минификацию автоматически через инструменты вроде Grunt или Gulp.

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

Добавляем модульные тесты

Их проще всего представить себе как набор тестов, которые удостоверяются, что разные аспекты кода работают как надо. Представим библиотеку с двумя функциями: getPeople и getPerson. Можно сделать два теста, по одному для каждой. Теперь представим, что getPeople позволяет выполнять поиск. Надо сделать третий тест для поиска. Если getPeople также позволяет делить результаты на страницы и задавать номер страницы, с которой надо возвращать результаты — вам нужны ещё тесты и для этого. Ну вы поняли. Чем больше тестов, тем больше вы можете быть уверены в коде.

У моей библиотеки 3 типа вызовов. Первый — getResources. Возвращает список других точек входа API. Затем есть возможность получить одну позицию и все позиции. То есть, для планет есть getPlanet and getPlanets. Но вызовы эти возвращают данные, разделённые на страницы. Поэтому API поддерживает также вызов вида getPlanets (n), где n — номер страницы. Значит, надо тестировать четыре вещи:

— вызов getResources— вызов getSingular для каждого ресурса— вызов getPlural для каждого ресурса— вызов getPlural для заданной страницы

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

1 + (3 * количество_ресурсов)

Ресурсов 6 типов, итого — 19 тестов. Неплохо. У моей любимой библиотеки Moment.js 43,399 тестов.

Я решил использовать для тестов фреймворк Jasmine, т.к. он мне нравится и я знаком с ним лучше всего. Одна из приятных вещей — наличие примеров тестов, которые можно изменить под свои нужды и начать работу, а также файл для запуска тестов. Это HTML-файл, включающий вашу библиотеку и ваши тесты. При открытии он их все прогоняет и выводит результат. Я начал с теста getResources. Даже если вы не знакомы с Jasmine, вы сможете разобраться, что происходит:

it («должен уметь запросить ресурсы», function (done) { swapiModule.getResources (function (data) { expect (data.films).toBeDefined (); expect (data.people).toBeDefined (); expect (data.planets).toBeDefined (); expect (data.species).toBeDefined (); expect (data.starships).toBeDefined (); expect (data.vehicles).toBeDefined (); done (); }); }); Метод getResources возвращает объект с набором ключей, представляющих каждый ресурс, поддерживаемый API. Поэтому я просто использую toBeDefined как способ сказать «такой ключ должен быть». done () нужен для асинхронной обработки вызовов. Теперь рассмотрим другие типы. Сначала, получить один объект с ресурса.

it («должен уметь получить Person», function (done) {

swapiModule.getPerson (2, function (person) { var keys = [«birth_year», «created», «edited», «eye_color», «films», «gender», «hair_color», «height», «homeworld», «mass», «name», «skin_color», «species», «starships», «url», «vehicles»]; for (var i=0, len=keys.length; i

done (); });

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

Теперь возврат множества.

it («должен уметь получать People», function (done) {

swapiModule.getPeople (function (people) { var keys = [«count», «next», «previous», «results»]; for (var i=0, len=keys.length; i

done (); });

}); Вторая страница.

it («должен уметь получить вторую страницу People», function (done) {

swapiModule.getPeople (2, function (people) { var keys = [«count», «next», «previous», «results»]; for (var i=0, len=keys.length; i

}); Собственно и всё. Теперь надо только повторить три эти вызова для остальных пяти типов ресурсов. При написании тестов я уже увидел недостатки в коде. Например, getFilms возвращает только одну страницу. Кроме этого, я не занимался обработкой ошибок. Что мне возвращать на запрос getFilms (2)? Объект? Исключение? Пока не знаю, но позже решу.

Вот результат выполнения тестов.

image

Использование линтеров JSHint Следующий шаг — использование линтера. Это инструмент для оценки качества кода. Он может выделять ошибки, указывать на возможность оптимизации по скорости или выделять код, не соответствующий рекомендуемым правилам.Изначально для JS использовался JSLint, но я использую альтернативу JSHint. Он более расслабленный, а я тоже довольно расслабленный, так что он мне подходит больше.

Есть много способов использования JSHint, в том числе — в вашем любимом редакторе. Лично я использую Brackets, для которого есть расширение, поддерживающее JSHint. Но для этого проекта я буду использовать утилиту командной строки. Если у вас установлен npm, вы можете просто сказать

npm install -g jshint После этого можно тестировать код. Пример:

jshint swapi.js Уменьшаем библиотеку Хотя библиотека небольшая (128 строк), но она явно со временем уменьшаться не будет. И вообще, если это не стоит никаких усилий, почему бы это не сделать. При минификации удаляются лишние пробелы, укорачиваются имена переменных и файл ужимается. Я выбрал для этой цели UglifyJS: uglifyjs swapi.js -c -m -o swapi.min.js Забавно, что именно этот инструмент заметил неиспользуемую функцию getResource, которую я оставил в коде:

// одинаковая для всех вызовов. todo — оптимизировать function getResource (u, cb) {

} Итого, файл из 2750 байт стал занимать 1397 — примерно в 2 раза меньше. 2.7 Кб — не много, но со временем библиотеки только увеличиваются.

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

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

Добавив загрузку package.json для загрузки плагинов Grunt plugins (Jasmine, JSHint и Uglify), я построил следующий Gruntfile.js:

module.exports = function (grunt) {

// настройки проекта grunt.initConfig ({ pkg: grunt.file.readJSON ('package.json'), uglify: { build: { src: 'lib/swapi.js', dest: 'lib/swapi.min.js' } }, jshint: { all: ['lib/swapi.js'] }, jasmine: { all: { src: «lib/swapi.js», options: { specs: «tests/spec/swapiSpec.js», '--web-security': false } } } });

grunt.loadNpmTasks ('grunt-contrib-uglify'); grunt.loadNpmTasks ('grunt-contrib-jshint'); grunt.loadNpmTasks ('grunt-contrib-jasmine');

grunt.registerTask ('default', ['jasmine','jshint','uglify']);

}; Проще говоря — запустить все тесты (Jasmine), запустить JSHint и затем uglify. В командной строке нужно просто набрать «grunt».

image

Если я сломаю что-нибудь, например добавлю код, который сломает JSHint, Grunt сообщит об этом и остановится.

image

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

Теперь мой проект стал лучше и он мне нравится. Окончательную версию можно скачать здесь.

© Habrahabr.ru