Entendendo o Ext.Ajax.request (Success X Failure)

Olá pessoal,

Hoje vou falar um pouco sobre o Ext.Ajax.request, especialmente sobre as funções de callback de Success e Failure. Esse é um dos erros que mais vejo em códigos alheios por aí.

E por que especialmente sobre o Ext.Ajax.request? Mesmo quando a gente trabalha com Store ou com Submit de Forms no ExtJS, por trás o Ext.Ajax é chamado. Então entendendo como o callback funciona, podemos aplicar a mesma lógica para a Store e o Submit de um Form.

[caption id="attachment_6972" align="aligncenter" width="360"]success-and-failure-sign Fonte da Img: http://cdn.worldcupblog.org/www.worldcupblog.org/files/2010/07/success-and-failure-sign.jpg[/caption]

Primeiro vou mostrar para vocês como esses callbacks são usados e de forma errada. Depois vou mostrar a forma como devem ser usados. E por último, uma maneira de deixar o seu código ainda mais limpo e bonito, reusando o máximo de código que a gente pode (afinal, não basta funcionar, código limpo e bonito também agrega valor né?).

Para exemplificar, suponha o cenário de uma página de Login. Para esse exemplo, criei 3 páginas de Login idênticas, apenas diferindo o xtype e um Controller. Tenho também um php que vai no banco buscar o usuário e senha entrado pelo usuário. Para nosso exemplo, vamos criar uma tabela usuário (com colunas usuário e senha) e vamos inserir apenas 1 linha nessa tabela: usuário "admin" e senha "admin". Segue o script:

CREATE SCHEMA `blog` ;

CREATE  TABLE `blog`.`user` (

  `username` VARCHAR(45) NOT NULL ,

  `password` VARCHAR(45) NOT NULL ,

  PRIMARY KEY (`username`) ,

  UNIQUE INDEX `username_UNIQUE` (`username` ASC) );

 INSERT INTO `blog`.`user` (`username`, `password`) VALUES ('admin', 'admin');

Claro que isso é apenas um exemplo viu gente!

No PHP, criei um arquivo login.php que apenas vai ao banco de dados fazer o SELECT com o usuário e senha passado no Form de Login. Se usuário e senha existem na base, retorna um success = true e msg = "Usuário Autenticado!". Caso contrário: success = false e msg = "Usuário ou Senha Incorretos.". Lembrando que o retorno para ExtJS é sempre no formato JSON.

Segue o código php - bem simples que é pra mesmo quem não programa em PHP (como eu! rs) conseguir entender:

<?php

require("db/db.php");

// username and password sent from form
$userName = $_POST['user'];
$pass = $_POST['password'];

$userName = $mysqli->real_escape_string($userName);
$pass = $mysqli->real_escape_string($pass);
$sql = "SELECT * FROM USER WHERE userName='$userName' and password='$pass'";

$success = false;
$msg = ' ';

if ($resultdb = $mysqli->query($sql)) {

        // determine number of rows result set
        $count = $resultdb->num_rows;

        // If result matched $userName and $pass, table row must be 1 row
        if($count==1){

                $success = true;
                $msg = utf8_encode('Usuário Autenticado!');

        } else {

                $success = false;
                $msg = utf8_encode('Usuário ou Senha Incorretos.');
        }

        /* close result set */
        $resultdb->close();
}

/* close connection */
$mysqli->close();

//JSON encoding
echo json_encode(array(
        "success" => $success,
        "msg" => $msg
));
?>

No final do post coloco um link onde é possível fazer download do código completo ok?

Entendendo o Problema

Vamos agora ao Form de Login:

Ext.define('MyApp.view.LoginIncorrect', {
    extend: 'Ext.form.Panel',
    alias: 'widget.loginincorrect',

    bodyPadding: 10,
    title: 'Login Incorrect',

    initComponent: function() {
        var me = this;

        Ext.applyIf(me, {
            items: [
                {
                    xtype: 'textfield',
                    anchor: '100%',
                    itemId: 'user',
                    fieldLabel: 'User',
                    allowBlank: false
                },
                {
                    xtype: 'textfield',
                    anchor: '100%',
                    itemId: 'pass',
                    fieldLabel: 'Pass',
                    inputType: 'password',
                    allowBlank: false
                }
            ],
            dockedItems: [
                {
                    xtype: 'toolbar',
                    dock: 'bottom',
                    ui: 'footer',
                    layout: {
                        pack: 'end',
                        type: 'hbox'
                    },
                    items: [
                        {
                            xtype: 'button',
                            formBind: true,
                            itemId: 'submit',
                            text: 'Submit'
                        }
                    ]
                }
            ]
        });

        me.callParent(arguments);
    }

});

É um Form com dois campos: usuário de senha e um botão de submit. Nada de mágico no form acima!

Então vamos lá! Quando o usuário clicar no botão de submit, a gente quer enviar o usuário e senha para o server e mostrar um alert na tela apenas - se usuário autenticado ou outra mensagem de erro.

No Controller, vamos ouvir o seguinte evento:

"loginincorrect button#submit": {
    click: this.onButtonClickIncorrect
}

Vamos agora implementar a função/método onButtonClickIncorrect:

onButtonClickIncorrect: function(button, e, eOpts) {
    var formPanel = button.up('form'),
        user = formPanel.down('textfield#user').getValue(),
        pass = formPanel.down('textfield#pass').getValue();

    Ext.Ajax.request({
        url: 'php/login.php',
        params: {
            user: user,
            password: pass
        },
        success: function(conn, response, options, eOpts) {

            Ext.Msg.show({
                title:'Info',
                msg: 'Login feito com sucesso',
                icon: Ext.Msg.INFO,
                buttons: Ext.Msg.OK
            });
        },
        failure: function(conn, response, options, eOpts) {

            Ext.Msg.show({
                title:'Erro',
                msg: 'Usuário ou Senha incorretos.',
                icon: Ext.Msg.ERROR,
                buttons: Ext.Msg.OK
            });
        }
    });
}

E é mais ou menos assim que a gente vê as funções de callback na maioria dos códigos Ext JS! Notou alguma coisa de errado? Se sim, ótimo! Se não, vamos passo a passo para entender.

Cenário 1: entre com usuário e senha "admin"

Esse é o resultado:

ext.ajax.request-loiane-01

Parece ok né? É porque esse é o resultado esperado.

Cenário 2: entre com usuário "abc" e senha "abc"

Esse é o resultado:

ext.ajax.request-loiane-02

Hum, não era esse o resultado né? No server estamos retornando o resultado success = false. A mensagem esperada era outra né?

Aí está o erro: muita gente espera que quando se retorna success = false no server, o callback a ser executado é o da função failure, o que não é verdade. O código está correto, mas o resultado que os desenvolvedores espera não está correto.

Quando o success e quando o failure são executados?

O success significa que o request conseguiu chegar ao server com sucesso, não importa o resultado no server (o seu success interno como true ou false). O success que usamos no server para retornar para o ExtJS não tem nada a ver com o callback success do Ext.Ajax.request! Se o request chegou ao server, automaticamente o callback success será executado!

Mas e o failure? O failure é executado quando acontece uma falha na comunicação. Seja porque não encontrou a URL (erro 404), o servidor está fora do ar, etc.

Vamos ver um terceiro cenário e entender o porquê precisamos do failure também.

Cenário 3: Renomeie o arquivo login.php para login_.php

Quando o ExtJS tentar executar o login.php, teremos um erro 404 - de URL não encontrada.

Entre com usuário e senha "admin".

Esse é o resultado.

ext.ajax.request-loiane-03

