[Перевод] Улучшаем код JavaScript на примере StarWars API
Привет, меня зовут Рэймонд, и я пишу плохой код. Ну, не совсем плохой, но я точно не следую всем «лучшим практикам». Однако давайте я расскажу вам, как один проект помог мне начать писать код, которым я могу гордиться.
Как-то в выходной я решил отказаться от использования компьютера. Но ничего не вышло. Я наткнулся на 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)? Объект? Исключение? Пока не знаю, но позже решу. Вот результат выполнения тестов. Использование линтеров 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». Если я сломаю что-нибудь, например добавлю код, который сломает JSHint, Grunt сообщит об этом и остановится. Что в итоге?
В итоге функционально библиотека не поменялась, но зато: — у меня есть модульные тесты для проверки работы. Добавляя новые функции, я буду уверен, что не сломаю старые— я использовал линтер для проверки кода соответствию рекомендациям. Проверка кода внешним наблюдателем — это всегда плюс— добавил минификацию библиотеки. Особо не сэкономил, но это задел на будущее— автоматизировал всю эту кухню. Теперь всё это можно делать одной небольшой командой. Жизнь прекрасна, а я стал супер-ниндзей кода. Теперь мой проект стал лучше и он мне нравится. Окончательную версию можно скачать здесь.