Microsoft Officeを使用してアドインをビルドGraph
このチュートリアルでは、Microsoft Office API を使用してユーザー Excel予定表情報を取得する Graph アドインを作成する方法について説明します。
ヒント
完了したチュートリアルをダウンロードする場合は、リポジトリをダウンロードまたは複製GitHubできます。
前提条件
このデモを開始する前に、開発Node.jsにインストールされている必要があります。 ファイルまたは Yarn がNode.js場合は、ダウンロード オプションの前のリンクを参照してください。
注意
Windows C/C++ からコンパイルする必要Visual Studio Build Tools NPM モジュールをサポートするために、Python と windows をインストールする必要がある場合があります。 このNode.jsインストーラー Windowsこれらのツールを自動的にインストールするオプションを提供します。 または、以下の手順に従います https://github.com/nodejs/node-gyp#on-windows。
また、Outlook.com 上のメールボックスを持つ個人用 Microsoft アカウント、または Microsoft の仕事用または学校用のアカウントを持っている必要があります。 Microsoft アカウントをお持ちでない場合は、無料アカウントを取得するためのオプションが 2 つご利用できます。
- 新しい 個人用 Microsoft アカウントにサインアップできます。
- 開発者プログラムにサインアップして、Microsoft 365サブスクリプションをMicrosoft 365できます。
注意
このチュートリアルは、ノード バージョン 14.15.0 と Yarn バージョン 1.22.0 で記述されています。 このガイドの手順は、他のバージョンでも動作しますが、テストされていない場合があります。
フィードバック
このチュートリアルに関するフィードバックは、リポジトリのGitHubしてください。
Office アドインを作成する
この演習では、Express を使用してOfficeアドイン ソリューションを作成します。 ソリューションは 2 つの部分で構成されます。
- 静的 HTML ファイルおよび JavaScript ファイルとして実装されたアドイン。
- アドインNode.jsサービスを提供し、アドインのデータを取得するための Web API を実装する/Express サーバー。
サーバーの作成
コマンド ライン インターフェイス (CLI) を開き、プロジェクトを作成するディレクトリに移動し、次のコマンドを実行して package.json ファイルを生成します。
yarn init
必要に応じて、プロンプトの値を入力します。 不明な場合は、既定値で問題ありません。
依存関係をインストールするには、次のコマンドを実行します。
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
次のコマンドを実行して、tsconfig.json ファイルを生成します。
tsc --init
テキスト エディターで ./tsconfig.json を開き、次の変更を行います。
- に値を
target
変更しますes6
。 - 値のアンコメント
outDir
を解除し、に設定します./dist
。 - 値のアンコメント
rootDir
を解除し、に設定します./src
。
- に値を
./package.json を開 き、次のプロパティを JSON に追加します。
"scripts": { "start": "nodemon ./src/server.ts", "build": "tsc --project ./" },
次のコマンドを実行して、アドインの開発証明書を生成してインストールします。
npx office-addin-dev-certs install
確認を求めるメッセージが表示されたら、操作を確認します。 コマンドが完了すると、次のような出力が表示されます。
You now have trusted access to https://localhost. Certificate: <path>\localhost.crt Key: <path>\localhost.key
プロジェクトのルートに .env という名前の 新しいファイルを作成し、次のコードを追加します。
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'
localhost.crt
PATH_TO_LOCALHOST.CRT
PATH_TO_LOCALHOST.KEY
へのパスと、前のコマンドによる localhost.key 出力へのパスに置き換える。src という名前のプロジェクトのルートに新しいディレクトリを作成 します。
./src ディレクトリに addin と api の 2 つのディレクトリを 作成 します。
./src/api ディレクトリ に auth.ts という名前の新しいファイルを作成し、次のコードを追加します。
import Router from 'express-promise-router'; const authRouter = Router(); // TODO: Implement this router export default authRouter;
./src/api ディレクトリに graph.ts という名前の新しいファイルを作成し、次のコードを追加します。
import Router from 'express-promise-router'; const graphRouter = Router(); // TODO: Implement this router export default graphRouter;
./src ディレクトリに server.ts という 名前の新しいファイルを作成 し、次のコードを追加します。
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}`); });
アドインを作成する
./src/addin ディレクトリに taskpane.html という名前の新しいファイルを作成し、次のコードを追加します。
<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>
./src/addin ディレクトリに taskpane.css という名前の新しいファイルを作成し、次のコードを追加します。
.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%; }
./src/addin ディレクトリに taskpane.js という名前の新しいファイルを作成し、次のコードを追加します。
// TEMPORARY CODE TO VERIFY ADD-IN LOADS 'use strict'; Office.onReady(info => { if (info.host === Office.HostType.Excel) { $(function() { $('p').text('Hello World!!'); }); } });
assets という名前の .src/addin ディレクトリに新しい ディレクトリを作成 します。
次の表に従って、このディレクトリに 3 つの PNG ファイルを追加します。
ファイル名 ピクセル単位のサイズ icon-80.png 80x80 icon-32.png 32x32 icon-16.png 16x16 注意
この手順に必要な任意のイメージを使用できます。 また、このサンプルで使用されている画像は、このサンプルから直接ダウンロードGitHub。
manifest という名前のプロジェクトのルートに新しいディレクトリを作成 します。
./manifest フォルダーに manifest.xml という名前の新しい ファイルを作成 し、次のコードを追加します。 など、
NEW_GUID_HERE
新しい GUID に置き換えるb4fa03b8-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>
アドインをサイド ロードExcel
次のコマンドを実行して、サーバーを起動します。
yarn start
ブラウザーを開き、を参照します
https://localhost:3000/taskpane.html
。 メッセージが表示Not loaded
されます。ブラウザーで、[Office.com] に移動してサインインします。 左側 のツールバーで [作成] を選択し、[スプレッドシート] を 選択します。
[挿入 ] タブを 選択し、[アドイン Office選択します。
[アップロードアドイン] を選択し、[ 参照] を 選択します。 アップロード ./manifest/manifest.xmlファイルをmanifest.xml します。
[ホーム ] タブの [予定表 のインポート] ボタンを選択 して、タスクウィンドウを開きます。
taskpane が開いたら、メッセージが表示
Hello World!
されます。
ポータルでアプリを登録する
この演習では、管理者センターを使用して新AD Azure Azure Active Directory作成します。
ブラウザーを開き、Azure Active Directory 管理センターへ移動します。 個人用アカウント (別名: Microsoft アカウント)、または 職場/学校アカウント を使用してログインします。
左側のナビゲーションで [Azure Active Directory] を選択し、それから [管理] で [アプリの登録] を選択します。
[新規登録] を選択します。 [アプリケーションを登録] ページで、次のように値を設定します。
Office Add-in Graph Tutorial
に [名前] を設定します。- [サポートされているアカウントの種類] を [任意の組織のディレクトリ内のアカウントと個人用の Microsoft アカウント] に設定します。
- [リダイレクト URI] で、最初のドロップダウン リストを
Single-page application (SPA)
に設定し、それからhttps://localhost:3000/consent.html
に値を設定します。
[登録] を選択します。 [チュートリアル Officeアドイン Graph] ページで、アプリケーション (クライアント) ID の値をコピーして保存します。次の手順で必要になります。
[管理] の下の [認証] を選択します。 [暗黙的な 付与] セクションを見 つけて 、Access トークンと ID トークン を有効にします。 [保存] を選択します。
[管理] で [証明書とシークレット] を選択します。 [新しいクライアント シークレット] ボタンを選択します。 [説明] に値を入力して、[有効期限] のオプションのいずれかを選び、[追加] を選択します。
クライアント シークレットの値をコピーしてから、このページから移動します。 次の手順で行います。
重要
このクライアント シークレットは今後表示されないため、今必ずコピーするようにしてください。
[管理 ] で [API アクセス 許可] を選択し、[アクセス許可 の追加] を選択します。
[Microsoft Graph] を選択 し、[委任されたアクセス許可] を選択します。
次のアクセス許可を選択し、[アクセス許可の 追加] を選択します。
- offline_access - これにより、アプリが期限切れになったときにアクセス トークンを更新できます。
- Calendars.ReadWrite - これにより、アプリはユーザーの予定表に対する読み取りおよび書き込みを行います。
- MailboxSettings.Read - これにより、アプリはメールボックス設定からユーザーのタイム ゾーンを取得できます。
アドインOfficeシングル サインオンの構成
このセクションでは、アプリの登録を更新して、Office シングル サインオン(SSO) をサポートします。
[API の公開] を選択します。 [この API で定義されたスコープ] セクションで 、[スコープの 追加] を選択します。 アプリケーション ID URI の設定を求めるメッセージが 表示されたら、値をアプリケーション ID に
api://localhost:3000/YOUR_APP_ID_HERE
YOUR_APP_ID_HERE
置き換えるに設定します。 [保存 して続行] を選択します。次のようにフィールドに入力し、[スコープの追加] を選択します。
- スコープ名:
access_as_user
- Who同意できますか:管理者とユーザー
- 管理者の同意表示名:
Access the app as the user
- 管理者の同意の説明:
Allows Office Add-ins to call the app's web APIs as the current user.
- ユーザーの同意表示名:
Access the app as you
- ユーザーの同意の説明:
Allows Office Add-ins to call the app's web APIs as you.
- 状態: 有効
- スコープ名:
[承認済 みクライアント アプリケーション] セクションで、[ クライアント アプリケーション の追加] を選択します。 次の一覧からクライアント ID を入力し、[承認済みスコープ] でスコープを有効にし、[アプリケーションの追加]を選択します。 リスト内のクライアント ID ごとにこのプロセスを繰り返します。
d3590ed6-52b3-4102-aeff-aad2292ab01c
(Microsoft Office)ea5a67f6-b6f3-4338-b240-c655ddc3cc8e
(Microsoft Office)57fb890c-0dab-4253-a5e0-7188c88b2bb4
(Office on the web)08e18876-6177-487e-b8b5-cf950c1e598c
(Office on the web)
Azure AD 認証を追加する
この演習では、アドインOfficeシングル サインオン(SSO)を有効にし、Web API を拡張して、代理フローをサポートします。 これは、Microsoft サーバーを呼び出す必要がある OAuth アクセス トークンを取得するために必要Graph。
概要
Officeアドイン SSO はアクセス トークンを提供しますが、そのトークンはアドインが独自の Web API を呼び出す場合にのみ有効にします。 Microsoft サーバーへの直接アクセスは有効Graph。 このプロセスは次のように動作します。
- アドインは getAccessToken を呼び出して トークンを取得します。 このトークンの対象ユーザー (
aud
クレーム) は、アドインのアプリ登録のアプリケーション ID です。 - アドインは、Web API を呼び出す際にヘッダーに
Authorization
このトークンを送信します。 - Web API はトークンを検証し、その後、代理フローを使用してこのトークンを Microsoft のトークンと交換Graphします。 この新しいトークンの対象ユーザーはです
https://graph.microsoft.com
。 - Web API は、新しいトークンを使用して Microsoft Graphを呼び出し、結果をアドインに返します。
ソリューションを構成する
./.env を開 き、アプリ登録のアプリケーション
AZURE_APP_ID
ID とクライアント シークレットを更新AZURE_CLIENT_SECRET
します。重要
git などのソース管理を使用している場合は 、.env ファイルをソース管理から除外して、アプリ ID とクライアント シークレットが誤って漏洩しないようにする良い時期です。
./manifest/manifest.xml 開き、すべてのインスタンスをアプリ
YOUR_APP_ID_HERE
登録のアプリケーション ID に置き換える。config.jsという名前の ./src/addin ディレクトリに新しいファイルを作成し 、次の コードを追加し、アプリ登録のアプリケーション
YOUR_APP_ID_HERE
ID に置き換えます。authConfig = { clientId: 'YOUR_APP_ID_HERE' };
サインインの実装
./src/api/auth.ts を開き、ファイルの上部に次の
import
ステートメントを追加します。import jwt, { SigningKeyCallback, JwtHeader } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as msal from '@azure/msal-node';
ステートメントの後に次のコード
import
を追加します。// 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; } }
このコードは、MSAL機密クライアントを初期化し、アドインから送信されたトークンから Graph トークンを取得する関数をエクスポートします。
行の前に次のコードを
export default authRouter;
追加します。// 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(); } } );
このコードは API ( ) を実装し、アドイン トークンをサイレント モードでトークンと交換Graph
GET /auth/status
します。 アドインは、この API を使用して、ユーザーに対話的なログインを提示する必要があるかどうかを判断します。./src/addin/taskpane.js を開き、次のコードをファイルに追加します。
// 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'); }
このコードでは、UI を更新する関数を追加し、Officeダイアログ API を使用して対話型認証フローを開始します。
一時的なメイン UI を実装するには、次の関数を追加します。
function showMainUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Authenticated!' }).appendTo('.container'); }
既存の呼び出しを
Office.onReady
次に置き換える。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(); } } }); } });
このコードの動作を検討します。
- 作業ウィンドウが最初に読み込まれると、アドインの Web API のスコープが設定されたトークンを取得
getAccessToken
するために呼び出されます。 - このトークンを使用して API を呼び出して、ユーザーが Microsoft のスコープにまだ同意Graph
/auth/status
確認します。- ユーザーが同意していない場合は、ポップアップ ウィンドウを使用して、対話型ログインを通じてユーザーの同意を取得します。
- ユーザーが同意した場合は、メイン UI が読み込まれます。
- 作業ウィンドウが最初に読み込まれると、アドインの Web API のスコープが設定されたトークンを取得
ユーザーの同意を取得する
アドインが SSO を使用している場合でも、ユーザーは、Microsoft サービス経由でデータにアクセスするアドインに同意する必要Graph。 同意の取得は、1 回のプロセスです。 ユーザーが同意を与えた後、SSO トークンをユーザーの操作なしでGraphトークンと交換できます。 このセクションでは、msal-browser を使用してアドインに同意エクスペリエンス を実装します。
という名前の ./src/addin ディレクトリに新しいconsent.jsを作成し 、次の コードを追加します。
'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); } } } }
このコードはユーザーにログインし、アプリの登録で構成されている microsoft Graphアクセス許可のセットを要求します。
l という名前の ./src/addin ディレクトリに 新しいconsent.htmを作成し、次の コードを追加します。
<!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>
このコードは、ファイルを読み込む基本的な HTML ページconsent.js します。 このページはポップアップ ダイアログに読み込まれます。
変更内容をすべて保存し、サーバーを再起動します。
「アドインをサイド ロードmanifest.xml 同じ手順を使用して、アドイン ファイルを再 アップロードExcel。
[ホーム] タブの [予定表 のインポート] ボタンを選択 して作業ウィンドウを開きます。
作業ウィンドウ で [権限の付与 ] ボタンを選択して、ポップアップ ウィンドウで同意ダイアログを起動します。 サインインして同意を付与します。
作業ウィンドウは"認証済み" で更新されます。 メッセージ。 トークンは次のように確認できます。
- brower の開発者ツールでは、API トークンがコンソールに表示されます。
- サーバーを実行している CLI で、Node.jsトークンGraphされます。
これらのトークンは、 で比較できます https://jwt.ms 。 API トークンの対象ユーザー ( ) がアプリ登録のアプリケーション ID に設定され、スコープ
aud
(scp
) が .access_as_user
予定表ビューを取得する
この演習では、アプリケーションに Microsoft Graphを組み込む必要があります。 このアプリケーションでは、microsoft-graph-client ライブラリを使用して Microsoft Graph。
Outlook からカレンダー イベントを取得する
まず、API を追加して、ユーザー の予定表から 予定表ビューを取得します。
./src/api/graph.ts を開き、次のステートメントをファイルの上部
import
に追加します。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';
次の関数を追加して、Microsoft Graph SDK を初期化し、クライアントを返 します。
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!); } }); }
次の関数を追加して、ユーザーのタイム ゾーンをメールボックス設定から取得し、その値を IANA タイム ゾーン識別子に変換します。
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; }
次の関数 (行の下) を
const graphRouter = Router();
追加して、API エンドポイント ( ) を実装しますGET /graph/calendarview
。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(); } } );
このコードの動作を検討します。
- ユーザーのタイム ゾーンを取得し、それを使用して要求された予定表ビューの開始と終了を UTC 値に変換します。
- API エンドポイント
GET
のGraph/me/calendarview
します。- この関数を使用してヘッダーを設定し、返されるイベントの開始時刻と終了時刻をユーザーのタイム ゾーン
header
Prefer: outlook.timezone
に合わせて調整します。 - 関数を使用
query
してパラメーターを追加startDateTime
endDateTime
し、カレンダー ビューの開始と終了を設定します。 - この関数を
select
使用して、アドインで使用されるフィールドのみを要求します。 - 関数を使用
orderby
して、開始時刻で結果を並べ替える。 - 関数を使用
top
して、1 つの要求の結果を 25 に制限します。
- この関数を使用してヘッダーを設定し、返されるイベントの開始時刻と終了時刻をユーザーのタイム ゾーン
- PageIteratorCallback オブジェクトを使用して 結果を反復処理し、結果のページが利用可能な場合は追加の要求を行います。
UI の更新
次に、作業ウィンドウを更新して、ユーザーが予定表ビューの開始日と終了日を指定できます。
./src/addin/taskpane.js開 き、既存の関数を次のように
showMainUi
置き換える。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'); }
このコードは、ユーザーが開始日と終了日を指定できるよう、単純なフォームを追加します。 また、新しいイベントを作成するための 2 番目のフォームも実装します。 このフォームは今のところ何も行いません。次のセクションでこの機能を実装します。
次のコードをファイルに追加して、予定表ビューから取得したイベントを含むテーブルをアクティブワークシートに作成します。
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); } }); }
カレンダー ビュー API を呼び出す次の関数を追加します。
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); } }
すべての変更を保存し、サーバーを再起動し、Excel で作業ウィンドウを更新します (開いている作業ウィンドウを閉じて、再び開きます)。
開始日と終了日を選択し、[インポート] を 選択します。
新しいイベントを作成する
このセクションでは、ユーザーの予定表にイベントを作成する機能を追加します。
API の実装
./src/api/graph.ts を開き、次のコードを追加して新しいイベント API ( ) を実装します
POST /graph/newevent
。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(); } } );
./src/addin/taskpane.js開 き、次の関数を追加して新しいイベント API を呼び出します。
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); }
すべての変更を保存し、サーバーを再起動し、Excel で作業ウィンドウを更新します (開いている作業ウィンドウを閉じて、再び開きます)。
フォームに入力し、[作成] を 選択します。 イベントがユーザーの予定表に追加されるのを確認します。
おめでとうございます。
Microsoft アドインのチュートリアルOffice完了Graphしました。 Microsoft Graph を呼び出す作業用アドインが作成されたので、新しい機能を試して追加できます。 Microsoft Graphの概要を参照して、Microsoft Graphでアクセスできるすべてのデータを確認Graph。
フィードバック
このチュートリアルに関するフィードバックは、GitHubしてください。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。