App Engine Serie #5:El front-end

Esta es la quinta parte de nuestra serie App Engine, en la que estamos creando una aplicación web de panel de control de tiempo de actividad utilizando la potente plataforma y las herramientas de App Engine de Google. Lea la cuarta parte, donde creamos los controladores, que manejan las solicitudes de URL a nuestra aplicación.

Esta es la parte final de la serie, donde crearemos el front-end jQuery/CSS. Usaremos el complemento Flot jquery para crear una visualización en vivo de los datos de tiempo de actividad recopilados por la aplicación. Es posible que desee mantener la tercera parte de la serie abierta en una pestaña, ya que nos basaremos en el marcado HTML discutido allí.

La solicitud JSON

Como recordará de la última vez, tenemos un controlador dedicado para manejar las solicitudes de AJAX. Maneja y genera respuestas JSON para /ajax/24hours/ , /ajax/7days/ y /ajax/30days/ .

Las respuestas tienen la siguiente estructura:

{
    "chart": {
        "data": [{
            "value": 231,
            "label": "Apr, 10"
        }, {
            "value": 243,
            "label": "Apr, 11"
        }, {
            "value": 330,
            "label": "Apr, 12"
        }, {
            "value": 404,
            "label": "Apr, 13"
        }],
        "tooltip": "Average response time for %1: %2ms"
    },
    "downtime": [{
        "begin": "15:37 on Apr, 11, 2011",
        "end": "15:42 on Apr, 11, 2011",
        "period": "00:05:00"
    }, {
        "begin": "08:16 on Apr, 12, 2011",
        "end": "08:21 on Apr, 12, 2011",
        "period": "00:05:00"
    }, {
        "begin": "08:09 on Apr, 13, 2011",
        "end": "08:14 on Apr, 13, 2011",
        "period": "00:05:00"
    }]
}

El valor las propiedades en la matriz de datos corresponden a los tiempos de respuesta para el ping dado, al seleccionar las últimas 24 horas, o el día al seleccionar 7 o 30 días.

Usaremos estos datos JSON para construir el gráfico y la lista de tiempo de inactividad debajo de él.

JQuery

En esta sección, veremos el código en assets/js/scripts.js . Tenga en cuenta que este no es el único archivo JavaScript en esta aplicación. También estamos usando la biblioteca jQuery , el complemento Flot y excanvas (solicitado solo en navegadores IE más antiguos para compatibilidad).

El primer paso es escuchar el document.ready evento para que podamos acceder al DOM de la página subyacente.

