JavaScript, fetch y JSON

Es posible que desee hacer que su página HTML sea dinámica, cambiando los datos sin recargar toda la página. En lugar de enviar un HTML <form> y realizar una redirección para volver a renderizar la plantilla, puedes añadir JavaScript que llame a fetch() y reemplace el contenido de la página.

fetch() es la solución moderna e integrada de JavaScript para realizar peticiones desde una página. Es posible que haya oído hablar de otros métodos y bibliotecas «AJAX», como XMLHttpRequest() o jQuery. Éstos ya no son necesarios en los navegadores modernos, aunque puede optar por utilizarlos o por otra biblioteca dependiendo de los requisitos de su aplicación. Estos documentos sólo se centrarán en las características incorporadas de JavaScript.

Plantillas de renderizado

Es importante entender la diferencia entre plantillas y JavaScript. Las plantillas se renderizan en el servidor, antes de que la respuesta se envíe al navegador del usuario. El JavaScript se ejecuta en el navegador del usuario, después de que la plantilla se renderice y se envíe. Por lo tanto, es imposible usar JavaScript para afectar la forma en que se renderiza la plantilla Jinja, pero es posible renderizar datos en el JavaScript que se ejecutará.

Para proporcionar datos a JavaScript al renderizar la plantilla, utilice el filtro tojson() en un bloque <script>. Esto convertirá los datos en un objeto JavaScript válido, y asegurará que cualquier carácter HTML inseguro se renderice de forma segura. Si no utilizas el filtro tojson, obtendrás un SyntaxError en la consola del navegador.

data = generate_report()
return render_template("report.html", chart_data=data)
<script>
    const chart_data = {{ chart_data|tojson }}
    chartLib.makeChart(chart_data)
</script>

Un patrón menos común es añadir los datos a un atributo data- en una etiqueta HTML. En este caso, debes usar comillas simples alrededor del valor, no comillas dobles, de lo contrario producirás un HTML inválido o inseguro.

<div data-chart='{{ chart_data|tojson }}'></div>

Generación de URLs

La otra forma de obtener datos del servidor a JavaScript es hacer una petición de los mismos. En primer lugar, es necesario conocer la URL a solicitar.

La forma más sencilla de generar URLs es seguir utilizando url_for() al renderizar la plantilla. Por ejemplo:

const user_url = {{ url_for("user", id=current_user.id)|tojson }}
fetch(user_url).then(...)

Sin embargo, es posible que necesites generar una URL basada en información que sólo conoces en JavaScript. Como se ha comentado anteriormente, JavaScript se ejecuta en el navegador del usuario, no como parte del renderizado de la plantilla, por lo que no puedes utilizar url_for en ese punto.

En este caso, necesitas conocer la «URL raíz» bajo la cual se sirve tu aplicación. En configuraciones simples, esto es /, pero también podría ser algo más, como https://example.com/myapp/.

Una forma sencilla de informar a su código JavaScript sobre esta raíz es establecerla como una variable global al renderizar la plantilla. Entonces podrá utilizarla cuando genere URLs desde JavaScript.

const SCRIPT_ROOT = {{ request.script_root|tojson }}
let user_id = ...  // do something to get a user id from the page
let user_url = `${SCRIPT_ROOT}/user/${user_id}`
fetch(user_url).then(...)

Hacer una petición con fetch

fetch() toma dos argumentos, una URL y un objeto con otras opciones, y devuelve una Promise. No cubriremos todas las opciones disponibles, y sólo usaremos then() en la promise, no otras devoluciones de llamada o la sintaxis await. Lee los documentos MDN vinculados para obtener más información sobre estas características.

Por defecto, se utiliza el método GET. Si la respuesta contiene JSON, se puede utilizar con una cadena de devolución de llamada then().

const room_url = {{ url_for("room_detail", id=room.id)|tojson }}
fetch(room_url)
    .then(response => response.json())
    .then(data => {
        // data is a parsed JSON object
    })