Repare no console. O usuário nessa hora fica maluco, pois tá entrando com a senha e usuário corretos, mas a mensagem que o sistema mostra para o usuário é meio sem noção. Por isso é importante saber essas diferenças para que o seu sistema possa tomar as decisões corretas! O código acima não prevê todos os cenários e isso faz com que o sistema não funcione direito! O sistema funciona de maneira errada, e isso é um erro gravíssimo.

Maneira Certa de Usar

Vamos ver agora a maneira correta de prever esses casos. Temos 3 casos então.

Essa seria a função/método com os 3 cenários:

onButtonClickCorrect: function(button, e, eOpts) {
    var formPanel = button.up('form'),
        user = formPanel.down('textfield#user').getValue(),
        pass = formPanel.down('textfield#pass').getValue();

    Ext.Ajax.request({
        url: 'php/login.php',
        params: {
            user: user,
            password: pass
        },
        success: function(conn, response, options, eOpts) {

            var result = Ext.JSON.decode(conn.responseText, true);

            if (!result){ // caso seja null
                result = {};
                result.success = false;
                result.msg = conn.responseText;
            }

            if (result.success) {

                Ext.Msg.show({
                    title:'Info',
                    msg: result.msg,
                    icon: Ext.Msg.INFO,
                    buttons: Ext.Msg.OK
                });

            } else {

                Ext.Msg.show({
                    title:'Erro',
                    msg: result.msg,
                    icon: Ext.Msg.ERROR,
                    buttons: Ext.Msg.OK
                });
            }
        },
        failure: function(conn, response, options, eOpts) {

            Ext.Msg.show({
                title:'Erro - Contate Administrador do sistema!',
                msg: conn.responseText,
                icon: Ext.Msg.ERROR,
                buttons: Ext.Msg.OK
            });
        }
    });
}

Vamos por partes. Quando o success e o failure são executados, eles recebem alguns parâmetros que são passados no request. E temos como usar esses parâmetros. No server, estamos retornando success e msg, então vamos usar isso né? O parâmetro conn.responseText é que contém o success e a msg, porém estão em uma String com formato JSON. Se quisermos usar algum desses parâmetros, temos que decodificar (linha 14). Caso o server não envie nada (por exemplo, caso ocorra algum erro na conexão com o banco de dados) o resultado dessa codificação será null. Então vamos criar um objeto que podemos usar ( linhas 16 a 20 - nesse caso apenas o responseText vai conter alguma mensagem de execeção ou erro).

Agora sim vamos usar o success que usamos no server. Se ele for true, tudo ocorreu bem, então a gente mostra a mensagem que o server enviou (usuário autenticado - linhas 24 a 29). Caso contrário, a gente também exibe uma mensagem (que pode ser de usuário ou senha incorretos ou alguma mensagem de exceção/erro - linhas 33 a 38).

A gente está acostumado a retornar o success no ExtJS, mas como você pode ver, a gente pode retornar qualquer flag de true/false que nesse caso não faz diferença.

E caso tenha alguma falha de comunicação com o servidor, a gente também exibe uma mensagem para o usuário, pedindo para ele/ela entrar em contato com alguém, já que isso não é falha do usuário, e sim do sistema.

Vamos ver nossos 3 cenários novamente para ver o resultado e um cenário 4 (erro com banco de dados).

Cenário 1: entre com usuário e senha "admin"

Esse é o resultado:

ext.ajax.request-loiane-04

Parece ok né? É porque esse é o resultado esperado.

Cenário 2: entre com usuário "abc" e senha "abc"

Esse é o resultado:

ext.ajax.request-loiane-05

Resultado é o esperado.

Cenário 3: Renomeie o arquivo login.php para login_.php

Quando o ExtJS tentar executar o login.php, teremos um erro 404 - de URL não encontrada.

Entre com usuário e senha "admin".

Esse é o resultado.

ext.ajax.request-loiane-06

Agora sim temos uma mensagem que significa algo!

Cenário 4: Erro/Exception com Banco de Dados

Vá no arquivo db.php e troque a senha do banco de dados - para uma errada. Queremos que o php lance um erro.

