Erstellen von Office-Add-Ins mit Microsoft Graph
In diesem Lernprogramm erfahren Sie, wie Sie ein Office-Add-In für Excel erstellen, das die Microsoft Graph-API zum Abrufen von Kalenderinformationen für einen Benutzer verwendet.
Tipp
Wenn Sie es vorziehen, nur das abgeschlossene Lernprogramm herunterzuladen, können Sie das GitHub Repository herunterladen oder klonen.
Voraussetzungen
Bevor Sie mit dieser Demo beginnen, sollten SieNode.js und Yarn auf Ihrem Entwicklungscomputer installiert haben. Wenn Sie nicht über Node.js oder Yarn verfügen, besuchen Sie den vorherigen Link, um Downloadoptionen zu erhalten.
Hinweis
Windows Benutzer müssen möglicherweise Python und Visual Studio Build Tools installieren, um NPM-Module zu unterstützen, die aus C/C++ kompiliert werden müssen. Das Node.js-Installationsprogramm auf Windows bietet die Möglichkeit, diese Tools automatisch zu installieren. Alternativ können Sie die Anweisungen unter https://github.com/nodejs/node-gyp#on-windowsfolgen.
Sie sollten auch über ein persönliches Microsoft-Konto mit einem Postfach auf Outlook.com oder ein Microsoft-Geschäfts-, Schul- oder Unikonto verfügen. Wenn Sie kein Microsoft-Konto haben, gibt es einige Optionen, um ein kostenloses Konto zu erhalten:
- Sie können sich für ein neues persönliches Microsoft-Konto registrieren.
- Sie können sich für das Microsoft 365 Entwicklerprogramm registrieren, um ein kostenloses Microsoft 365 Abonnement zu erhalten.
Hinweis
Dieses Lernprogramm wurde mit Node Version 14.15.0 und Yarn Version 1.22.0 geschrieben. Die Schritte in diesem Handbuch funktionieren möglicherweise mit anderen Versionen, die jedoch nicht getestet wurden.
Feedback
Bitte geben Sie Feedback zu diesem Lernprogramm im GitHub Repository.
Erstellen eines Office-Add-Ins
In dieser Übung erstellen Sie eine Office-Add-In-Lösung mit Express. Die Lösung besteht aus zwei Teilen.
- Das Add-In, implementiert als statische HTML- und JavaScript-Dateien.
- Ein Node.js/Express-Server, der das Add-In bereitstellt und eine Web-API zum Abrufen von Daten für das Add-In implementiert.
Erstellen des Servers
Öffnen Sie die Befehlszeilenschnittstelle (CLI), navigieren Sie zu einem Verzeichnis, in dem Sie Ihr Projekt erstellen möchten, und führen Sie den folgenden Befehl aus, um eine "package.json"-Datei zu generieren.
yarn init
Geben Sie ggf. Werte für die Eingabeaufforderungen ein. Wenn Sie sich nicht sicher sind, sind die Standardwerte in Ordnung.
Führen Sie die folgenden Befehle aus, um Abhängigkeiten zu installieren.
yarn add express@4.17.1 express-promise-router@4.1.0 dotenv@10.0.0 node-fetch@2.6.1 jsonwebtoken@8.5.1@ yarn add jwks-rsa@2.0.4 @azure/msal-node@1.3.0 @microsoft/microsoft-graph-client@3.0.0 yarn add date-fns@2.23.0 date-fns-tz@1.1.6 isomorphic-fetch@3.0.0 windows-iana@5.0.2 yarn add -D typescript@4.3.5 ts-node@10.2.0 nodemon@2.0.12 @types/node@16.4.13 @types/express@4.17.13 yarn add -D @types/node-fetch@2.5.12 @types/jsonwebtoken@8.5.4 @types/microsoft-graph@2.0.0 yarn add -D @types/office-js@1.0.195 @types/jquery@3.5.6 @types/isomorphic-fetch@0.0.35
Führen Sie den folgenden Befehl aus, um eine tsconfig.json-Datei zu generieren.
tsc --init
Öffnen Sie "./tsconfig.json" in einem Text-Editor, und nehmen Sie die folgenden Änderungen vor.
- Ändern Sie den
target
Wert ines6
. - Entfernen Sie die Auskommentierung des
outDir
Werts, und legen Sie ihn auf./dist
. - Entfernen Sie die Auskommentierung des
rootDir
Werts, und legen Sie ihn auf./src
.
- Ändern Sie den
Öffnen Sie "./package.json" , und fügen Sie der JSON die folgende Eigenschaft hinzu.
"scripts": { "start": "nodemon ./src/server.ts", "build": "tsc --project ./" },
Führen Sie den folgenden Befehl aus, um Entwicklungszertifikate für Ihr Add-In zu generieren und zu installieren.
npx office-addin-dev-certs install
Wenn Sie zur Bestätigung aufgefordert werden, bestätigen Sie die Aktionen. Nach Abschluss des Befehls wird eine Ausgabe ähnlich der folgenden angezeigt.
You now have trusted access to https://localhost. Certificate: <path>\localhost.crt Key: <path>\localhost.key
Erstellen Sie eine neue Datei mit dem Namen ".env " im Stammverzeichnis des Projekts, und fügen Sie den folgenden Code hinzu.
AZURE_APP_ID='YOUR_APP_ID_HERE' AZURE_CLIENT_SECRET='YOUR_CLIENT_SECRET_HERE' TLS_CERT_PATH='PATH_TO_LOCALHOST.CRT' TLS_KEY_PATH='PATH_TO_LOCALHOST.KEY'
Ersetzen Sie
PATH_TO_LOCALHOST.CRT
dies durch den Pfad zu "localhost.crt" undPATH_TO_LOCALHOST.KEY
durch den Pfad zur "localhost.key"-Ausgabe durch den vorherigen Befehl.Erstellen Sie ein neues Verzeichnis im Stammverzeichnis Ihres Projekts mit dem Namen "src".
Erstellen Sie zwei Verzeichnisse im Verzeichnis ./src : addin und api.
Erstellen Sie eine neue Datei mit dem Namen "auth.ts " im Verzeichnis "./src/api ", und fügen Sie den folgenden Code hinzu.
import Router from 'express-promise-router'; const authRouter = Router(); // TODO: Implement this router export default authRouter;
Erstellen Sie eine neue Datei mit dem Namen "graph.ts " im Verzeichnis "./src/api ", und fügen Sie den folgenden Code hinzu.
import Router from 'express-promise-router'; const graphRouter = Router(); // TODO: Implement this router export default graphRouter;
Erstellen Sie eine neue Datei namens " server.ts " im Verzeichnis "./src", und fügen Sie den folgenden Code hinzu.
import express from 'express'; import https from 'https'; import fs from 'fs'; import dotenv from 'dotenv'; import path from 'path'; // Load .env file dotenv.config(); import authRouter from './api/auth'; import graphRouter from './api/graph'; const app = express(); const PORT = 3000; // Support JSON payloads app.use(express.json()); app.use(express.static(path.join(__dirname, 'addin'))); app.use(express.static(path.join(__dirname, 'dist/addin'))); app.use('/auth', authRouter); app.use('/graph', graphRouter); const serverOptions = { key: fs.readFileSync(process.env.TLS_KEY_PATH!), cert: fs.readFileSync(process.env.TLS_CERT_PATH!), }; https.createServer(serverOptions, app).listen(PORT, () => { console.log(`⚡️[server]: Server is running at https://localhost:${PORT}`); });
Erstellen des Add-Ins
Erstellen Sie eine neue Datei mit dem Namen taskpane.html im Verzeichnis ./src/addin , und fügen Sie den folgenden Code hinzu.
<html> <head> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css"/> <link rel="stylesheet" href="taskpane.css"/> </head> <body class="ms-Fabric"> <div class="container"> <p class="ms-fontSize-32">Checking authentication...</p> </div> <div class="status"></div> <div class="overlay"> <p class="ms-fontSize-24 ms-fontColor-white">Working...</p> </div> <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.5.1.min.js"></script> <script src="https://appsforoffice.microsoft.com/lib/beta/hosted/office.js"></script> <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js"></script> <script src="taskpane.js"></script> </body> </html>
Erstellen Sie eine neue Datei namens "taskpane.css " im Verzeichnis "./src/addin ", und fügen Sie den folgenden Code hinzu.
.container { margin: 10px; } .status { margin: 10px; } .overlay { position: fixed; display: none; width: 100%; height: 100%; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 2; cursor: pointer; } .overlay p { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); } .status-card { padding: 1em; } .primary-button { padding: .5em; color: white; background-color: #0078d4; border: none; display: block; } .success-msg { background-color: #dff6dd; } .error-msg { background-color: #fde7e9; } .date-picker { display: block; padding: .5em; margin: 10px 0; } .form-input { display: block; padding: .5em; margin: 10px 0; width: 100%; }
Erstellen Sie eine neue Datei mit dem Namen taskpane.js im Verzeichnis ./src/addin , und fügen Sie den folgenden Code hinzu.
// TEMPORARY CODE TO VERIFY ADD-IN LOADS 'use strict'; Office.onReady(info => { if (info.host === Office.HostType.Excel) { $(function() { $('p').text('Hello World!!'); }); } });
Erstellen Sie ein neues Verzeichnis im Verzeichnis .src/addin mit dem Namen "Assets".
Fügen Sie in diesem Verzeichnis gemäß der folgenden Tabelle drei PNG-Dateien hinzu.
Dateiname Größe in Pixeln icon-80.png 80x80 icon-32.png 32x32 icon-16.png 16x16 Hinweis
Sie können für diesen Schritt ein beliebiges Bild verwenden. Sie können die in diesem Beispiel verwendeten Bilder auch direkt aus GitHub herunterladen.
Erstellen Sie ein neues Verzeichnis im Stammverzeichnis des Projekts namens Manifest.
Erstellen Sie eine neue Datei mit dem Namenmanifest.xmlim Ordner "./manifest ", und fügen Sie den folgenden Code hinzu. Ersetzen Sie
NEW_GUID_HERE
durch eine neue GUID, z. Bb4fa03b8-1eb6-4e8b-a380-e0476be9e019
. .<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp"> <Id>NEW_GUID_HERE</Id> <Version>1.0.0.0</Version> <ProviderName>Contoso</ProviderName> <DefaultLocale>en-US</DefaultLocale> <DisplayName DefaultValue="Excel Graph Calendar"/> <Description DefaultValue="An add-in that shows how to call Microsoft Graph to access the user's calendar."/> <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/> <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-80.png"/> <SupportUrl DefaultValue="https://www.contoso.com/help"/> <AppDomains> <AppDomain>https://localhost:3000</AppDomain> </AppDomains> <Hosts> <Host Name="Workbook"/> </Hosts> <DefaultSettings> <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/> </DefaultSettings> <Permissions>ReadWriteDocument</Permissions> <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> <Hosts> <Host xsi:type="Workbook"> <DesktopFormFactor> <GetStarted> <Title resid="GetStarted.Title"/> <Description resid="GetStarted.Description"/> <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/> </GetStarted> <ExtensionPoint xsi:type="PrimaryCommandSurface"> <OfficeTab id="TabHome"> <Group id="CommandsGroup"> <Label resid="CommandsGroup.Label"/> <Icon> <bt:Image size="16" resid="Icon.16x16"/> <bt:Image size="32" resid="Icon.32x32"/> <bt:Image size="80" resid="Icon.80x80"/> </Icon> <Control xsi:type="Button" id="TaskpaneButton"> <Label resid="TaskpaneButton.Label"/> <Supertip> <Title resid="TaskpaneButton.Label"/> <Description resid="TaskpaneButton.Tooltip"/> </Supertip> <Icon> <bt:Image size="16" resid="Icon.16x16"/> <bt:Image size="32" resid="Icon.32x32"/> <bt:Image size="80" resid="Icon.80x80"/> </Icon> <Action xsi:type="ShowTaskpane"> <TaskpaneId>ImportCalendar</TaskpaneId> <SourceLocation resid="Taskpane.Url"/> </Action> </Control> </Group> </OfficeTab> </ExtensionPoint> </DesktopFormFactor> </Host> </Hosts> <Resources> <bt:Images> <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/> <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/> <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/> </bt:Images> <bt:Urls> <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://docs.microsoft.com/graph"/> <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/> </bt:Urls> <bt:ShortStrings> <bt:String id="GetStarted.Title" DefaultValue="Get started with the Excel Graph Calendar add-in!"/> <bt:String id="CommandsGroup.Label" DefaultValue="Graph Calendar"/> <bt:String id="TaskpaneButton.Label" DefaultValue="Import Calendar"/> </bt:ShortStrings> <bt:LongStrings> <bt:String id="GetStarted.Description" DefaultValue="Add-in loaded succesfully. Go to the HOME tab and click the 'Import Calendar' button to get started."/> <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to open the Import Calendar task pane"/> </bt:LongStrings> </Resources> <WebApplicationInfo> <Id>YOUR_APP_ID_HERE</Id> <Resource>api://localhost:3000/YOUR_APP_ID_HERE</Resource> <Scopes> <Scope>openid</Scope> <Scope>profile</Scope> <Scope>access_as_user</Scope> </Scopes> </WebApplicationInfo> </VersionOverrides> </OfficeApp>
Querladen des Add-Ins in Excel
Starten Sie den Server, indem Sie den folgenden Befehl ausführen.
yarn start
Öffnen Sie Ihren Browser, und navigieren Sie zu
https://localhost:3000/taskpane.html
. Es sollte eineNot loaded
Meldung angezeigt werden.Wechseln Sie in Ihrem Browser zu Office.com, und melden Sie sich an. Wählen Sie in der linken Symbolleiste Erstellen und dann Spreadsheet aus.
Wählen Sie die Registerkarte "Einfügen" und dann Office Add-Ins aus.
Wählen Sie Hochladen "Mein Add-In" und dann "Durchsuchen" aus. Hochladen Die ./manifest/manifest.xml-Datei.
Wählen Sie auf der Registerkarte "Start" die Schaltfläche "Kalender importieren" aus, um den Aufgabenbereich zu öffnen.
Nachdem der Aufgabenbereich geöffnet wurde, sollte eine
Hello World!
Meldung angezeigt werden.
Registrieren der App im Portal
In dieser Übung erstellen Sie eine neue Azure AD-Webanwendungsregistrierung mithilfe des Azure Active Directory Admin Centers.
Öffnen Sie einen Browser, und navigieren Sie zum Azure Active Directory Admin Center. Melden Sie sich mit einem persönlichen Konto (auch: Microsoft-Konto) oder einem Geschäfts- oder Schulkonto an.
Wählen Sie in der linken Navigationsleiste Azure Active Directory aus, und wählen Sie dann App-Registrierungen unter Verwalten aus.
Wählen Sie Neue Registrierung aus. Legen Sie auf der Seite Anwendung registrieren die Werte wie folgt fest.
- Legen Sie Name auf
Office Add-in Graph Tutorial
fest. - Legen Sie Unterstützte Kontotypen auf Konten in allen Organisationsverzeichnissen und persönliche Microsoft-Konten fest.
- Legen Sie unter Umleitungs-URI die erste Dropdownoption auf
Single-page application (SPA)
fest, und legen Sie den Wert aufhttps://localhost:3000/consent.html
fest.
- Legen Sie Name auf
Wählen Sie Registrieren aus. Kopieren Sie auf der Seite Office Add-In Graph Lernprogramm den Wert der Anwendungs-ID (Client-ID), und speichern Sie ihn. Sie benötigen ihn im nächsten Schritt.
Wählen Sie unter Verwalten die Option Authentifizierung aus. Suchen Sie den Abschnitt "Implizite Genehmigung", und aktivieren Sie Zugriffstoken und ID-Token. Wählen Sie Speichern aus.
Wählen Sie unter Verwalten die Option Zertifikate und Geheime Clientschlüssel aus. Wählen Sie die Schaltfläche Neuen geheimen Clientschlüssel aus. Geben Sie einen Wert in Beschreibung ein, wählen Sie eine der Optionen für Gilt bis aus, und wählen Sie dann Hinzufügen aus.
Kopieren Sie den Wert des geheimen Clientschlüssels, bevor Sie diese Seite verlassen. Sie benötigen ihn im nächsten Schritt.
Wichtig
Dieser geheime Clientschlüssel wird nicht noch einmal angezeigt, stellen Sie daher sicher, dass Sie ihn jetzt kopieren.
Wählen Sie API-Berechtigungen unter "Verwalten" aus, und wählen Sie dann "Berechtigung hinzufügen" aus.
Wählen Sie Microsoft Graph und dann delegierte Berechtigungen aus.
Wählen Sie die folgenden Berechtigungen aus, und wählen Sie dann Berechtigungen hinzufügen aus.
- offline_access : Dadurch kann die App Zugriffstoken aktualisieren, wenn sie ablaufen.
- Calendars.ReadWrite – Dadurch kann die App den Kalender des Benutzers lesen und in diesen schreiben.
- MailboxSettings.Read – Dadurch kann die App die Zeitzone des Benutzers aus den Postfacheinstellungen abrufen.
Konfigurieren Office einmaligen Add-In-Anmeldens
In diesem Abschnitt aktualisieren Sie die App-Registrierung, um Office Einmaliges Anmelden (Single Sign-On, SSO)zu unterstützen.
Wählen Sie "API verfügbar machen" aus. Wählen Sie im Abschnitt Bereiche , die in diesem API-Abschnitt definiert sind, die Option Bereich hinzufügen aus. Wenn Sie aufgefordert werden, einen Anwendungs-ID-URI festzulegen, legen Sie den Wert auf
api://localhost:3000/YOUR_APP_ID_HERE
, ersetzen Sie ihn durch dieYOUR_APP_ID_HERE
Anwendungs-ID. Wählen Sie "Speichern" aus, und fahren Sie fort.Füllen Sie die Felder wie folgt aus, und wählen Sie Bereich hinzufügen aus.
- Bereichsname:
access_as_user
- Wer zustimmen können?: Administratoren und Benutzer
- Anzeigename der Administratorzustimmung:
Access the app as the user
- Beschreibung der Administratorzustimmung:
Allows Office Add-ins to call the app's web APIs as the current user.
- Anzeigename der Zustimmung des Benutzers:
Access the app as you
- Beschreibung der Benutzerbewilligung:
Allows Office Add-ins to call the app's web APIs as you.
- Status: Aktiviert
- Bereichsname:
Wählen Sie im Abschnitt "Autorisierte Clientanwendungen" die Option "Clientanwendung hinzufügen" aus. Geben Sie eine Client-ID aus der folgenden Liste ein, aktivieren Sie den Bereich unter "Autorisierte Bereiche", und wählen Sie "Anwendung hinzufügen" aus. Wiederholen Sie diesen Vorgang für jede Client-IDs in der Liste.
d3590ed6-52b3-4102-aeff-aad2292ab01c
(Microsoft Office)ea5a67f6-b6f3-4338-b240-c655ddc3cc8e
(Microsoft Office)57fb890c-0dab-4253-a5e0-7188c88b2bb4
(Office im Web)08e18876-6177-487e-b8b5-cf950c1e598c
(Office im Web)
Hinzufügen der Azure AD-Authentifizierung
In dieser Übung aktivieren Sie Office Einmaliges Anmelden (Single Sign-On, SSO) im Add-In und erweitern die Web-API,um den Fluss im Auftrag von zu unterstützen. Dies ist erforderlich, um das erforderliche OAuth-Zugriffstoken zum Aufrufen der Microsoft Graph abzurufen.
Übersicht
Office Add-In-SSO stellt ein Zugriffstoken bereit, aber dieses Token ermöglicht es dem Add-In nur, seine eigene Web-API aufzurufen. Es ermöglicht keinen direkten Zugriff auf die Microsoft-Graph. Der Prozess funktioniert wie folgt.
- Das Add-In ruft ein Token durch Aufrufen von getAccessTokenab. Die Zielgruppe dieses Tokens (der
aud
Anspruch) ist die Anwendungs-ID der App-Registrierung des Add-Ins. - Das Add-In sendet dieses Token im
Authorization
Header, wenn es einen Aufruf an die Web-API sendet. - Die Web-API überprüft das Token und verwendet dann den Im-Auftrag-von-Fluss, um dieses Token gegen ein Microsoft Graph-Token auszutauschen. Die Zielgruppe dieses neuen Tokens ist
https://graph.microsoft.com
. - Die Web-API verwendet das neue Token, um Aufrufe an die Microsoft Graph zu senden, und gibt die Ergebnisse zurück an das Add-In zurück.
Konfigurieren der Lösung
Öffnen Sie ./.env, und aktualisieren Sie
AZURE_APP_ID
das und mit derAZURE_CLIENT_SECRET
Anwendungs-ID und dem geheimen Clientschlüssel aus Ihrer App-Registrierung.Wichtig
Wenn Sie die Quellcodeverwaltung wie Git verwenden, wäre jetzt ein guter Zeitpunkt, um die .env-Datei aus der Quellcodeverwaltung auszuschließen, um zu vermeiden, dass versehentlich Ihre App-ID und ihr geheimer Clientschlüssel offengelegt werden.
Öffnen Sie ./manifest/manifest.xml, und ersetzen Sie alle Instanzen von
YOUR_APP_ID_HERE
mit der Anwendungs-ID aus Ihrer App-Registrierung.Erstellen Sie eine neue Datei im Verzeichnis ./src/addin mit dem Namen config.js, und fügen Sie den folgenden Code hinzu, indem Sie
YOUR_APP_ID_HERE
die Anwendungs-ID aus Ihrer App-Registrierung ersetzen.authConfig = { clientId: 'YOUR_APP_ID_HERE' };
Implementieren der Anmeldung
Öffnen Sie ./src/api/auth.ts, und fügen Sie die folgenden
import
Anweisungen am Anfang der Datei hinzu.import jwt, { SigningKeyCallback, JwtHeader } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as msal from '@azure/msal-node';
Fügen Sie den folgenden Code nach den
import
Anweisungen hinzu.// Initialize an MSAL confidential client const msalClient = new msal.ConfidentialClientApplication({ auth: { clientId: process.env.AZURE_APP_ID!, clientSecret: process.env.AZURE_CLIENT_SECRET! } }); const keyClient = jwksClient({ jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys' }); // Parses the JWT header and retrieves the appropriate public key function getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void { if (header) { keyClient.getSigningKey(header.kid!, (err, key) => { if (err) { callback(err, undefined); } else { callback(null, key.getPublicKey()); } }); } } // Validates a JWT and returns it if valid async function validateJwt(authHeader: string): Promise<string | null> { return new Promise((resolve, reject) => { const token = authHeader.split(' ')[1]; // Ensure that the audience matches the app ID // and the signature is valid const validationOptions = { audience: process.env.AZURE_APP_ID }; jwt.verify(token, getSigningKey, validationOptions, (err, payload) => { if (err) { console.log(`Verify error: ${JSON.stringify(err)}`); resolve(null); } else { resolve(token); } }); }); } // Gets a Graph token from the API token contained in the // auth header export async function getTokenOnBehalfOf(authHeader: string): Promise<string | undefined> { // Validate the supplied token if present const token = await validateJwt(authHeader); if (token) { const result = await msalClient.acquireTokenOnBehalfOf({ oboAssertion: token, skipCache: true, scopes: ['https://graph.microsoft.com/.default'] }); return result?.accessToken; } }
Dieser Code initialisiert einen vertraulichen MSAL-Clientund exportiert eine Funktion, um ein Graph Token aus dem vom Add-In gesendeten Token abzurufen.
Fügen Sie den folgenden Code vor der
export default authRouter;
Zeile hinzu.// Checks if the add-in token can be silently exchanged // for a Graph token. If it can, the user is considered // authenticated. If not, then the add-in needs to do an // interactive login so the user can consent. authRouter.get('/status', async function(req, res) { // Validate access token const authHeader = req.headers['authorization']; if (authHeader) { try { const graphToken = await getTokenOnBehalfOf(authHeader); // If a token was returned, consent is already // granted if (graphToken) { console.log(`Graph token: ${graphToken}`); res.status(200).json({ status: 'authenticated' }); } else { // Respond that consent is required res.status(200).json({ status: 'consent_required' }); } } catch (error) { // Respond that consent is required if the error indicates, // otherwise return the error. const payload = error.name === 'InteractionRequiredAuthError' ? { status: 'consent_required' } : { status: 'error', error: error}; res.status(200).json(payload); } } else { // No auth header res.status(401).end(); } } );
Dieser Code implementiert eine API (
GET /auth/status
), die überprüft, ob das Add-In-Token im Hintergrund gegen ein Graph-Token ausgetauscht werden kann. Das Add-In verwendet diese API, um festzustellen, ob dem Benutzer eine interaktive Anmeldung angezeigt werden muss.Öffnen Sie ./src/addin/taskpane.js, und fügen Sie der Datei den folgenden Code hinzu.
// Handle to authentication pop dialog let authDialog = undefined; // Build a base URL from the current location function getBaseUrl() { return location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : ''); } // Process the response back from the auth dialog function processConsent(result) { const message = JSON.parse(result.message); authDialog.close(); if (message.status === 'success') { showMainUi(); } else { const error = JSON.stringify(message.result, Object.getOwnPropertyNames(message.result)); showStatus(`An error was returned from the consent dialog: ${error}`, true); } } // Use the Office Dialog API to show the interactive // login UI function showConsentPopup() { const authDialogUrl = `${getBaseUrl()}/consent.html`; Office.context.ui.displayDialogAsync(authDialogUrl, { height: 60, width: 30, promptBeforeOpen: false }, (result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { authDialog = result.value; authDialog.addEventHandler(Office.EventType.DialogMessageReceived, processConsent); } else { // Display error const error = JSON.stringify(error, Object.getOwnPropertyNames(error)); showStatus(`Could not open consent prompt dialog: ${error}`, true); } }); } // Inform the user we need to get their consent function showConsentUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Consent for Microsoft Graph access needed' }).appendTo('.container'); $('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: 'In order to access your calendar, we need to get your permission to access the Microsoft Graph.' }).appendTo('.container'); $('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: 'We only need to do this once, unless you revoke your permission.' }).appendTo('.container'); $('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: 'Please click or tap the button below to give permission (opens a popup window).' }).appendTo('.container'); $('<button/>', { class: 'primary-button', text: 'Give permission' }).on('click', showConsentPopup) .appendTo('.container'); } // Display a status function showStatus(message, isError) { $('.status').empty(); $('<div/>', { class: `status-card ms-depth-4 ${isError ? 'error-msg' : 'success-msg'}` }).append($('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: isError ? 'An error occurred' : 'Success' })).append($('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: message })).appendTo('.status'); } function toggleOverlay(show) { $('.overlay').css('display', show ? 'block' : 'none'); }
Dieser Code fügt Funktionen hinzu, um die Benutzeroberfläche zu aktualisieren und die Office Dialog-API zu verwenden, um einen interaktiven Authentifizierungsfluss zu initiieren.
Fügen Sie die folgende Funktion hinzu, um eine temporäre Haupt-UI zu implementieren.
function showMainUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Authenticated!' }).appendTo('.container'); }
Ersetzen Sie den vorhandenen
Office.onReady
Aufruf durch Folgendes.Office.onReady(info => { // Only run if we're inside Excel if (info.host === Office.HostType.Excel) { $(async function() { let apiToken = ''; try { apiToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true }); console.log(`API Token: ${apiToken}`); } catch (error) { console.log(`getAccessToken error: ${JSON.stringify(error)}`); // Fall back to interactive login showConsentUi(); } // Call auth status API to see if we need to get consent const authStatusResponse = await fetch(`${getBaseUrl()}/auth/status`, { headers: { authorization: `Bearer ${apiToken}` } }); const authStatus = await authStatusResponse.json(); if (authStatus.status === 'consent_required') { showConsentUi(); } else { // report error if (authStatus.status === 'error') { const error = JSON.stringify(authStatus.error, Object.getOwnPropertyNames(authStatus.error)); showStatus(`Error checking auth status: ${error}`, true); } else { showMainUi(); } } }); } });
Überlegen Sie, was dieser Code bewirkt.
- Wenn der Aufgabenbereich zum ersten Mal geladen wird, wird ein Token aufgerufen,
getAccessToken
das für die Web-API des Add-Ins ausgelegt ist. - Es verwendet dieses Token, um die
/auth/status
API aufzurufen, um zu überprüfen, ob der Benutzer den Microsoft Graph Bereichen noch zugestimmt hat.- Wenn der Benutzer nicht zugestimmt hat, wird ein Popupfenster verwendet, um die Zustimmung des Benutzers über eine interaktive Anmeldung zu erhalten.
- Wenn der Benutzer seine Zustimmung erteilt hat, wird die Haupt-UI geladen.
- Wenn der Aufgabenbereich zum ersten Mal geladen wird, wird ein Token aufgerufen,
Einholen der Zustimmung des Benutzers
Obwohl das Add-In SSO verwendet, muss der Benutzer dennoch zustimmen, dass das Add-In über Microsoft Graph auf seine Daten zugreift. Das Einholen der Zustimmung ist ein einmaliger Prozess. Nachdem der Benutzer seine Zustimmung erteilt hat, kann das SSO-Token ohne Benutzerinteraktion gegen ein Graph Token ausgetauscht werden. In diesem Abschnitt implementieren Sie die Zustimmungsoberfläche im Add-In mithilfe von msal-browser.
Erstellen Sie eine neue Datei im Verzeichnis ./src/addin mit dem Namen consent.js, und fügen Sie den folgenden Code hinzu.
'use strict'; const msalClient = new msal.PublicClientApplication({ auth: { clientId: authConfig.clientId } }); const msalRequest = { scopes: [ 'https://graph.microsoft.com/.default' ] }; // Function that handles the redirect back to this page // once the user has signed in and granted consent function handleResponse(response) { localStorage.removeItem('msalCallbackExpected'); if (response !== null) { localStorage.setItem('msalAccountId', response.account.homeId); Office.context.ui.messageParent(JSON.stringify({ status: 'success', result: response.accessToken })); } } Office.initialize = function () { if (Office.context.ui.messageParent) { // Let MSAL process a redirect response if that's what // caused this page to load. msalClient.handleRedirectPromise() .then(handleResponse) .catch((error) => { console.log(error); Office.context.ui.messageParent(JSON.stringify({ status: 'failure', result: error })); }); // If we're not expecting a callback (because this is // the first time the page has loaded), then start the // login process if (!localStorage.getItem('msalCallbackExpected')) { // Set the msalCallbackExpected property so we don't // make repeated token requests localStorage.setItem('msalCallbackExpected', 'yes'); // If the user has signed into this machine before // do a token request, otherwise do a login if (localStorage.getItem('msalAccountId')) { msalClient.acquireTokenRedirect(msalRequest); } else { msalClient.loginRedirect(msalRequest); } } } }
Dieser Code anmeldet sich für den Benutzer und fordert den Satz von Microsoft Graph Berechtigungen an, die für die App-Registrierung konfiguriert sind.
Erstellen Sie eine neue Datei im Verzeichnis ./src/addin mit dem Namen consent.html, und fügen Sie den folgenden Code hinzu.
<!DOCTYPE html> <html> <head> <script src="https://appsforoffice.microsoft.com/lib/beta/hosted/office.js"></script> <script src="https://alcdn.msauth.net/browser/2.6.1/js/msal-browser.min.js"></script> <script src="config.js"></script> <script src="consent.js"></script> </head> <body class="ms-Fabric"> <p>Authenticating...</p> </body> </html>
Dieser Code implementiert eine einfache HTML-Seite, um die consent.js Datei zu laden. Diese Seite wird in einem Popupdialogfeld geladen.
Speichern Sie alle Änderungen, und starten Sie den Server neu.
Laden Sie Ihre manifest.xml-Datei erneut hoch, indem Sie die gleichen Schritte zum Querladen des Add-Ins in Excelausführen.
Wählen Sie die Schaltfläche Kalender importieren auf der Registerkarte "Start" aus, um den Aufgabenbereich zu öffnen.
Wählen Sie im Aufgabenbereich die Schaltfläche "Berechtigung erteilen" aus, um das Zustimmungsdialogfeld in einem Popupfenster zu starten. Melden Sie sich an, und erteilen Sie die Zustimmung.
Der Aufgabenbereich wird mit "Authentifiziert!" aktualisiert. Nachricht. Sie können die Token wie folgt überprüfen.
- In den Entwicklertools Ihres Entwicklers wird das API-Token in der Konsole angezeigt.
- In Ihrer CLI, auf der Sie den Node.js Server ausführen, wird das Graph Token gedruckt.
Sie können dieses Token unter https://jwt.ms vergleichen. Beachten Sie, dass die Zielgruppe des API-Tokens (
aud
) auf die Anwendungs-ID Ihrer App-Registrierung festgelegt ist und der Bereich (scp
) istaccess_as_user
.
Abrufen einer Kalenderansicht
In dieser Übung integrieren Sie Microsoft Graph in die Anwendung. Für diese Anwendung verwenden Sie die Microsoft-Graph-Clientbibliothek, um Aufrufe an Microsoft Graph zu tätigen.
Abrufen von Kalenderereignissen von Outlook
Fügen Sie zunächst eine API hinzu, um eine Kalenderansicht aus dem Kalender des Benutzers abzurufen.
Öffnen Sie ./src/api/graph.ts, und fügen Sie die folgenden
import
Anweisungen am Anfang der Datei hinzu.import { zonedTimeToUtc } from 'date-fns-tz'; import { findIana } from 'windows-iana'; import * as graph from '@microsoft/microsoft-graph-client'; import { Event, MailboxSettings } from 'microsoft-graph'; import 'isomorphic-fetch'; import { getTokenOnBehalfOf } from './auth';
Fügen Sie die folgende Funktion hinzu, um das Microsoft Graph SDK zu initialisieren und einen Client zurückzugeben.
async function getAuthenticatedClient(authHeader: string): Promise<graph.Client> { const accessToken = await getTokenOnBehalfOf(authHeader); return graph.Client.init({ authProvider: (done) => { // Call the callback with the // access token done(null, accessToken!); } }); }
Fügen Sie die folgende Funktion hinzu, um die Zeitzone des Benutzers aus den Postfacheinstellungen abzurufen und diesen Wert in einen IANA-Zeitzonenbezeichner zu konvertieren.
interface TimeZones { // The string returned by Microsoft Graph // Could be Windows name or IANA identifier. graph: string; // The IANA identifier iana: string; } async function getTimeZones(client: graph.Client): Promise<TimeZones> { // Get mailbox settings to determine user's // time zone const settings: MailboxSettings = await client .api('/me/mailboxsettings') .get(); // Time zone from Graph can be in IANA format or a // Windows time zone name. If Windows, convert to IANA const ianaTzs = findIana(settings.timeZone!) const ianaTz = ianaTzs ? ianaTzs[0] : null; const returnValue: TimeZones = { graph: settings.timeZone!, iana: ianaTz ?? settings.timeZone! }; return returnValue; }
Fügen Sie die folgende Funktion (unterhalb der
const graphRouter = Router();
Zeile) hinzu, um einen API-Endpunkt ( ) zuGET /graph/calendarview
implementieren.graphRouter.get('/calendarview', async function(req, res) { const authHeader = req.headers['authorization']; if (authHeader) { try { const client = await getAuthenticatedClient(authHeader); const viewStart = req.query['viewStart']?.toString(); const viewEnd = req.query['viewEnd']?.toString(); const timeZones = await getTimeZones(client); // Convert the start and end times into UTC from the user's time zone const utcViewStart = zonedTimeToUtc(viewStart!, timeZones.iana); const utcViewEnd = zonedTimeToUtc(viewEnd!, timeZones.iana); // GET events in the specified window of time const eventPage: graph.PageCollection = await client .api('/me/calendarview') // Header causes start and end times to be converted into // the requested time zone .header('Prefer', `outlook.timezone="${timeZones.graph}"`) // Specify the start and end of the calendar view .query({ startDateTime: utcViewStart.toISOString(), endDateTime: utcViewEnd.toISOString() }) // Only request the fields used by the app .select('subject,start,end,organizer') // Sort the results by the start time .orderby('start/dateTime') // Limit to at most 25 results in a single request .top(25) .get(); const events: any[] = []; // Set up a PageIterator to process the events in the result // and request subsequent "pages" if there are more than 25 // on the server const callback: graph.PageIteratorCallback = (event) => { // Add each event into the array events.push(event); return true; }; const iterator = new graph.PageIterator(client, eventPage, callback, { headers: { 'Prefer': `outlook.timezone="${timeZones.graph}"` } }); await iterator.iterate(); // Return the array of events res.status(200).json(events); } catch (error) { console.log(error); res.status(500).json(error); } } else { // No auth header res.status(401).end(); } } );
Überlegen Sie, was dieser Code bewirkt.
- Sie ruft die Zeitzone des Benutzers ab und verwendet diese, um den Anfang und das Ende der angeforderten Kalenderansicht in UTC-Werte zu konvertieren.
- Dies erfolgt
GET
für den/me/calendarview
Graph-API-Endpunkt.- Sie verwendet die
header
Funktion, um denPrefer: outlook.timezone
Header festzulegen, wodurch die Start- und Endzeiten der zurückgegebenen Ereignisse an die Zeitzone des Benutzers angepasst werden. - Sie verwendet die Funktion zum Hinzufügen von Parametern und zum
query
Festlegen von Start und Ende derstartDateTime
endDateTime
Kalenderansicht. - Sie verwendet die
select
Funktion, um nur die vom Add-In verwendeten Felder anzufordern. - Die
orderby
Funktion wird verwendet, um die Ergebnisse nach der Startzeit zu sortieren. - Sie verwendet die
top
Funktion, um die Ergebnisse in einer einzelnen Anforderung auf 25 zu beschränken.
- Sie verwendet die
- Es verwendet ein PageIteratorCallback -Objekt, um die Ergebnisse zu durchlaufen und zusätzliche Anforderungen zu stellen, wenn weitere Seiten mit Ergebnissen verfügbar sind.
Aktualisieren der Benutzeroberfläche
Nun aktualisieren wir den Aufgabenbereich, damit der Benutzer ein Start- und Enddatum für die Kalenderansicht angeben kann.
Öffnen Sie ./src/addin/taskpane.js, und ersetzen Sie die vorhandene
showMainUi
Funktion durch Folgendes.function showMainUi() { $('.container').empty(); // Use luxon to calculate the start // and end of the current week. Use // those dates to set the initial values // of the date pickers const now = luxon.DateTime.local(); const startOfWeek = now.startOf('week'); const endOfWeek = now.endOf('week'); $('<h2/>', { class: 'ms-fontSize-24 ms-fontWeight-semibold', text: 'Select a date range to import' }).appendTo('.container'); // Create the import form $('<form/>').on('submit', getCalendar) .append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'Start' })).append($('<input/>', { class: 'form-input', type: 'date', value: startOfWeek.toISODate(), id: 'viewStart' })).append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'End' })).append($('<input/>', { class: 'form-input', type: 'date', value: endOfWeek.toISODate(), id: 'viewEnd' })).append($('<input/>', { class: 'primary-button', type: 'submit', id: 'importButton', value: 'Import' })).appendTo('.container'); $('<hr/>').appendTo('.container'); $('<h2/>', { class: 'ms-fontSize-24 ms-fontWeight-semibold', text: 'Add event to calendar' }).appendTo('.container'); // Create the new event form $('<form/>').on('submit', createEvent) .append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'Subject' })).append($('<input/>', { class: 'form-input', type: 'text', required: true, id: 'eventSubject' })).append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'Start' })).append($('<input/>', { class: 'form-input', type: 'datetime-local', required: true, id: 'eventStart' })).append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'End' })).append($('<input/>', { class: 'form-input', type: 'datetime-local', required: true, id: 'eventEnd' })).append($('<input/>', { class: 'primary-button', type: 'submit', id: 'importButton', value: 'Create' })).appendTo('.container'); }
Dieser Code fügt ein einfaches Formular hinzu, damit der Benutzer ein Start- und Enddatum angeben kann. Außerdem wird ein zweites Formular zum Erstellen eines neuen Ereignisses implementiert. Dieses Formular führt vorerst keine Aktion aus, Sie implementieren dieses Feature im nächsten Abschnitt.
Fügen Sie der Datei den folgenden Code hinzu, um eine Tabelle im aktiven Arbeitsblatt zu erstellen, die die aus der Kalenderansicht abgerufenen Ereignisse enthält.
const DAY_MILLISECONDS = 86400000; const DAY_MINUTES = 1440; const EXCEL_DATE_OFFSET = 25569; // Excel date cells require an OLE Automation date format // You can use the Moment-MSDate plug-in // (https://docs.microsoft.com/office/dev/add-ins/excel/excel-add-ins-ranges-advanced#work-with-dates-using-the-moment-msdate-plug-in) // Or you can do the conversion yourself function convertDateToOAFormat(dateTime) { const date = new Date(dateTime); // Get the time zone offset for the browser's time zone // since all of the dates here are handled in that time zone const tzOffset = date.getTimezoneOffset() / DAY_MINUTES; // Calculate the OLE Automation date, which is // the number of days since midnight, December 30, 1899 const oaDate = date.getTime() / DAY_MILLISECONDS + EXCEL_DATE_OFFSET - tzOffset; return oaDate; } async function writeEventsToSheet(events) { await Excel.run(async (context) => { const sheet = context.workbook.worksheets.getActiveWorksheet(); const eventsTable = sheet.tables.add('A1:D1', true); // Create the header row eventsTable.getHeaderRowRange().values = [[ 'Subject', 'Organizer', 'Start', 'End' ]]; // Create the data rows const data = []; events.forEach((event) => { data.push([ event.subject, event.organizer.emailAddress.name, convertDateToOAFormat(event.start.dateTime), convertDateToOAFormat(event.end.dateTime) ]); }); eventsTable.rows.add(null, data); const tableRange = eventsTable.getRange(); tableRange.numberFormat = [["[$-409]m/d/yy h:mm AM/PM;@"]]; tableRange.format.autofitColumns(); tableRange.format.autofitRows(); try { await context.sync(); } catch (err) { console.log(`Error: ${JSON.stringify(err)}`); showStatus(err, true); } }); }
Fügen Sie die folgende Funktion hinzu, um die Kalenderansichts-API aufzurufen.
async function getCalendar(evt) { evt.preventDefault(); toggleOverlay(true); try { const apiToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true }); const viewStart = $('#viewStart').val(); const viewEnd = $('#viewEnd').val(); const requestUrl = `${getBaseUrl()}/graph/calendarview?viewStart=${viewStart}&viewEnd=${viewEnd}`; const response = await fetch(requestUrl, { headers: { authorization: `Bearer ${apiToken}` } }); if (response.ok) { const events = await response.json(); writeEventsToSheet(events); showStatus(`Imported ${events.length} events`, false); } else { const error = await response.json(); showStatus(`Error getting events from calendar: ${JSON.stringify(error)}`, true); } toggleOverlay(false); } catch (err) { console.log(`Error: ${JSON.stringify(err)}`); showStatus(`Exception getting events from calendar: ${JSON.stringify(error)}`, true); } }
Speichern Sie alle Änderungen, starten Sie den Server neu, und aktualisieren Sie den Aufgabenbereich in Excel (schließen Sie alle geöffneten Aufgabenbereiche, und öffnen Sie ihn erneut).
Wählen Sie Start- und Enddatum aus, und wählen Sie Importieren aus.
Erstellen eines neuen Ereignisses
In diesem Abschnitt fügen Sie die Möglichkeit hinzu, Ereignisse im Kalender des Benutzers zu erstellen.
Implementieren der API
Öffnen Sie ./src/api/graph.ts, und fügen Sie den folgenden Code hinzu, um eine neue Ereignis-API ( ) zu
POST /graph/newevent
implementieren.graphRouter.post('/newevent', async function(req, res) { const authHeader = req.headers['authorization']; if (authHeader) { try { const client = await getAuthenticatedClient(authHeader); const timeZones = await getTimeZones(client); // Create a new Graph Event object const newEvent: Event = { subject: req.body['eventSubject'], start: { dateTime: req.body['eventStart'], timeZone: timeZones.graph }, end: { dateTime: req.body['eventEnd'], timeZone: timeZones.graph } }; // POST /me/events await client.api('/me/events') .post(newEvent); // Send a 201 Created res.status(201).end(); } catch (error) { console.log(error); res.status(500).json(error); } } else { // No auth header res.status(401).end(); } } );
Öffnen Sie ./src/addin/taskpane.js, und fügen Sie die folgende Funktion hinzu, um die neue Ereignis-API aufzurufen.
async function createEvent(evt) { evt.preventDefault(); toggleOverlay(true); const apiToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true }); const payload = { eventSubject: $('#eventSubject').val(), eventStart: $('#eventStart').val(), eventEnd: $('#eventEnd').val() }; const requestUrl = `${getBaseUrl()}/graph/newevent`; const response = await fetch(requestUrl, { method: 'POST', headers: { authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { showStatus('Event created', false); } else { const error = await response.json(); showStatus(`Error creating event: ${JSON.stringify(error)}`, true); } toggleOverlay(false); }
Speichern Sie alle Änderungen, starten Sie den Server neu, und aktualisieren Sie den Aufgabenbereich in Excel (schließen Sie alle geöffneten Aufgabenbereiche, und öffnen Sie ihn erneut).
Füllen Sie das Formular aus, und wählen Sie "Erstellen" aus. Stellen Sie sicher, dass das Ereignis dem Kalender des Benutzers hinzugefügt wird.
Herzlichen Glückwunsch!
Sie haben das Lernprogramm Office Add-Ins für Microsoft Graph abgeschlossen. Da Sie nun über ein funktionierendes Add-In verfügen, das Microsoft Graph aufruft, können Sie experimentieren und neue Features hinzufügen. Besuchen Sie die Übersicht über Microsoft Graph, um alle Daten anzuzeigen, auf die Sie mit Microsoft Graph zugreifen können.
Feedback
Bitte geben Sie Feedback zu diesem Lernprogramm im GitHub Repository.
Liegt ein Problem mit diesem Abschnitt vor? Wenn ja, senden Sie uns Feedback, damit wir den Abschnitt verbessern können.