Para enviar datos, utilice un método de datos como POST, y pase la opción body. Los tipos de datos más comunes son los datos de formulario o los datos JSON.

Para enviar los datos del formulario, pasa un objeto FormData poblado. Esto utiliza el mismo formato que un formulario HTML, y se accedería con request.form en una vista Flask.

let data = new FormData()
data.append("name": "Flask Room")
data.append("description": "Talk about Flask here.")
fetch(room_url, {
    "method": "POST",
    "body": data,
}).then(...)

En general, prefiera enviar los datos de la solicitud como datos de formulario, como se utilizaría al enviar un formulario HTML. JSON puede representar datos más complejos, pero a menos que lo necesites es mejor quedarse con el formato más simple. Al enviar datos JSON, la cabecera Content-Type: application/json debe ser enviada también, de lo contrario Flask devolverá un error 400.

let data = {
    "name": "Flask Room",
    "description": "Talk about Flask here.",
}
fetch(room_url, {
    "method": "POST",
    "headers": {"Content-Type": "application/json"},
    "body": JSON.stringify(data),
}).then(...)

Siguiendo las redirecciones

Una respuesta puede ser una redirección, por ejemplo, si usted se registra con JavaScript en lugar de un formulario HTML tradicional, y su vista devuelve una redirección en lugar de JSON. Las solicitudes de JavaScript siguen las redirecciones, pero no cambian la página. Si quieres hacer que la página cambie puedes inspeccionar la respuesta y aplicar la redirección manualmente.

fetch("/login", {"body": ...}).then(
    response => {
        if (response.redirected) {
            window.location = response.url
        } else {
            showLoginError()
        }
    }
)

Sustitución de contenidos

A response might be new HTML, either a new section of the page to add or replace, or an entirely new page. In general, if you’re returning the entire page, it would be better to handle that with a redirect as shown in the previous section. The following example shows how to replace a <div> with the HTML returned by a request.

<div id="geology-fact">
    {{ include "geology_fact.html" }}
</div>
<script>
    const geology_url = {{ url_for("geology_fact")|tojson }}
    const geology_div = getElementById("geology-fact")
    fetch(geology_url)
        .then(response => response.text)
        .then(text => geology_div.innerHtml = text)
</script>

Devolver JSON desde las vistas

Para devolver un objeto JSON desde su vista de la API, puede devolver directamente un dict desde la vista. Se serializará a JSON automáticamente.

@app.route("/user/<int:id>")
def user_detail(id):
    user = User.query.get_or_404(id)
    return {
        "username": User.username,
        "email": User.email,
        "picture": url_for("static", filename=f"users/{id}/profile.png"),
    }

Si quieres devolver otro tipo de JSON, utiliza la función jsonify(), que crea un objeto de respuesta con los datos dados serializados a JSON.

from flask import jsonify

@app.route("/users")
def user_list():
    users = User.query.order_by(User.name).all()
    return jsonify([u.to_json() for u in users])

Normalmente no es una buena idea devolver datos de archivos en una respuesta JSON. JSON no puede representar datos binarios directamente, por lo que debe ser codificado en base64, lo que puede ser lento, requiere más ancho de banda para enviar, y no es tan fácil de almacenar en caché. En su lugar, sirva los archivos usando una vista, y genere una URL al archivo deseado para incluirlo en el JSON. Entonces el cliente puede hacer una petición separada para obtener el recurso vinculado después de obtener el JSON.

Recepción de JSON en las vistas

Utilice la propiedad json del objeto request para decodificar el cuerpo de la solicitud como JSON. Si el cuerpo no es un JSON válido, o la cabecera Content-Type no está definida como application/json, se producirá un error 400 Bad Request.

from flask import request

@app.post("/user/<int:id>")
def user_update(id):
    user = User.query.get_or_404(id)
    user.update_from_json(request.json)
    db.session.commit()
    return user.to_json()