Blog
Nuestra app hace todas las peticiones a través del método cliente servidor, en el cual el cliente hace una petición al servidor y luego el servidor le devuelve al cliente una respuesta, así es como ejecuta nuestro sitio web las tareas. Esto no es malo del todo, pero si que requiere un poco más de tiempo a la hora de realizar las peticiones ya que todas se hacen de la misma manera, este tipo de peticiones, su rapidez depende de varios factores y entre ellos los que más destacan son el estado del servidor y la velocidad de conexión a internet que tenga el cliente para recibir las respuestas del servidor.
Esto antes era un problema, ya que si el servidor presentaba fallas por ende la web se vería afectada en términos de velocidad. Pero eso fue hace un par de años, en 2005 nació la tecnología AJAX que es una manera de realizar peticiones asíncronas (en segundo plano).
Una de las funciones más utilizadas de AJAX es recargar secciones de una página sin tener que hacerle la petición al servidor, ya sabemos que la petición de recargar una página web el navegador le dice al servidor que nos envíe nuevamente la web completa. En esta lección veremos como usar AJAX para enviar los posts en modo asíncrono al servidor y guardarlos en la base de datos, además de eso también realizar otra petición asíncrona luego de enviar el post para hacer que se refresque la linea del tiempo en donde se muestran los posts.
Realizando las peticiones AJAX al servidor
Lo primero que debemos hacer es realizar unas modificaciones en nuestras plantillas, la primera plantilla a modificar será la plantilla index.html, ahora debe verse de la siguiente manera:
{% extends "base.html" %} {% block contenido %} <div class="container-fluid" style="background-color: #dedeee;"> <h1 class="pt-4">Hola, {{ current_user.username }}!</h1> {% if form %} <form id="form-file" action="" method="post" enctype="multipart/form-data" novalidate> {{ form.hidden_tag() }} <div class="form-group mb-0"> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4, class="form-control") }} {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </div> <div class="form-group d-none"> {{ form.imagen(class="form-control-file") }} </div> <div class="form-group"> <input id="input-html" class="form-control-file d-inline mt-3" style="width: 30px;" type="image" src="../static/icons/picture.png"> <p id="p-imagen" style="margin-left: 2.2rem; margin-top: -1.65rem;">Adjuntar imagen al post</p> </div> {{ form.submit(class="btn btn-primary btn-sm", id="ajax-button") }} </form> {% endif %} <div id="post-contenido"> {% include '$post.html' %} </div> <nav aria-label="..."> <ul class="pagination pagination-md"> {% if pagina_ant %} <li class="page-item"> <a class="page-link" href="{{ pagina_ant }}">Página anterior</a> </li> {% endif %} {% if pagina_sig %} <li class="page-item"> <a class="page-link" href="{{ pagina_sig }}">Página siguiente</a> {% endif %} </li> </ul> </nav> </div> <div class="toast" id="toastEnviando" style="position: absolute; top: 7%; right: 0;"> <div class="toast-header"> <strong class="mr-auto">Enviando post...</strong> <small id="toast-tiempo"></small> <button type="button" class="ml-2 mb-1 close" data-dismiss="toast"> <span aria-hidden="true">×</span> </button> </div> <div class="toast-body"> <div>Estamos enviado tu post un momento por favor.</div> </div> </div> <div class="toast" id="toastEnviado" style="position: absolute; top: 7%; right: 0;"> <div class="toast-header"> <strong class="mr-auto">Post enviado.</strong> <small id="toast-tiempo2"></small> <button type="button" class="ml-2 mb-1 close" data-dismiss="toast"> <span aria-hidden="true">×</span> </button> </div> <div class="toast-body"> <div>Gracias por contarle algo al mundo.</div> </div> </div> {% endblock %}
Las modificaciones que le hemos hecho son las siguientes, primero le agregamos al formulario el id form-file
, segundo le agregamos al botón que envía el formulario el id ajax-button
, tercero le quitamos el bucle for donde hacíamos el include de nuestra subplantilla $post.html, y por último hemos agregamos dos elementos de bootstrap que se llaman toasts, esto es usado para enviar notificaciones y es lo que haremos con ellos, este código de los elementos toasts lo he copiado desde la misma web oficial de Bootstrap y les he agregado un id en su etiqueta <small>
para mostrar en terminos de tiempo, el típico “hace unos segundos” usando moment.js (el que usamos en la lección anterior).
A continuación debemos realizar otros cambios en nuestra subplantilla $post.html, para que luzca de la siguiente manera:
{% for post in posts %} <table class="table table-hover"> <tr> <td width="70px"> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> <img class="rounded-circle" src="{{ post.autor.imagen_perfil(70) }}" /> </a> </td> <td> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> {{ post.autor.username }} </a> dijo {{ moment(post.timestamp).fromNow() }}: <br> <p class="mt-2">{{ post.cuerpo }}</p> <img class="img-fluid mt-1" style="width: 300px; border-radius: 1rem;" src="{% if post.post_imagen %} {{ post.post_imagen }} {% else %} {% endif %}" alt=""> </td> </tr> </table> {% endfor %}
El único cambio que hemos hecho es el de agregar el bucle for que quitamos anteriormente de la plantilla index.html, esto es importante para luego poder hacer la petición AJAX para que traiga nuestros posts.
Ahora, vamos a crear otra subplantilla llamada $ajax_posts.html, esta plantilla es prácticamente igual a la subplantilla $post.html lo único que tiene de diferente es que utilizamos código JavaScript para hacer uso de moment.js ya que esta plantilla la va a renderizar AJAX y no podemos usar la sintaxis de flask, entonces:
{% for post in posts %} <table class="table table-hover"> <tr> <td width="70px"> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> <img class="rounded-circle" src="{{ post.autor.imagen_perfil(70) }}" /> </a> </td> <td> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> {{ post.autor.username }} </a> dijo <span class="post-tiempo"></span>: <br> <p class="mt-2">{{ post.cuerpo }}</p> <img class="img-fluid mt-1" style="width: 300px; border-radius: 1rem;" src="{% if post.post_imagen %} {{ post.post_imagen }} {% else %} {% endif %}" alt=""> </td> </tr> </table> {% endfor %}
Ya tenemos la subplantilla que se encargará de renderizar AJAX, ahora solo falta escribir un poco de código python y de Jquery, comencemos con el código de python y ahora vamos a nuestro archivo de rutas.py y modificar la ruta index
y agregar dos rutas nuevas, además importar la librería time:
import time @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = Publicaciones() pagina = request.args.get('pagina', 1, type=int) posts = current_user.pubs_seguidores().paginate( pagina, app.config['POSTS_PER_PAGE'], False) pagina_sig = url_for('index', pagina=posts.next_num) \ if posts.has_next else None pagina_ant = url_for('index', pagina=posts.prev_num) \ if posts.has_prev else None return render_template('index.html', titulo='Página de inicio', form=form, posts=posts.items, pagina_sig=pagina_sig, pagina_ant=pagina_ant) @app.route('/obtener_post', methods=["POST"]) def obtener_post(): form = Publicaciones() if form.validate_on_submit(): post = Pubs(cuerpo=form.post.data, autor=current_user) imagen = form.imagen.data nombre_imagen = secure_filename( current_user.username + '_' + imagen.filename) ruta_imagen = os.path.abspath( 'app\\static\\uploads\\{}'.format(nombre_imagen)) ruta_html = '../static/uploads/{}'.format(nombre_imagen) if imagen.filename != '': imagen.save(ruta_imagen) post.post_imagen = ruta_html else: pass bdd.session.add(post) bdd.session.commit() return "Success" @app.route('/ajax_posts', methods=["GET", "POST"]) @login_required def ajax_posts(): time.sleep(1) # Hacemos una pequeña espera para que la petición AJAX lea los datos correctamente. print('Iniciar') pagina = request.args.get('pagina', 1, type=int) posts = current_user.pubs_seguidores().paginate( pagina, app.config['POSTS_PER_PAGE'], False) pagina_sig = url_for('index', pagina=posts.next_num) \ if posts.has_next else None pagina_ant = url_for('index', pagina=posts.prev_num) \ if posts.has_prev else None return render_template('$ajax_posts.html', posts=posts.items, pagina_sig=pagina_sig, pagina_ant=pagina_ant)
La nueva ruta obtener_post
es la encargada de registrar el post en nuestra base de datos a través de una petición AJAX que haremos más adelante con Jquery, esta ruta posee la misma lógica que tenía anteriormente la ruta index
solo que esta vez será solicitada por AJAX en lugar de ser solicitada por el propio servidor para que registre el post en nuestra base de datos.
La ruta ajax_posts
es la encargada de hacer la petición a la base de datos para traer de vuelta nuestros posts, lo que en si hará es solo refrescar la sección donde se muestran los post y cada vez que agreguemos un post nuevo este aparecera sin necesidad de refrescar la página entera y todo eso es gracias a AJAX.
Para lograr todo eso que acabo de comentarte, se debe escribir el código de Jquery que realice dicha petición AJAX, para ello iremos a nuestra plantilla base.html que es donde hemos escrito un poco de código Jquery anteriormente y agregamos el siguiente código:
<script> // Debajo del código anterior creamos este /* Ajax */ $(document).ready(function () { $('#form-file').submit(function (event) { event.preventDefault(); const data = new FormData($('#form-file')[0]) $.ajax({ url: "/obtener_post", type: "POST", data: data, processData: false, contentType: false, beforeSend: function () { $('#toastEnviando').toast({ delay: 1000 }); $('#toastEnviando').toast('show') $('#toast-tiempo').text(moment.utc().fromNow()); }, statusCode: { 404: function (event) { console.log('La URL solicitada no existe, solicitud no enviada.') }, 200: function () { console.log('URL encontrada, solicitud enviada.') }, 500: function () { console.log('Error interno del servidor, solicitud no enviada.') } }, success: function (textStatus) { if (textStatus == 'Success') { $('#toastEnviado').toast({ delay: 5000 }); $('#toastEnviado').toast('show') $('#toast-tiempo2').text(moment.utc().fromNow()); // Solicitud ajax para traer los posts y mostrarlos luego de enviar un post. $.ajax({ url: "/ajax_posts", type: "POST", beforeSend: function () { console.log('Cargando contenido') }, success: function (data) { setTimeout(function () { $('#post-contenido').html(data); $('.post-tiempo').text(moment.utc().fromNow()) }, 3000) } }); } else { console.log('No es posible traer los posts') } setTimeout(function(){ $('#post').val(""); $('#imagen').val(""); $('#p-imagen').text('Adjuntar imagen al post') }, 1000) } }) }) }) </script>
Enviando y recibiendo posts con AJAX
Inicialmente, escribimos el código de jquery que escribimos anteriomente, el cual le dice a JavaScript que cuando la página este totalmente cargada, ejecute una función que contiene el código que enlaza al formulario seguido del evento .submit()
que envía el formulario, al enviarse el formulario se ejecutará una función que recibe el evento del botón submit y le decimos event.preventDefault()
para prevenir que envíe el formulario con el servidor (ya que lo haremos a través de AJAX).
Seguidamente en una variable imagen creamos un nuevo objeto del tipo formulario, lo hacemos con new FormData()
este objeto recibe un argumento, el cual es el formulario que queremos procesar a través de AJAX, entonces le pasamos como argumento el selector de Jquery con dicho formulario a través de $('#form-file')[0]
utilizamos el indice [0]
para decirle que solo queremos enviar el contenido del formulario y no el objeto entero y acto seguido comienza la magia de AJAX.
Abrimos una petición AJAX con $.ajax({})
y dentro le pasaremos algunos argumentos:
url
es la url donde queremos que AJAX haga nuestra petición, en este caso le diremos que es a la url/obtener_post
de nuestro archivo de rutas.pytype
es el tipo de peteción que en este caso sería una petición del tipo POST, aunque AJAX también realiza peticiones del tipoGET
.data
es el formulario que estamos procesando entonces acá le pasamos la variablepost
donde guardamos dicho formulario.processData
lo que hace es procesar los datos en automático si está entrue
, para este caso nosotros a través de data le decimos como procesarlos, entonces lo establecemos enfalse
.contentType
lo establecemos enfalse
ya que el objeto que estamos enviando no es del tipo XML.beforeSend
aquí se coloca código que se ejecutara antes de que se envíe la petición, en este caso he usado las notificaciones toasts de bootstrap, primero le di la configuración a la primera notificacion toast con$('#toastEnviando').toast({ delay: 1000 })
le he dicho que la notificacion toast que contenga el id#toastEnviando
desaparezca luego de 1000 ms (1seg), luego de darle dicha configuración le digo que se muestre con$('#toastEnviando').toast('show')
, recuerda que estas notificaciones toast las introducimos en la plantilla index.html al inicio de esta sección y las activamos acá con elbeforeSend
de AJAX.statusCode
lo usamos para manipular las respuestas del servidor, 404 si la url no existe, 200 si la solicitud AJAX fue enviada correctamente, 500 si ocurre un error interno del servidor.success
acá escribimos el código que queremos que se ejecute cuando se realice la petición satisfactoriamente, le he dicho que la función reciba como argumento eltextStatus
esta es una variable que nos devuelve elstatusCode
y hacemos uso de ella para indicarle a la petición AJAX que ejecute la otra porción del código una vez valide la respuesta del servidor y si la respuesta del servidor es distinta a 200 pues no ejecutará la petición AJAX, esto lo hacemos para aseguramos de que el servidor ya ha devuelto los datos que leerá AJAX y luego mostrará en el contenido de nuestra app, cuando se recibe esta respuesta del servidor la función se encarga de mostrar mi segunda notificación toast que al igual que la anterior primero la configuramos para que desaparezca, a diferencia de que esta lo hace luego de 5 seg y por último seleccionamos la etiqueta<small>
que anteriormente le agregamos el id para que muestre el tiempo “hace unos segundos” con moment.js y lo hacemos con la instrucción$('#toast-tiempo2').text(moment.utc().fromNow())
, por último le decimos que muestre la notificación toast.
Luego de haber terminado esta primera petición AJAX ya nuestros post se guarda en modo asíncrono (segundo plano) en nuestra base de datos, pero ahora falta hacer que cuando se envié ese post, automaticamente se muestre en el timeline, entonces para ello tenemos que hacer otra petición AJAX, como la vemos en el código y esta petición va a la ruta /ajax_posts
de nuestro archivo rutas.py.
Esta petición AJAX la realizamos dentro del método success
de la petición AJAX anterior, cuando la petición AJAX es recibida en la ruta /ajax_posts
Flask le devuelve información a AJAX en una variable llamada data
(al igual como en la petición anterior, solo que allí no hicimos uso de esta variable data
), entonces le decimos a JavaScript que la función que se ejecuta en el success
reciba esta variable data
, y a partir de aquí seleccioné donde voy a mostrar esos post y le he dicho que los muestre en el elemento que tiene el id post-contenido
que se encuentra en la plantilla index.html, la variable data
para este caso contiene los posts que vienen desde nuestra base de datos.
La intrucción para inyectar los datos que le proporciona Flask a AJAX se hace seleccionando el elemento con dicho id seguido de la función .html()
, sería de la siguiente manera $('#post-contenido').html(data)
esto agregará los posts que vienen desde la base de datos y usamos $('.post-tiempo').text(moment.utc().fromNow())
para mostrar el tiempo de dichos posts en formato de hora local. Esto se hace porque no podemos usar el código del motor de plantilla de Flask como lo explique anteriormente ya que esta es la petición que renderiza la subplantilla $ajax_posts.html.
Hasta aquí se envían, se guardan en la base de datos y se muestran nuestros posts. Pero debemos implementar una última función para reiniciar los formularios, ya que si no lo hacemos quedará escrito el mismo post anterior que ya enviamos. Esto lo solucionamos estableciendo los values de los input en vacíos, lo hacemos con $('#post').val("")
; $('#imagen').val("")
; $('#p-imagen').text('Adjuntar imagen al post')
, lo que hacemos es reiniciar todos los inputs a su estado original luego de que se envíe el post.
Así es como se vería nuestra aplicación hasta ahora al momento de enviar un post:
Listo, esta es alguna de las funciones de AJAX debido a que son muchas las cosas que se pueden hacer, pero hasta aquí implementamos una forma más rápida de enviar, guardar y consultar nuestros posts gracias a AJAX.
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: