NuGet Packages: SquishIt speeds JavaScript and CSS managementPacotes NuGet: SquishIt agiliza o gerenciamento de JavaScript e CSS

Scripts sendo carregados sem tratamento

Scripts sendo carregados sem tratamento

Hoje vou apresentar, para os que ainda não conhecem, o SquishIt. Esse pacote possibilita a automação de diversas tarefas antes tediosas, possivelmente demoradas e que muitas vezes acabavam não sendo feitas sobre a integração de arquivos JavaScript e CSS.

Obs.: ainda não está familiarizado com o uso do NuGet? Acesse nuget.org agora mesmo e você não vai se arrepender!

A grande questão aqui é que a quantidade de dados carregados com o propósito de tornar nossas aplicações web mais dinâmicas e modernas está cada vez maior. No exemplo acima, carrego algumas bibliotecas JavaScript em seu estado bruto para exemplificar a quantidade de requisições feitas e de dados sendo transferidos em uma aplicação web complexa. Note que leva-se, nesse caso, 76ms para baixar 996,3 KB de dados em 8 requisições diferentes. No exemplo, nenhum dos arquivos está otimizado e o problema da latência não está evidente, já que estou usando localhost.

O que o SquishIt nos dá praticamente de graça é a possibilidade de aplicarmos diversas soluções populares, antes trabalhosas, de uma só vez. Eis o que ele faz:

  • Minifica“ arquivos CSS e JavaScript para reduzir o tamanho dos downloads
  • Combina arquivos diferentes a fim de reduzir o número de requisições ao servidor, dessa forma amenizando o impacto do tempo de latência de cada item
  • Gera um código aleatório que elimina o problema de cache do browser quando seus arquivos são alterados
  • Compila seu código LESS em CSS, agilizando o seu trabalho caso esteja usando arquivos LESS
JavaScript carregado com SquishIt

JavaScript carregado com SquishIt

Certamente ainda há algumas outras vantagens, mas por enquanto vou me concentrar somente nessas, que considero as principais. Para ilustrar o que acontece quando carrego aqueles mesmos arquivos usando o SquishIt, veja como o número de requisições caiu de 8 para 1 (13%), a quantidade de dados de 996,3 para 473,6 KB (48%) e o tempo decorrido de 76 para 24ms (32%).

Inclusão de arquivos

Após instalar o pacote, somente é necessário substituir as tags e

TDD com Knockout JS e QUnit

Knockout + QUnitUltimamente tenho usado muito a fantástica biblioteca Knockout, criada por Steve Sanderson, para criar interfaces de usuário dinâmicas com JavaScript. Em suma, é uma ótima forma de se trazer o padrão MVVM (Model View ViewModel) para o ambiente web. Recomendo fortemente os tutoriais interativos no próprio site para quem quiser aprender.

Uma das grandes vantagens que descobri conforme fui usando o Knockout é que permite muito boa separação entre modelo e interface. E mais! Desenvolver interfaces incrivelmente dinâmicas com TDD de forma simples finalmente é uma realidade se utilizado com o QUnit. Meu objetivo aqui não é entrar em detalhes sobre QUnit ou Knockout, mas sim mostrar como os dois funcionam muito bem juntos seguindo as orientações do test-driven development. Para esse objetivo, vou criar uma simples aplicação de carrinho de compras em JavaScript.

O código-fonte do exemplo completo está no repositório do GitHub e eu também criei uma página para demonstar o resultado.

O primeiro passo é criar os modelos de cada entidade. Meu padrão costuma ser colocá-los dentro de /Scripts/model.

// Scripts/model/Carrinho .js
var Carrinho = function (id) {
    var _self = this;

    this.id = id;
    this.total = ko.observable(0);
    this.itens = ko.observableArray([]);
};
// Scripts/model/Produto .js
var Produto = function (id, nome, valor) {
    var _self = this;

    this.id = id;
    this.nome = nome;
    this.valor = valor;
};
// Scripts/model/Item.js
var Item = function (produto, carrinho, quantidade) {
    var _self = this;

    this.produto = produto;
    this.carrinho = carrinho;
    this.quantidade = ko.observable(quantidade);
};