$(document).ready(function(){

    // Caching some of the selectors for better performance
    var periodDropDown = $('#periodDropDown'),
        dropDownUL = $('ul',periodDropDown),
        currentPeriod = $('.currentPeriod',periodDropDown),
        performancePlot = $('#plot'),
        cache = {};

    // Listening for clicks on the dropdown:
    periodDropDown.find('li').click(function(){
        var li = $(this);
        currentPeriod.text(li.text());
        loadPeriod(li.data('action'));
    });

    // Disabling the dropdown when an AJAX request is active:
    periodDropDown.ajaxStart(function(){
        periodDropDown.addClass('inactive');
    }).ajaxComplete(function(){
        periodDropDown.removeClass('inactive');
    });

Como puede ver, estamos asignando algunos de los selectores que usaremos con más frecuencia a variables, lo que tiene un efecto positivo en el rendimiento. Además, estoy usando el ajaxStart() y ajaxComplete() métodos para vincular eventos que se ejecutan una vez que se inicia una solicitud AJAX. En el código anterior, el inactive la clase se agrega a periodDropDown , lo que evita que se expanda al pasar el mouse por encima.

A continuación, vincularemos un evento personalizado al div de la trama, lo que nos ayudará a organizar mejor nuestro código. Más adelante en el código, podremos crear una nueva trama simplemente activando el render método.

  // Binding a custom "render" event to the plot div:

    performancePlot.bind('render',function(e,plotData,labels){

        var ticksLength = 7;

        // Using the Flot jQuery plugin to generate
        // the performance graph:

        var plot = $.plot(performancePlot,
            [{
                // Passing the datapoints received as a parameter
                // and setting the color and legend label.

                data: plotData,
                color:'#86c9ff',
                label: "Response Time"
            }], {
                series: {
                    // Setting additional options for the styling.
                    lines: {
                        show:true,
                        fill:true,
                        fillColor:'rgba(237,247,255,0.4)',
                        lineWidth:1
                    },
                    shadowSize: 0,
                    points: { show: (labels.length == 1) }
                },
                grid: {
                    tickColor:'#e0e0e0',
                    hoverable: true,
                    borderWidth:1,
                    borderColor:'#cccccc'
                },
                xaxis:{

                    // This function is called by the plugin
                    // which passes a "range" object. The function
                    // must generate an array with the divisions ticks:

                    ticks:function(range){

                        ticksLength = range.max-range.min;
                        var dv = 1;

                        // Trying to find a suitable number of ticks,
                        // given the varying number of data points in the
                        // graph:

                        while(ticksLength>12){
                            ticksLength = Math.floor(ticksLength/++dv);
                            if(dv>30) break;
                        }

                        var ratio = (range.max-range.min)/ticksLength,
                            ret = [];

                        ticksLength++;

                        for(var i=0;i<ticksLength;i++){
                            ret.push(Math.floor(i*ratio));
                        }

                        return ret;
                    }
                }

        });

        // The Flot plugin has some limitations. In the snippet below
        // we are replacing the ticks with proper, more descriptive labels:

        var elem = $('div.tickLabel').slice(0,ticksLength).each(function(){
            var l = $(this);
            l.text(labels[parseInt(l.text())]);
        }).last().next().hide();

        // Displaying a tooltip over the points of the plot:

        var prev = null;
        performancePlot.bind("plothover", function (e,pos,item) {

            if (item) {

                if(item.datapoint.toString() == prev){
                    return;
                }

                prev = item.datapoint.toString();

                // Calling the show method of the tooltip object,
                // with X and Y coordinates, and a tooltip text:

                tooltip.show(
                    item.pageX,
                    item.pageY,
                    currentData.chart.tooltip.replace('%2',item.datapoint[1])
                                             .replace('%1',currentData.chart.data[item.dataIndex].label)
                );
            }
            else {
                tooltip.hide();
                prev = null;
            }

        });

    }).bind("mouseleave",function(){
        tooltip.hide();
        prev = null;
    });

El complemento Flot jQuery, que estamos usando para mostrar el bonito gráfico, admite una amplia API. Muchas de las opciones se usaron aquí, y aunque el código está comentado, voy a explicar algunas de ellas con más detalle.

El complemento toma un objeto jQuery (o un nodo DOM) como primer argumento, los puntos de datos que se mostrarán en el gráfico vienen en segundo lugar y el tercero es un objeto con opciones adicionales. El objeto jQuery determina dónde se mostrará el gráfico (el gráfico ocupa todo el ancho y alto del elemento).

Los puntos de datos se solicitan a través de AJAX como verá en un momento. Como tercer argumento de la llamada del complemento, estamos pasando un objeto con tres propiedades:series , que determinan el estilo de los puntos de datos, grid y eje x .

El último atributo - xaxis - es una función de devolución de llamada que llama el complemento antes de generar las divisiones horizontales de la trama. Lo estamos usando para anular y disminuir su número de divisiones predeterminadas, para que estén espaciadas correctamente. De lo contrario, el eje x se desordenaría cuando se muestren los pings de un día completo en el gráfico.

También vinculamos un detector de eventos para el plothover evento que es activado por el complemento, cuando el usuario se desplaza sobre él. Lo estamos usando para mostrar una información sobre herramientas simple, usando los métodos del tooltip objeto, dado a continuación:

// This object provides methods for hiding and showing the tooltip:

    var tooltip = {
        show : function(x, y, str) {

            if(!this.tooltipObj){
                this.tooltipObj = $('<div>',{
                    id      : 'plotTooltip',
                    html    : str,
                    css     : {
                        opacity : 0.75
                    }
                }).appendTo("body");
            }

            this.tooltipObj.hide().html(str);
            var width = this.tooltipObj.outerWidth();

            this.tooltipObj.css({left: x-width/2, top: y+15}).fadeIn(200);
        },
        hide : function(){
            $("#plotTooltip").hide();
        }
    }

    // Loading the data for the last 24hours on page load:
    loadPeriod('24hours');

En la parte inferior del fragmento, puede ver que estamos llamando a una función loadPeriod. Maneja las solicitudes de AJAX y se activa seleccionando un período diferente del menú desplegable. Sin embargo, durante la carga, debemos activarlo manualmente. Puedes ver su código a continuación.

  var currentData;

    // This function fetches and caches AJAX data.
    function loadPeriod(period){

        // If the period exists in cache, return it.
        if(cache[period]){
            render(cache[period]);
        }
        else{

            // Otherwise initiate an AJAX request:
            $.get('/ajax/'+period+'/',function(r){
                cache[period] = r;
                render(r);
            },'json');
        }

        function render(obj){

            var plotData = [],
                labels = [],
                downtimeData = $('#downtimeData');

            // Generating plotData and labels arrays.
            $.each(obj.chart.data,function(i){
                plotData.push([i,this.value]);
                labels.push(this.label);
            });

            // They are passed with our custom "render" event to the plot:
            performancePlot.trigger('render',[plotData, labels]);

            // Formatting the downtime:
            if(obj.downtime.length){

                $('#noDowntime').hide();

                if(!obj.processed){
                    // Adding the heading for the downtime list:
                    obj.downtime.push({begin: 'FROM',end:'TO',period:'DURATION'});
                    obj.downtime = obj.downtime.reverse();
                }
                obj.processed = true;

                var tmp = $('<div class="dtContainer">'),
                    className = '';

                $.each(obj.downtime,function(){
                    if(this.end == 'NOW'){
                        className = ' ongoing';
                    }
                    else className = '';

                    tmp.append(
                        '<div class="row'+className+'">'+
                            '<span class="from">'+this.begin+'</span>'+
                            '<span class="to">'+this.end+'</span>'+
                            '<span class="period">'+this.period+'</span>'
                        +'</div>'
                    );
                });

                downtimeData.html(tmp)
            }
            else {
                downtimeData.empty();
                $('#noDowntime').show();
            }

            currentData = obj;
        }
    }
});