Entre com usuário e senha "admin".

Esse é o resultado.

ext.ajax.request-loiane-07

Erro no banco de dados. Mesmo assim caiu no success (o request chegou ao servidor, mas teve um erro no código). E a gente consegue mostrar uma mensagem mais útil também.

Viu como codificar corretamente faz toda a diferença? :)

Usando Funções de Escopo

Bem, não basta codificar corretamente, temos que codificar bonito também! Esse terceiro tópico é uma boa prática a ser seguida, ou seja, é opcional, mas é sempre legal a gente seguir boas práticas (isso nos torna desenvolvedores melhores também!).

No nosso exemplo, tem pouco código dentro das funções de callback. Se você pegar um sistema de verdade, geralmente tem bem mais código, e deixar tudo junto assim acaba ficando muito grande e mais complicado pra ler (e também pra dar manutanção).

onButtonClickBestPractice: function(button, e, eOpts) {
    var formPanel = button.up('form'),
        user = formPanel.down('textfield#user').getValue(),
        pass = formPanel.down('textfield#pass').getValue();

    Ext.Ajax.request({
        url: 'php/login.php',
        params: {
            user: user,
            password: pass
        },
        scope: this,
        success: this.onLoginSucess,
        failure: this.onLoginFailure
    });
}

Pra deixar o código mais limpo e mais bonito e podendo reusar código, vamos usar funções de escopo. Vamos ter como escopo o próprio Controller (linha 12).

Se você quiser saber o que acontece em caso de success, basta ir até o função onLoginSuccess e caso failure, basta ir até a função onLoginFailure. Bem mais organizado.

Vamos ver a função onLoginSuccess primeiro:

onLoginSucess: function(conn, response, options, eOpts) {
    var result = Ext.JSON.decode(conn.responseText, true);

    console.log(result);

    if (!result){ // caso seja null
        result = {};
        result.success = false;
        result.msg = conn.responseText;
    }

    if (result.success) {

        this.showMessage('Info', result.msg, Ext.Msg.INFO);

    } else {

        this.showMessage('Erro', result.msg, Ext.Msg.ERROR);

    }
}

É o mesmo código que apresentamos no tópico anterior, só que criamos uma outra função que recebe o título, a mensagem e o ícone a ser mostrado no alerta:

showMessage: function(title, msg, icon) {
    Ext.Msg.show({
        title: title,
        msg: msg,
        icon: icon,
        buttons: Ext.Msg.OK
    });
}

Assim dá pra gente já fazer um reuso de código. Você ainda pode criar uma classe Util para funções assim. E por exemplo, na parte que faz o decode da mensagem do server e criar o objeto result, você pode criar uma função para isso também. Geralmente faço isso para seguir um padrão de retorno do server e também manter um padrão no código inteiro.

Vamos agora para a função de failure:

onLoginFailure: function(conn, response, options, eOpts) {
    this.showMessage('Erro - Contate Administrador do sistema!', conn.responseText, Ext.Msg.ERROR);
}

Muito mais limpo o código né?

São pequenas coisas assim que fazem a diferença. Pode parecer que isso não faz diferença, mas faz sim. Tem diferença na performance e quando fazemos o build de produção, o arquivo fica menor e com isso o javascript da app fica com um tamanho menor (em apps maiores - da vida real - é que a gente percebe essa diferença). E o melhor, faz muita diferença depois de um tempo que você (ou outro desenvolvedor) vai ler esse código.

Download do Código Fonte Completo

Quem tiver interesse de fazer download e brincar para ver a diferença, segue repositório do github (tá em forma de projeto do Sencha Architect 3 também): https://github.com/loiane/extjs-ajax-request-example

Espero que tenham gostado desse tutorial, tempo que não escrevia algo um pouco mais intermediário-avançado.

Se tiverem mais alguma boa prática que querem compartilhar, basta deixar nos comentários!

Até a próxima! :)