Partindo do pressuposto que o QUnit esteja em importado na aplicação, declaro os meus testes antes de tudo. Ao meu ver, esse processo de levantamento dos casos de testes antes da codificação ajuda a entender mais precisamente o que terei que implementar. Como produto não terá comportamento algum, faço somente dois módulos QUnit, para Carrinho e Item, e os incluo na página de testes.

// qunit/CarrinhoTests.js
(function () {
    module("Carrinho");

    test("Adicionar produto ao carrinho", function () {
        ok(false, "Não implementado");
    });

    test("Não deve adicionar um produto que já está no carrinho", function () {
        ok(false, "Não implementado");
    });

    test("Remover um produto do carrinho", function () {
        ok(false, "Não implementado");
    });

    test("Alterar a quantidade de um item", function() {
        ok(false, "Não implementado");
    });
})();
// qunit/ItemTests.js
(function () {
    module("Item");

    test("Incrementar quantidade de um item", function () {
        ok(false, "Não implementado");
    });

    test("Decrementar quantidade de um item", function () {
        ok(false, "Não implementado");
    });

    test("Remover um item", function() {
        ok(false, "Não implementado");
    });
})();

Agora, quando acessarmos a página de testes podemos esperar algo como o seguinte.

Todos os testes deverão falhar

A primeira alteração é ser feita é criar um mock do jQuery.ajax, que pretendo usar para gravar no banco de dados o item incluído. Isso deve ser feito na definição do módulo, alterando-se a chamada a module().

//  qunit/CarrinhoTests.js
...
    var urlChamada = "";
    var jqAjax = $.ajax;

    module("Carrinho", {
        setup: function () {
            // Armazena a url chamada em uma variável
            $.ajax = function (url, settings) {
                urlChamada = url;
            };
        },
        teardown: function () {
            // Retorna aos valores originais
            urlChamada = "";
            $.ajax = jqAjax;
        }
    });
...

Agora sim eu escolho um teste para começar e escrevo o código para ele. Vou começar por “Adicionar produto ao carrinho”.

//  qunit/CarrinhoTests.js
...
    test("Adicionar um produto ao carrinho", function () {
        // Arrange
        var urlEsperada = "/api/item/incluir";
        var idCarrinho = 10;
        var carrinho = new Carrinho(idCarrinho);
        var produto = new Produto(15, "Treinamento de TDD", 150);

        // Act
        carrinho.addProduto(produto);

        // Assert
        equal(urlChamada, urlEsperada, "Deve ter chamado o serviço para incluir o item");
        equal(carrinho.itens.length, 1, "Deve haver um item no carrinho");
        equal(carrinho.itens()[0].produto, produto, "O item deve ter o produto correto");
        equal(carrinho.itens()[0].quantidade, 1, "A quantidade do item deve ser 1");
        equal(carrinho.total(), produto.valor, "O valor total do carrinho deve estar correto");
    });
...

Nesse momento, eu também crio em Carrinho o método addProduto, deixando ele em branco. Assim que abro esse teste na página do QUnit, vejo que ele está falhando, exatamente como esperarias por não ter ainda implementado o comportamento de addProduto.

Teste com todas as assertivas falhando

Após implementado, o método ficou assim:

// Scripts/model/Carrinho.js
...
    this.addProduto = function (produto) {
        var item = new Item(produto, _self, 1);

        $.ajax("/api/item/incluir", { data: "..." });

        _self.total(_self.total() + produto.valor);
        _self.itens.push(item);
    };
...

