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

в 23:18, , рубрики: grunt, javascript, jshint, star wars api, uglify, метки:

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<len; i++) {
            expect(person[keys[i]]).toBeDefined();
        }

        done();
    });

});

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

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

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

    swapiModule.getPeople(function(people) {
        var keys = ["count", "next", "previous", "results"];
        for(var i=0, len=keys.length; i<len; i++) {
            expect(people[keys[i]]).toBeDefined();
        }

        done();
    });

});

Вторая страница.

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

    swapiModule.getPeople(2, function(people) {
        var keys = ["count", "next", "previous", "results"];
        for(var i=0, len=keys.length; i<len; i++) {
            expect(people[keys[i]]).toBeDefined();
        }
        expect(people.previous).toMatch("page=1");
        done();
    });

});

Собственно и всё. Теперь надо только повторить три эти вызова для остальных пяти типов ресурсов. При написании тестов я уже увидел недостатки в коде. Например, 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

Что в итоге?

В итоге функционально библиотека не поменялась, но зато:

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

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

Автор: SLY_G

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js