Analysieren der Einschränkungen einer abrufbasierten Web-App

Abgeschlossen

Abrufbasierte Webanwendung

Die derzeitige Architektur der Anwendung meldet Aktieninformationen, indem sie alle Aktienkursinformationen basierend auf einem Timer vom Server abruft. Dieser Aufbau wird oft als abrufbasierter Aufbau bezeichnet.

Server

Die Informationen zum Aktienkurs werden in einer Azure Cosmos DB-Datenbank gespeichert. Wenn sie von einer HTTP-Anforderung ausgelöst wird, gibt die Funktion getStocks alle Zeilen aus der Datenbank zurück.

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,
        };
    },
});
  • Abrufen von Daten: Der erste Codeabschnitt, cosmosInput, ruft alle Elemente in der Tabelle stocks mit der Abfrage SELECT * from c in der Datenbank stocksdb in Cosmos DB ab.
  • Daten zurückgeben: Der zweite Codeabschnitt app.http empfängt diese Daten in der Funktion als Eingabe in context.extraInputs und gibt sie dann als Antworttext an den Client zurück.

Client

Der Beispielclient verwendet Vue.js, um die Benutzeroberfläche zu entwerfen, und den Fetch-Client, um Anforderungen an die API zu verarbeiten.

Die HTML-Seite verwendet einen Timer, um alle fünf Sekunden eine Anforderung an den Server zu senden, um Aktieninformationen anzufordern. Die Antwort gibt ein Array mit Aktien zurück, die dann dem Benutzer angezeigt werden.

<!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();
    }
});

Sobald die startPoll-Methode mit der Abfrage beginnt, wird die update-Methode alle fünf Sekunden aufgerufen. In der Methode update wird eine GET-Anforderung an den /api/getStocks-API-Endpunkt gesendet, und das Ergebnis wird auf app.stocks festgelegt, wodurch die Benutzeroberfläche aktualisiert wird.

Der Server- und Clientcode ist relativ einfach: Abrufen aller Daten, Anzeigen aller Daten. Wie wir in unserer Analyse feststellen, bringt diese Einfachheit einige Einschränkungen mit sich.

Analyse der Prototyplösung

Als Ingenieurin bzw. Ingenieur von Tailwind Traders haben Sie einige der Nachteile dieses zeitgeberbasierten Abrufansatzes identifiziert.

  • Unnötige API-Anforderungen: Bei timerbasierten Abrufprototyp kontaktiert die Clientanwendung den Server, egal ob Änderungen an den zugrunde liegenden Daten vorgenommen wurden oder nicht.

  • Unnötige Seitenaktualisierungen: Sobald Daten vom Server zurückgegeben werden, wird die gesamte Liste der Aktien auf der Webseite aktualisiert, auch wenn keine Daten geändert wurden. Dieser Abrufmechanismus ist ineffizient.

  • Abrufintervalle: Ein passendes Abrufintervall für Ihr Szenario zu finden, ist zudem eine Herausforderung. Beim Abrufen sind Sie gezwungen, sich zu entscheiden, wie kostspielig jeder Aufruf des Back-Ends sein soll und wie schnell Ihre App auf neue Daten reagieren soll. Es bestehen häufig Verzögerungen zwischen dem Zeitpunkt, zu dem neue Daten verfügbar sind, und dem Zeitpunkt, zu dem die App sie erkennt. Die folgende Abbildung veranschaulicht dieses Problem.

    Illustration: Eine Zeitachse und ein Umfragetrigger, der alle fünf Minuten auf neue Daten prüft. Neue Daten werden nach sieben Minuten verfügbar. Die App erkennt die Daten erst nach der nächsten Umfrage, die bei zehn Minuten erfolgt.

    Im schlimmsten Fall entspricht die potenzielle Verzögerung bis zur Erkennung der neuen Daten dem Abrufintervall. Warum sollte also nicht einfach ein kürzeres Intervall verwendet werden?

  • Datenmenge: Da die Anwendung skaliert wird, kann die zwischen Client und Server ausgetauschte Datenmenge zu einem Problem werden. Jeder HTTP-Anforderungsheader enthält neben dem Cookie der Sitzung auch noch Hunderte von Bytes an Daten. Durch diesen Mehraufwand werden insbesondere bei hoher Auslastung unnötige Ressourcen erstellt und der Server strapaziert.

Nachdem Sie nun mit dem Prototyp vertrauter sind, ist es an der Zeit, die Anwendung auf Ihrem Computer auszuführen.

Unterstützung für CORS

In der Datei local.settings.json der Functions-App enthält der Abschnitt Host die folgenden Einstellungen.

{
  "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
  }
}

Durch diese Konfiguration kann eine Webanwendung, die auf localhost:3000 ausgeführt wird, Anforderungen an die Funktions-App senden, die auf localhost:7071 ausgeführt wird. Die Eigenschaft CORSCredentials sagt der Funktions-App, dass sie die Anmeldeinformationscookies der Anforderung akzeptieren soll.

Cross-Origin Resource Sharing (CORS) ist eine HTTP-Funktion, mit der eine Webanwendung, die unter einer Domäne ausgeführt wird, auf Ressourcen in einer anderen Domäne zugreifen kann. Webbrowser implementieren eine als same-origin-Richtlinie bekannte Sicherheitseinschränkung, die verhindert, dass eine Website APIs in einer anderen Domäne aufruft. CORS ist eine sichere Methode, um einer Domäne (der Ursprungsdomäne) den Aufruf von APIs in einer anderen Domäne zu ermöglichen.

Bei der lokalen Ausführung ist CORS für Sie in der Datei local.settings.json des Beispiels konfiguriert, die nie veröffentlicht wird. Wenn Sie die Client-App (Lerneinheit 7) bereitstellen, müssen Sie auch die CORS-Einstellungen in der Funktions-App aktualisieren, um den Zugriff über die Client-App zu ermöglichen.