Esta función emite solicitudes AJAX, activa el render método de la parcela div, y genera el registro de tiempo de inactividad. Además, también utiliza un mecanismo de almacenamiento en caché simple, por lo que las solicitudes se realizan solo la primera vez.

Todo lo que nos queda por hacer es agregar algo de brillo CSS.

El CSS

Los estilos utilizados por nuestro panel de tiempo de actividad residen en assets/css/styles.css . Este archivo maneja todo, desde el estilo del cuerpo hasta la información sobre herramientas del gráfico.

Primero diseñaremos el menú desplegable. Este es el elemento que contiene los diferentes períodos para los que proporcionamos informes de tiempo de inactividad.

/*===============================
*   Styling the dropdown:
================================*/

#periodDropDown{
    background: url("../img/sprite.png") repeat-x 0 -7px;
    bottom: 0;
    cursor: pointer;
    height: 38px;
    padding: 0 25px 0 12px;
    position: absolute;
    right: 0;
    text-indent: 37px;
    width: 150px;
    text-shadow: 1px 1px 0 #95999D;
}

#periodDropDown ul{
    display:none;
    background-color: #9FA2A8;
    left: 1px;
    list-style: none outside none;
    padding-bottom: 10px;
    position: absolute;
    right: 1px;
    top: 36px;

    -moz-border-radius:0 0 3px 3px;
    -webkit-border-radius:0 0 3px 3px;
    border-radius:0 0 3px 3px;
}

#periodDropDown:hover ul{
    display:block;
}

#periodDropDown.inactive ul{
    display:none !important;
}

