Analyser les limitations d’une application web basée sur l’interrogation

Effectué

Application web basée sur l’interrogation.

L’architecture actuelle de l’application signale les informations boursières en extrayant toutes les informations boursières provenant du serveur selon un minuteur. Cette conception est souvent appelée conception basée sur l’interrogation.

Serveur

Les informations boursières sont stockées dans une base de données Azure Cosmos DB. Quand elle est déclenchée par une requête HTTP, la fonction getStocks retourne toutes les lignes de la base de données.

import { app, input } from "@azure/functions";

const cosmosInput = input.cosmosDB({
    databaseName: 'stocksdb',
    containerName: 'stocks',
    connection: 'COSMOSDB_CONNECTION_STRING',
    sqlQuery: 'SELECT * from c',
});

app.http('getStocks', {
    methods: ['GET'],
    authLevel: 'anonymous',
    extraInputs: [cosmosInput],
    handler: (request, context) => {
        const stocks = context.extraInputs.get(cosmosInput);
        
        return {
            jsonBody: stocks,
        };
    },
});
  • Obtenir des données : La première section du code, cosmosInput, obtient tous les éléments de la table stocks, avec la requête SELECT * from c, dans la base de données stocksdb dans Cosmos DB.
  • Retourner les données : La deuxième section du code, app.http, reçoit ces données dans la fonction en tant qu’entrée dans context.extraInputs, puis les retourne en tant que corps de réponse au client.

Client

L’exemple de client utilise Vue.js pour composer l’interface utilisateur et le client Fetch pour gérer les demandes adressées à l’API.

La page HTML utilise un minuteur pour envoyer une demande au serveur toutes les cinq secondes afin de demander des actions. La réponse retourne un tableau d’actions, qui sont ensuite affichées pour l’utilisateur.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" integrity="sha256-8B1OaG0zT7uYA572S2xOxWACq9NXYPQ+U5kHPV1bJN4=" crossorigin="anonymous" />
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css">
    <title>Stocks | Enable automatic updates in a web application using Azure Functions and SignalR</title>
</head>
<body>
    
    <!-- BEGIN: Replace markup in this section -->
    <div id="app" class="container">
        <h1 class="title">Stocks</h1>
        <div id="stocks">
            <div v-for="stock in stocks" class="stock">
                <div class="lead">{{ stock.symbol }}: ${{ stock.price }}</div>
                <div class="change">Change:
                    <span :class="{ 'is-up': stock.changeDirection === '+', 'is-down': stock.changeDirection === '-' }">
                        {{ stock.changeDirection }}{{ stock.change }}
                    </span></div>
            </div>
        </div>
    </div>
    <!-- END  -->

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js" integrity="sha256-chlNFSVx3TdcQ2Xlw7SvnbLAavAQLO0Y/LBiWX04viY=" crossorigin="anonymous"></script>
    <script src="bundle.js" type="text/javascript"></script>
</body>
</html>
import './style.css';

function getApiUrl() {

    const backend = process.env.BACKEND_URL;
    
    const url = (backend) ? `${backend}` : ``;
    return url;
}

const app = new Vue({
    el: '#app',
    interval: null,
    data() { 
        return {
            stocks: []
        }
    },
    methods: {
        async update() {
            try {
                
                const url = `${getApiUrl()}/api/getStocks`;
                console.log('Fetching stocks from ', url);

                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                app.stocks = await response.json();
            } catch (ex) {
                console.error(ex);
            }
        },
        startPoll() {
            this.interval = setInterval(this.update, 5000);
        }
    },
    created() {
        this.update();
        this.startPoll();
    }
});

Dès que la méthode startPoll débute l’interrogation, la méthode update est appelée toutes les cinq secondes. À l’intérieur de la méthode update, une demande GET est envoyée au point de terminaison d’API /api/getStocks, et le résultat est défini sur app.stocks, ce qui met à jour l’interface utilisateur.

Le code serveur et client est relativement simple : obtenir toutes les données, afficher toutes les données. Comme nous le constatons dans notre analyse, cette simplicité est assortie de quelques limitations.

Analyse de la solution prototype

En tant qu’ingénieur Tailwind Traders, vous avez identifié certains des inconvénients de cette approche d’interrogation basée sur un minuteur.

  • Demandes d’API inutiles : Dans le prototype de l’interrogation basée sur un minuteur, l’application cliente contacte le serveur, que des modifications soient apportées ou non aux données sous-jacentes.

  • Actualisations de la page inutiles : Après que le serveur a retourné des données, la liste entière d’actions est mise à jour sur la page web, même si les données n’ont pas changé. Ce mécanisme d’interrogation est une solution inefficace.

  • Intervalles d’interrogation : Le choix du meilleur intervalle d’interrogation pour votre scénario est également une opération délicate. L’interrogation vous oblige à faire un choix entre le coût de chaque appel au back-end et la vitesse à laquelle vous voulez que votre application réponde aux nouvelles données. Il y a souvent un décalage entre le moment où les nouvelles données deviennent disponibles et le moment où l’application les détecte. L’illustration suivante montre le problème.

    Illustration montrant une chronologie et un déclencheur d’interrogation qui vérifie la présence de nouvelles données toutes les cinq minutes. Les nouvelles données sont disponibles après sept minutes. L’application n’a pas connaissance des nouvelles données avant la prochaine interrogation, qui se produit à 10 minutes.

    Dans le pire des cas, le délai potentiel de détection de nouvelles données est égal à l’intervalle d’interrogation. Alors pourquoi ne pas utiliser un intervalle plus petit ?

  • Quantité de données : À mesure que l’application évolue, la quantité de données échangées entre le client et le serveur devient un problème. Chaque en-tête de requête HTTP inclut des centaines d’octets de données ainsi que le cookie de la session. Tout ce traitement, en particulier sous une charge importante, aboutit à un gaspillage des ressources et taxe inutilement le serveur.

Maintenant que vous êtes familiarisé avec le prototype, il est temps de faire fonctionner l’application sur votre ordinateur.

Prise en charge de CORS

Dans le fichier local.settings.json de l’application de fonction, la section Host inclut les paramètres suivants.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "<STORAGE_CONNECTION_STRING>",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "COSMOSDB_CONNECTION_STRING": "<COSMOSDB_CONNECTION_STRING>"
  },
  "Host" : {
    "LocalHttpPort": 7071,
    "CORS": "http://localhost:3000",
    "CORSCredentials": true
  }
}

Cette configuration permet à une application web s’exécutant sur localhost:3000 d’adresser des demandes à l’application de fonction s’exécutant sur localhost:7071. La propriété CORSCredentials indique à l’application de fonction d’accepter les cookies d’informations d’identification de la demande.

Le partage des ressources Cross-Origin (CORS) est une fonctionnalité HTTP qui permet à une application web s’exécutant sous un domaine d’accéder aux ressources d’un autre domaine. Les navigateurs Web implémentent une restriction de sécurité appelée stratégie de même origine qui empêche une page Web d'appeler des API d'un autre domaine ; CORS constitue un moyen sûr pour autoriser un domaine (le domaine d'origine) à appeler des API d'un autre domaine.

En cas d’exécution locale, CORS est configuré dans le fichier local.settings.json de l’exemple, qui n’est jamais publié. Lorsque vous déployez l’application cliente (unité 7), vous devez également mettre à jour les paramètres CORS dans l’application de fonction pour autoriser l’accès à partir de l’application cliente.