Para manter a simplicidade do exemplo, essa implementação não está completa. O adequado seria enviar os dados necessários e fazer um bom tratamento da resposta à chamada do serviço. Mas só com isso já podemos ver que as assertivas escritas anteriormente estão resultando em sucesso. Em um cenário real, eu poderia (e deveria) também escrever assertivas mais detalhadas.

O primeiro teste agora é bem sucedido

Dessa forma, continuo implementando o resto das funcionalidades até que meus testes sejam satisfatórios e todos estejam resultando em sucesso. Após diversos ajustes, o resultado da tela do QUnit é esse:

Testes completos

Ok, então os modelos já estão implementados. Mas afinal, isso tudo é sobre interfaces dinâmicas e eu ainda não mostrei nada de HTML. O incrível do Knckout é que, tendo esses modelos prontos, é só um pequeno passo para fazer uma interface customizada do jeito que você quiser. Apresento a seguir o código do View Model. Ou seja, aquele que faz a ligação dos nossos modelos com a View no padrão MVVM.

// Scripts/app.js
$(document).ready(function () {
    var viewModel = (function () {
        var _self = this;

        this.carrinho = new Carrinho(1);

        this.produtos = ko.observableArray([
            new Produto(15, "Treinamento de TDD", 150),
            new Produto(97, "Cartas de Planning Poker", 20),
            new Produto(52, "Guia do Scrum", 49.5)
        ]);

        this.produtoSelecionado = ko.observable();
        this.produtoSelecionado.subscribe(function (valor) {
            if (valor == undefined) return;

            for (var i in _self.produtos()) {
                if (_self.produtos()[i].id == valor) {
                    _self.carrinho.addProduto(_self.produtos()[i]);
                    _self.produtoSelecionado(undefined);
                    break;
                }
            }

        }, _self);
    })();

    ko.applyBindings(viewModel);
});

Por fim, segue a implementação da view, em HTML. Eu procurei deixar esse código o menor possível, e por isso tive que remover todos os estilos. Mas deixei tudo no repositório com estilos adequados, usando o Twitter Bootstrap como framework CSS.

<!-- index.html -->
<html>
  <head>
    <title>Ko Shopping Cart</title>

    <script type="text/javascript" src="Scripts/jquery.min.js"></script>
    <script type="text/javascript" src="Scripts/knockout-1.3.0beta.js"></script>

    <script type="text/javascript" src="Scripts/model/Carrinho.js"></script>
    <script type="text/javascript" src="Scripts/model/Item.js"></script>
    <script type="text/javascript" src="Scripts/model/Produto.js"></script>

    <script type="text/javascript" src="Scripts/app.js"></script>
  </head>

  <body>
        <select data-bind="options: produtos, optionsText: 'nome', optionsValue: 'id',
            optionsCaption: 'Escolha um...', value: produtoSelecionado">
        </select>
        <br/><br/>
        <table data-bind="with: carrinho">
            <thead><tr><th>Item</th><th>Preço</th><th>Qtde.</th><th></th></tr></thead>
            <tbody data-bind="foreach: itens">
                <tr>
                    <td data-bind="text: produto.nome"></td>
                    <td data-bind="text: produto.valor"></td>
                    <td>
                        <a href="javascript:;">
                            <img src="images/minus.png" data-bind="click: decrementar"/></a>
                        <input type="text" data-bind="value: quantidade"/>
                        <a href="javascript:;" data-bind="click: incrementar">
                            <img src="images/plus.png"/></a>
                    </td>
                    <td>
                        <a href="javascript:;" data-bind="click: remover">
                            <img src="images/cross.png"/></a>
                    </td>
                </tr>
            </tbody>
            <tfoot><tr><td></td><td></td>
                    <td>Total: <span data-bind="text: total"></span></td>
                    <td></td></tr></tfoot>
        </table>
  </body>
</html>

Por enquanto é isso. Não deixe de pegar o código completo no repositório GitHub. A página de demonstração está aqui. Caso tenha alguma dúvida ou crítica, por favor comente abaixo! Todas as sugestões são muito bem vindas.