#periodDropDown.inactive{
    cursor:default;
}

#periodDropDown li{
    padding:2px;
    text-indent: 47px;
}
#periodDropDown li:hover{
    background-color:#b0b2b7
}

#periodDropDown .left,
#periodDropDown .right,
#periodDropDown .arrow{
    background: url("../img/sprite.png") repeat-x 0 -45px;
    position:absolute;
    top:0;
    left:0;
    height:38px;
    width:11px;
}

#periodDropDown .right{
    left:auto;
    right:0;
    background-position:0 -83px;
}

#periodDropDown .arrow{
    background-position:0 0;
    width:10px;
    height:7px;
    top:50%;
    margin-top:-2px;
    left:auto;
    right:15px;
}

#periodDropDown .currentPeriod{
    line-height: 34px;
}

Puede ver que estamos usando un sprite CSS para la izquierda y la derecha y las flechas del menú desplegable. Los estilos que son comunes para todos estos elementos se colocan en un solo grupo de declaración, con un estilo individual asignado más adelante.

Después de esto, diseñamos el gráfico y la información sobre herramientas del gráfico.

/*===============================
*   Styling the plot:
================================*/

#plot{
    height:400px;
    margin:30px;
    position:relative;
}

#plot .preloader{
    background: url('../img/preloader.gif') no-repeat center center;
    position:absolute;
    width:100%;
    height:100%;
    left:0;
    top:0;
}

#plotTooltip{
    position: absolute;
    display: none;
    border: 1px solid #a1d0f6;
    padding: 2px 5px;
    background-color: #c5e2f9;
    color:#2a4f6d;
    font-size:11px;
    text-shadow:0 0 1px rgba(255,255,255,0.3);

    -moz-box-shadow:2px 2px 0 rgba(0,0,0,0.1),1px 1px 0 #fff;
    -webkit-box-shadow:2px 2px 0 rgba(0,0,0,0.1),1px 1px 0 #fff;
    box-shadow:2px 2px 0 rgba(0,0,0,0.1),1px 1px 0 #fff;
}

Y por último las secciones y la lista de datos de tiempo de inactividad.

/*===============================
*   The sections:
================================*/

#page h3{
    font-size: 15px;
    line-height: 34px;
    padding: 0 15px;
}

#page .section{
    background: url("../img/sprite.png") repeat-x 0 -121px #FAFAFA;
    border: 1px solid #D1D3D5;
    min-height: 300px;
    margin: 40px 0;
    overflow-x: hidden;

    -moz-border-radius: 4px;
    -webkit-border-radius: 4px;
    border-radius: 4px;

    -moz-box-shadow: 0 0 1px #fff inset;
    -webkit-box-shadow: 0 0 1px #fff inset;
    box-shadow: 0 0 1px #fff inset;
}

/*===============================
*   Downtime styles:
================================*/

#noDowntime{
    color: #999999;
    line-height: 160px;
    text-align: center;
    display:none;
}

.dtContainer{
    color:#777;
}

.row{
    border-bottom: 1px solid #DDDDDD;
    overflow: hidden;
    padding: 6px 0;
}

.row.ongoing{
    color:#C30;
}

#downtimeData .row span{
    float:left;
}

#downtimeData .row:first-child{
    font-size:10px;
}

#downtimeData .from,
#downtimeData .to{
    margin-left: 10px;
    width:40%;
}

#downtimeData{
    margin: 50px 100px;
}

¡Con esto, la interfaz de nuestra aplicación y esta serie están completas!

Para terminar

En el transcurso de esta serie, creamos una aplicación web sencilla de App Engine usando Python y jQuery, mientras mostrábamos las diversas API de Google y cómo usarlas para crear un informe de tiempo de inactividad amigable, completo con un gráfico en vivo. Puede utilizar la aplicación resultante para realizar un seguimiento del rendimiento de su sitio web e incluso modificarlo para incluir funciones más útiles.

¡Asegúrese de compartir sus pensamientos y sugerencias en la sección de comentarios a continuación!