Improving JavaScript code using the StarWars API as an example

Original author: Raymond Camden
  • Transfer
image

Hi, my name is Raymond and I am writing bad code. Well, not really bad, but I definitely don’t follow all the “best practices”. However, let me tell you how one project helped me start writing code that I can be proud of.

Somehow on the weekend, I decided to abandon the use of a computer. But nothing came of it. I came across the Star Wars API. This simple interface is based on REST, and with it you can request information about characters, films, spaceships and other things from the SW universe. No search, but free service.

And I quickly set up a JS library to work with the API. In the simplest case, you can request all resources of the same type:

// получить все корабли
swapiModule.getStarships(function(data) {
    console.log("Результат getStarships", data);
});


Or get one item:

// получить один корабль, если 2 – это допустимый номер
swapiModule.getStarship(2,function(data) {
    console.log("Результат getStarship/2", data);
});


The code itself is in one file, I made test.html for it and uploaded it to GitHub: github.com/cfjedimaster/SWAPI-Wrapper/tree/v1.0 . (This is the initial draft. The final version is here ).

But then doubts began to prevail over me. Can I improve something in the code? Do I need to write unit tests? Add a smaller version?

And I began to gradually make a list of what can be done to improve projects.

- when writing code, some of its parts were repeated, and this required optimization. I ignored these cases and focused on making the code work, so as not to engage in premature optimization. Now I want to go back and do optimization
- Obviously, you need to do unit tests. Although the system works with a remote interface and it is rather difficult to do tests in this case, even a test assuming that the remote service works 100% is better than without tests at all. And then, by writing tests, I can make sure that my subsequent code changes will not break the program
- I am a big fan of JSHint , and would like to run it in my code
- I would like to make a smaller version of the library - I think some kind of utility would be suitable command line
- finally, I’m sure that I can perform unit tests, JSHint checks and minifications automatically through tools like Grunt or Gulp.

And as a result, I will get a project with which I will feel more confident, a project that will look more like a Jedi than JJ Binks.

Adding Unit Tests

They are easiest to imagine as a set of tests that make sure that different aspects of the code work as they should. Imagine a library with two functions: getPeople and getPerson. You can do two tests, one for each. Now imagine getPeople allows you to search. It is necessary to do the third test for the search. If getPeople also allows you to divide the results into pages and set the page number from which you want to return the results - you need more tests for this too. Well, you get the point. The more tests, the more you can be sure of the code.

My library has 3 types of calls. The first is getResources. Returns a list of other API entry points. Then there is the opportunity to get one position and all positions. That is, for planets there are getPlanet and getPlanets. But these calls return paginated data. Therefore, the API also supports a call of the form getPlanets (n), where n is the page number. So, you need to test four things:

- call getResources
- call getSingular for each resource
- call getPlural for each resource
- call getPlural for a given page

We have one common method and three for each resource, so there should be

1 + tests (3 * number of

resources ) There are 6 types of resources, total - 19 tests. Not bad. My favorite Moment.js library43,399 tests.

I decided to use the Jasmine framework for testing , as I like him and I know him best. One of the nice things is the availability of sample tests that you can change to fit your needs and get started, as well as a file for running tests. This is an HTML file that includes your library and your tests. When you open it, he runs them all and displays the result. I started with the getResources test. Even if you are not familiar with Jasmine, you can figure out what happens:

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();
    });
});


The getResources method returns an object with a set of keys representing each resource supported by the API. Therefore, I simply use toBeDefined as a way of saying "such a key should be." done () is needed for asynchronous call processing. Now consider other types. First, get one object from the resource.

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();
    });
});


There is a small problem - I assume the presence of a character with identifier 2, and also that the keys describing him will not change. But this is not scary - in which case the test can be easily corrected. Do not get involved in premature test optimization.

Now the return of the multitude.

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();
    });
});


Second page.

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();
    });
});


Actually that's all. Now you only need to repeat these three calls for the other five types of resources. When writing tests, I already saw flaws in the code. For example, getFilms returns only one page. Other than that, I did not handle error handling. What should I return to getFilms (2) request? An object? An exception? I don’t know yet, but I’ll decide later.

Here is the test result.

image

Using JSHint Linter


The next step is to use the linter. This is a tool for evaluating code quality. It can highlight errors, indicate the possibility of speed optimization, or highlight a code that does not comply with the recommended rules.

Initially, JS used JSLint, but I am using an alternative to JSHint. He is more relaxed, and I am also quite relaxed, so he suits me better.

There are many ways to use JSHint, including in your favorite editor. Personally, I use Brackets , for which there is an extension that supports JSHint. But for this project I will use the command line utility. If you have npm installed, you can just say

npm install -g jshint 


After that, you can test the code. Example:

jshint swapi.js


Reduce the library


Although the library is small (128 lines), but it obviously will not decrease over time. And anyway, if it’s not worth any effort, why not do it. During minification, extra spaces are removed, variable names are shortened and the file is compressed. I chose UglifyJS for this purpose :

uglifyjs swapi.js -c -m -o swapi.min.js


It's funny that this tool noticed the unused getResource function, which I left in the code:

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


Total, a file of 2750 bytes began to occupy 1397 - about 2 times less. 2.7 Kb - not a lot, but over time, libraries only increase.

Automate it!


As a very lazy person, I want to automate this whole process. Ideally, this should be:

- run unit tests. if successful
, get rid of JSHint. if successful
, create a mini version of the library.

For this, I'll take Grunt . This is not the only choice, there is still Gulp , but I have not used it. Grunt allows you to run a set of tasks, and you can make the chain break if one of them fails. For those who have not used Grunt, I suggest reading the introductory text .

By adding package.json to load the Grunt plugins (Jasmine, JSHint, and Uglify), I built the following 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']);
};


Simply put, run all the tests (Jasmine), run JSHint, and then uglify. At the command prompt, just type “grunt”.

image

If I break something, for example add code that breaks JSHint, Grunt will report it and stop.

image

What is the result?


As a result, the library functionally has not changed, but:

- I have unit tests to verify operation. By adding new functions, I will be sure that I will not break the old ones
- I used the linter to check the code according to the recommendations. Checking the code by an external observer is always a plus
- added a library minification. He didn’t save much, but it touched the future
- he automated this whole kitchen. Now all this can be done with one small team. Life is beautiful, and I became a super-ninja code.

Now my project has become better and I like it. The final version can be downloaded here .

Also popular now: