使用 Microsoft Graph 构建 React Native 应用
本教程指导你生成一个React Native应用,该应用使用 Microsoft Graph API 检索用户的日历信息。
提示
如果只想下载已完成的教程,可以下载或克隆GitHub存储库。
先决条件
在开始本教程之前,应在开发计算机上安装以下内容。
您还应该有一个在 Outlook.com 上拥有邮箱的个人 Microsoft 帐户,或者一个 Microsoft 工作或学校帐户。 如果你没有 Microsoft 帐户,则有几个选项可以获取免费帐户:
- 你可以 注册新的个人 Microsoft 帐户。
- 你可以注册开发人员计划Microsoft 365免费订阅Microsoft 365订阅。
备注
本教程是使用 React Native CLI 编写的,该 CLI 具有特定的先决条件,具体取决于你的操作系统和目标平台。 有关配置React Native计算机的说明,请参阅 React Native 入门。 下面列出了用于本教程的版本。 本指南中的步骤可能与其他版本一起运行,但该版本尚未经过测试。
- Android Studio 版本 4.1 和 Android 9.0 SDK
- Java开发工具包版本 12.0.2
- Xcode 版本 12.5.1
- Node.js版本 14.15.0
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 React Native 应用
首先新建一个React Native项目。
在要创建项目的 (中) CLI 命令打开命令行接口。 运行以下命令以运行 react-native-cli 工具并创建新的React Native项目。
npx react-native init GraphTutorial --template react-native-template-typescript
可选: 通过运行项目验证您的开发环境是否正确配置。 在 CLI 中,将目录更改为你刚刚创建的 GraphTu一l 目录,并运行以下命令之一。
- 对于 iOS:
npx react-native run-ios
- 对于 Android:启动 Android 仿真器实例并运行
npx react-native run-android
- 对于 iOS:
安装依赖项
在继续之前,请安装一些你稍后将使用的附加依赖项。
- react-navigation ,用于处理应用中视图之间的导航。
- react-native-gesture-handler、react-native-safe-area-context、react-native-screens、react-native-reanimate 和 react-navigation 所需的屏蔽视图。
- react-native-elements 和 react-native-vector-icons ,用于为 UI 提供图标。
- react-native-app-auth ,用于处理身份验证和令牌管理。
- async-storage ,用于为令牌提供存储。
- datetimepicker ,用于将日期和时间选取器添加到 UI。
- 处理 日期和时间分析和比较的时间。
- 用于将时区Windows IANA 格式的 windows-iana。
- 用于调用 Microsoft Graph 的 microsoft-graph-client。
在项目根目录中打开 CLI React Native。
运行以下命令:
npm install @react-navigation/native@5.9.6 @react-navigation/drawer@5.12.7 @react-navigation/stack@5.14.7 npm install @react-native-community/masked-view@0.1.11 react-native-safe-area-context@3.3.0 windows-iana npm install react-native-reanimated@2.2.0 react-native-screens@3.5.0 @react-native-async-storage/async-storage@1.15.5 npm install react-native-elements@3.4.2 react-native-vector-icons@8.1.0 react-native-gesture-handler@1.10.3 npm install react-native-app-auth@6.4.0 moment@2.29.1 moment-timezone @microsoft/microsoft-graph-client@3.0.0 npm install @react-native-community/datetimepicker@3.5.2 npm install @microsoft/microsoft-graph-types --save-dev
链接和配置 iOS 的依赖项
备注
如果您不面向 iOS,可以跳过此部分。
在 GraphTu一l/ios 目录中打开 CLI。
运行以下命令:
pod install
在文本 编辑器中打开 GraphTutorial/ios/GraphTu一l/Info.plist 文件。 在文件的最后一行之前
</dict>
添加以下内容。<key>UIAppFonts</key> <array> <string>AntDesign.ttf</string> <string>Entypo.ttf</string> <string>EvilIcons.ttf</string> <string>Feather.ttf</string> <string>FontAwesome.ttf</string> <string>FontAwesome5_Brands.ttf</string> <string>FontAwesome5_Regular.ttf</string> <string>FontAwesome5_Solid.ttf</string> <string>Foundation.ttf</string> <string>Ionicons.ttf</string> <string>MaterialIcons.ttf</string> <string>MaterialCommunityIcons.ttf</string> <string>SimpleLineIcons.ttf</string> <string>Octicons.ttf</string> <string>Zocial.ttf</string> </array>
在文本编辑器 中打开 GraphTutorial/ios/GraphTutorial/AppDelegate.h 文件。 将其内容替换为以下内容。
#import <React/RCTBridgeDelegate.h> #import <UIKit/UIKit.h> #import "RNAppAuthAuthorizationFlowManager.h" @interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate, RNAppAuthAuthorizationFlowManager> @property (nonatomic, strong) UIWindow *window; @property (nonatomic, weak) id<RNAppAuthAuthorizationFlowManagerDelegate> authorizationFlowManagerDelegate; @end
配置 Android 的依赖项
备注
如果你未面向 Android,可以跳过此部分。
在 编辑器中打开 GraphTu一l/android/app/build.gradle 文件。
找到 条目
defaultConfig
,在 内添加以下属性defaultConfig
。manifestPlaceholders = [ appAuthRedirectScheme: 'graph-tutorial' ]
条目
defaultConfig
应如下所示。defaultConfig { applicationId "com.graphtutorial" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" manifestPlaceholders = [ appAuthRedirectScheme: 'graph-tutorial' ] }
将以下行添加到文件的末尾。
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
保存文件。
设计应用
应用程序将使用导航 箱在不同 视图之间导航。 在此步骤中,你将创建应用使用的基本视图,并实现导航箱。
创建视图
在此部分中,你将为应用创建视图以支持身份验证 流。
打开 GraphTutorial/index.js ,在任何其他语句之前,将以下内容添加到文件
import
顶部。import 'react-native-gesture-handler';
在 GraphTu一l 目录中新建一个名为 AuthContext.tsx 的文件并添加以下代码。
import * as React from 'react'; type AuthContextType = { signIn: () => Promise<void>; signOut: () => void; } export const AuthContext = React.createContext<AuthContextType>({ signIn: async () => {}, signOut: () => {} });
在 GraphTu一l 目录中新建一个名为 UserContext.tsx 的文件,并添加以下代码。
import * as React from 'react'; import { ImageSourcePropType } from 'react-native'; type UserContextType = { userLoading: boolean; userFirstName: string; userFullName: string; userEmail: string; userTimeZone: string; userPhoto: ImageSourcePropType; } export const UserContext = React.createContext<UserContextType>({ userLoading: true, userFirstName: '', userFullName: '', userEmail: '', userTimeZone: '', userPhoto: require('./images/no-profile-pic.png') });
在 GraphTu一 l 目录中新建一个名为 "屏幕"的目录。
在 GraphTu一l/screens 目录中新建一个名为 HomeScreen.tsx 的文件。 将以下代码添加到文件中。
import React from 'react'; import { ActivityIndicator, Platform, StyleSheet, Text, View, } from 'react-native'; import { createStackNavigator } from '@react-navigation/stack'; import { UserContext } from '../UserContext'; const Stack = createStackNavigator(); const HomeComponent = () => { const userContext = React.useContext(UserContext); return ( <View style={styles.container}> <ActivityIndicator color={Platform.OS === 'android' ? '#276b80' : undefined} animating={userContext.userLoading} size='large' /> {userContext.userLoading ? null: <Text>Hello {userContext.userFirstName}!</Text>} </View> ); } export default class HomeScreen extends React.Component { render() { return ( <Stack.Navigator> <Stack.Screen name='Home' component={HomeComponent} options={{ headerShown: false }} /> </Stack.Navigator> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center' } });
在名为 CalendarScreen.tsx 的 GraphTu一l/screens 目录中创建新文件。 将以下代码添加到文件中。
import React from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {createStackNavigator} from '@react-navigation/stack'; const Stack = createStackNavigator(); // Temporary placeholder view const CalendarComponent = () => ( <View style={styles.container}> <Text>Calendar</Text> </View> ); export default class CalendarScreen extends React.Component { render() { return ( <Stack.Navigator> <Stack.Screen name='Calendar' component={CalendarComponent} options={{ headerShown: false, }} /> </Stack.Navigator> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, });
在 GraphTu一l/screens 目录中新建一个名为 SignInScreen.tsx 的文件。 将以下代码添加到文件中。
// Adapted from https://reactnavigation.org/docs/auth-flow import React from 'react'; import {Alert, Button, StyleSheet, View} from 'react-native'; import {ParamListBase} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import {AuthContext} from '../AuthContext'; type SignInProps = { navigation: StackNavigationProp<ParamListBase>; }; export default class SignInScreen extends React.Component<SignInProps> { static contextType = AuthContext; _signInAsync = async () => { await this.context.signIn(); }; componentDidMount() { this.props.navigation.setOptions({ title: 'Please sign in', headerShown: true, }); } render() { return ( <View style={styles.container}> <Button title='Sign In' onPress={this._signInAsync} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, });
在 GraphTu一l/screens 目录中新建一个名为 AuthLoadingScreen.tsx 的文件。 将以下代码添加到文件中。
// Adapted from https://reactnavigation.org/docs/auth-flow import React from 'react'; import { ActivityIndicator, Platform, Text, StyleSheet, View, } from 'react-native'; export default class AuthLoadingScreen extends React.Component { render() { return ( <View style={styles.container}> <ActivityIndicator color={Platform.OS === 'android' ? '#276b80' : undefined} size='large' /> <Text style={styles.statusText}>Logging in...</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, statusText: { marginTop: 10 } });
创建导航箱
在此部分中,你将为应用程序创建菜单,并更新应用程序以使用 react-navigation 在屏幕之间移动。
在 GraphTu一l 目录中新建一个名为 menus 的目录。
在 GraphTu一l/menus 目录中新建一个名为 DrawerMenu.tsx 的文件。 将以下代码添加到文件中。
import React, {FC} from 'react'; import { Alert, Image, StyleSheet, Text, View, ImageSourcePropType, } from 'react-native'; import { createDrawerNavigator, DrawerContentScrollView, DrawerItem, DrawerItemList, DrawerContentComponentProps, } from '@react-navigation/drawer'; import {ParamListBase} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import {AuthContext} from '../AuthContext'; import {UserContext} from '../UserContext'; import HomeScreen from '../screens/HomeScreen'; import CalendarScreen from '../screens/CalendarScreen'; const Drawer = createDrawerNavigator(); type CustomDrawerContentProps = DrawerContentComponentProps & { userName: string; userEmail: string; userPhoto: ImageSourcePropType; signOut: () => void; }; type DrawerMenuProps = { navigation: StackNavigationProp<ParamListBase>; }; const CustomDrawerContent: FC<CustomDrawerContentProps> = props => ( <DrawerContentScrollView {...props}> <View style={styles.profileView}> <Image source={props.userPhoto} resizeMode='contain' style={styles.profilePhoto} /> <Text style={styles.profileUserName}>{props.userName}</Text> <Text style={styles.profileEmail}>{props.userEmail}</Text> </View> <DrawerItemList {...props} /> <DrawerItem label='Sign Out' onPress={props.signOut} /> </DrawerContentScrollView> ); export default class DrawerMenuContent extends React.Component<DrawerMenuProps> { static contextType = AuthContext; state = { // TEMPORARY userLoading: true, userFirstName: 'Adele', userFullName: 'Adele Vance', userEmail: 'adelev@contoso.com', userTimeZone: 'UTC', userPhoto: require('../images/no-profile-pic.png'), }; _signOut = async () => { this.context.signOut(); }; async componentDidMount() { this.props.navigation.setOptions({ headerShown: false, }); } render() { const userLoaded = !this.state.userLoading; return ( <UserContext.Provider value={this.state}> <Drawer.Navigator drawerType='front' screenOptions={{ headerShown: true, headerStyle: { backgroundColor: '#276b80', }, headerTintColor: 'white', }} drawerContent={props => ( <CustomDrawerContent {...props} userName={this.state.userFullName} userEmail={this.state.userEmail} userPhoto={this.state.userPhoto} signOut={this._signOut} /> )}> <Drawer.Screen name='Home' component={HomeScreen} options={{drawerLabel: 'Home', headerTitle: 'Welcome'}} /> {userLoaded && ( <Drawer.Screen name='Calendar' component={CalendarScreen} options={{drawerLabel: 'Calendar'}} /> )} </Drawer.Navigator> </UserContext.Provider> ); } } const styles = StyleSheet.create({ container: { flex: 1, }, profileView: { alignItems: 'center', padding: 10, }, profilePhoto: { width: 80, height: 80, borderRadius: 40, }, profileUserName: { fontWeight: '700', }, profileEmail: { fontWeight: '200', fontSize: 10, }, });
在 GraphTu一l 目录中新建一个名为 images 的目录。
在此目录中添加名为 no-profile-pic.png 的默认配置文件映像。 可以使用任何喜欢的图像,也可以使用此 示例中的图像。
打开 GraphTu一l/App.tsx 文件,将全部内容替换为以下内容。
// Adapted from https://reactnavigation.org/docs/auth-flow import * as React from 'react'; import {NavigationContainer, ParamListBase} from '@react-navigation/native'; import { createStackNavigator, StackNavigationProp, } from '@react-navigation/stack'; import {AuthContext} from './AuthContext'; import SignInScreen from './screens/SignInScreen'; import DrawerMenuContent from './menus/DrawerMenu'; import AuthLoadingScreen from './screens/AuthLoadingScreen'; const Stack = createStackNavigator(); type Props = { navigation: StackNavigationProp<ParamListBase>; }; export default function App({navigation}: Props) { const [state, dispatch] = React.useReducer( (prevState: any, action: any) => { switch (action.type) { case 'RESTORE_TOKEN': return { ...prevState, userToken: action.token, isLoading: false, }; case 'SIGN_IN': return { ...prevState, isSignOut: false, userToken: action.token, }; case 'SIGN_OUT': return { ...prevState, isSignOut: true, userToken: null, }; } }, { isLoading: true, isSignOut: false, userToken: null, }, ); React.useEffect(() => { const bootstrapAsync = async () => { let userToken = null; // TEMPORARY dispatch({type: 'RESTORE_TOKEN', token: userToken}); }; bootstrapAsync(); }, []); const authContext = React.useMemo( () => ({ signIn: async () => { dispatch({type: 'SIGN_IN', token: 'placeholder-token'}); }, signOut: async () => { dispatch({type: 'SIGN_OUT'}); }, }), [], ); return ( <AuthContext.Provider value={authContext}> <NavigationContainer> <Stack.Navigator> {state.isLoading ? ( <Stack.Screen name='Loading' component={AuthLoadingScreen} /> ) : state.userToken == null ? ( <Stack.Screen name='SignIn' component={SignInScreen} /> ) : ( <Stack.Screen name='Main' component={DrawerMenuContent} /> )} </Stack.Navigator> </NavigationContainer> </AuthContext.Provider> ); }
保存所有更改。
在仿真器中重新加载应用程序。
应用的菜单应该可以在两个片段之间导航,并在点击"登录"或"注销"按钮 时 更改。
在门户中注册该应用
在此练习中,你将使用管理中心Azure Active Directory Azure AD 本机应用程序。
打开浏览器,并转到 Azure Active Directory 管理中心。然后,使用 个人帐户(亦称为“Microsoft 帐户”)或 工作或学校帐户 登录。
选择左侧导航栏中的“Azure Active Directory”,再选择“管理”下的“应用注册”。
选择“新注册”。 在“注册应用”页上,按如下方式设置值。
- 将“名称”设置为“
React Native Graph Tutorial
”。 - 将“受支持的帐户类型”设置为“任何组织目录中的帐户和个人 Microsoft 帐户”。
- 在 "重定向 URI" 下,将下拉列表更改为"公共客户端 (移动&桌面) ", 将值设置为
graph-tutorial://react-native-auth/
。
- 将“名称”设置为“
选择“注册”。 在 "React Native Graph教程"页上,复制 Application (client) ID 的值并将其保存,你将在下一步中需要该值。
添加 Azure AD 身份验证
在此练习中,你将扩展上一练习中的应用程序,以支持使用 Azure AD。 这是必需的,才能获取必要的 OAuth 访问令牌来调用 Microsoft Graph。 为此,需要将 react-native-app-auth 库 集成到应用程序中。
在 GraphTu一l 目录中新建一个名为 auth 的目录。
在 GraphTu一l/auth 目录中新建一个名为 AuthConfig.ts 的文件。 将以下代码添加到文件中。
export const AuthConfig = { appId: 'YOUR_APP_ID_HERE', appScopes: [ 'openid', 'offline_access', 'profile', 'User.Read', 'MailboxSettings.Read', 'Calendars.ReadWrite' ] };
将
YOUR_APP_ID_HERE
替换为应用注册中的应用 ID。
重要
如果你使用的是源代码管理(如 git),那么现在应该从源代码管理中排除 AuthConfig.ts 文件,以避免意外泄露应用 ID。
实施登录
在此部分中,你将创建身份验证帮助程序类,并更新应用以登录和注销。
在 GraphTu一l/auth 目录中新建一个名为 AuthManager.ts 的文件。 将以下代码添加到文件中。
import AsyncStorage from '@react-native-async-storage/async-storage'; import { authorize, refresh, AuthConfiguration } from 'react-native-app-auth'; import { Platform } from 'react-native'; import moment from 'moment'; import { AuthConfig } from './AuthConfig'; const config: AuthConfiguration = { clientId: AuthConfig.appId, redirectUrl: 'graph-tutorial://react-native-auth/', scopes: AuthConfig.appScopes, additionalParameters: { prompt: 'select_account' }, serviceConfiguration: { authorizationEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', } }; export class AuthManager { static signInAsync = async () => { const result = await authorize(config); console.log(result.accessToken); // Store the access token, refresh token, and expiration time in storage await AsyncStorage.setItem('userToken', result.accessToken); await AsyncStorage.setItem('refreshToken', result.refreshToken); await AsyncStorage.setItem('expireTime', result.accessTokenExpirationDate); } static signOutAsync = async () => { // Clear storage await AsyncStorage.removeItem('userToken'); await AsyncStorage.removeItem('refreshToken'); await AsyncStorage.removeItem('expireTime'); } static getAccessTokenAsync = async() => { const expireTime = await AsyncStorage.getItem('expireTime'); if (expireTime !== null) { // Get expiration time - 5 minutes // If it's <= 5 minutes before expiration, then refresh const expire = moment(expireTime).subtract(5, 'minutes'); const now = moment(); if (now.isSameOrAfter(expire)) { // Expired, refresh console.log('Refreshing token'); const refreshToken = await AsyncStorage.getItem('refreshToken'); console.log(`Refresh token: ${refreshToken}`); const result = await refresh(config, { refreshToken: refreshToken || '' }); // Store the new access token, refresh token, and expiration time in storage await AsyncStorage.setItem('userToken', result.accessToken); await AsyncStorage.setItem('refreshToken', result.refreshToken || ''); await AsyncStorage.setItem('expireTime', result.accessTokenExpirationDate); return result.accessToken; } // Not expired, just return saved access token const accessToken = await AsyncStorage.getItem('userToken'); return accessToken; } return null; } }
打开 GraphTu一l/App.tsx 文件,将以下
import
语句添加到文件顶部。import {AuthManager} from './auth/AuthManager';
将现有声明
authContext
替换为以下内容。const authContext = React.useMemo( () => ({ signIn: async () => { await AuthManager.signInAsync(); const token = await AuthManager.getAccessTokenAsync(); dispatch({ type: 'SIGN_IN', token: token }); }, signOut: async () => { await AuthManager.signOutAsync(); dispatch({ type: 'SIGN_OUT' }); } }), [] );
打开 GraphTu一l/menus/DrawerMenu.tsx
import
文件,将以下语句添加到文件顶部。import {AuthManager} from '../auth/AuthManager';
将以下代码添加到
componentDidMount
函数。try { const accessToken = await AuthManager.getAccessTokenAsync(); // TEMPORARY this.setState({userFirstName: accessToken, userLoading: false}); } catch (error) { Alert.alert( 'Error getting token', JSON.stringify(error), [ { text: 'OK' } ], { cancelable: false } ); }
保存更改,在仿真器中重新加载应用程序。
如果登录应用,应该会看到访问令牌显示在欢迎 屏幕上 。
获取用户详细信息
在此部分中,您将为 Graph 客户端库创建自定义身份验证提供程序,创建一个帮助程序类来保存对 Microsoft Graph 的所有调用,DrawerMenuContent
并更新该类以使用此新类获取登录用户。
在 GraphTu一l 目录中新建一个名为 graph 的目录。
在 GraphTu一l/graph 目录中新建一个名为 GraphAuthProvider.ts 的文件。 将以下代码添加到文件中。
import { AuthManager } from '../auth/AuthManager'; // Used by Graph client to get access tokens // See https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CustomAuthenticationProvider.md export class GraphAuthProvider { getAccessToken = async() => { const token = await AuthManager.getAccessTokenAsync(); return token || ''; } }
在 GraphTu一l/graph 目录中新建一个名为 GraphManager.ts 的文件。 将以下代码添加到文件中。
import {Client} from '@microsoft/microsoft-graph-client'; import {GraphAuthProvider} from './GraphAuthProvider'; // Set the authProvider to an instance // of GraphAuthProvider const clientOptions = { authProvider: new GraphAuthProvider(), }; // Initialize the client const graphClient = Client.initWithMiddleware(clientOptions); export class GraphManager { static getUserAsync = async () => { // GET /me return await graphClient .api('/me') .select('displayName,givenName,mail,mailboxSettings,userPrincipalName') .get(); }; }
打开 GraphTu一l/views/DrawerMenu.tsx 文件,将以下
import
语句添加到文件顶部。import {GraphManager} from '../graph/GraphManager'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
componentDidMount
将 方法替换为以下内容。async componentDidMount() { this.props.navigation.setOptions({ headerShown: false, }); try { // Get the signed-in user from Graph const user: MicrosoftGraph.User = await GraphManager.getUserAsync(); // Update UI with display name and email this.setState({ userLoading: false, userFirstName: user.givenName!, userFullName: user.displayName!, // Work/School accounts have email address in mail attribute // Personal accounts have it in userPrincipalName userEmail: user.mail! || user.userPrincipalName!, userTimeZone: user.mailboxSettings?.timeZone! }); } catch(error) { Alert.alert( 'Error getting user', JSON.stringify(error), [ { text: 'OK' } ], { cancelable: false } ); } }
如果保存更改并立即重新加载应用,登录 UI 后,会使用用户的 显示名称 和电子邮件地址进行更新。
获取日历视图
在此练习中,你将 Microsoft Graph应用程序。 对于此应用程序,你将使用 Microsoft Graph JavaScript 客户端库调用 Microsoft Graph。
从 Outlook 获取日历事件
在此部分中,你将扩展 类 GraphManager
以添加一个函数,以获取用户本周的事件,并更新 CalendarScreen
为使用这些新函数。
打开 GraphTu一l/graph/GraphManager.tsx 文件,将以下方法添加到
GraphManager
类。static getCalendarView = async(start: string, end: string, timezone: string) => { // GET /me/calendarview return await graphClient.api('/me/calendarview') .header('Prefer', `outlook.timezone="${timezone}"`) .query({ startDateTime: start, endDateTime: end}) // $select='subject,organizer,start,end' // Only return these fields in results .select('subject,organizer,start,end') // $orderby=createdDateTime DESC // Sort results by when they were created, newest first .orderby('start/dateTime') .top(50) .get(); }
备注
考虑代码正在
getCalendarView
执行哪些工作。- 将调用的 URL 为
/v1.0/me/calendarView
。 - 函数
header
将 标头Prefer: outlook.timezone
添加到请求中,导致响应中的时间在用户的首选时区。 - 函数
query
添加 和startDateTime
endDateTime
参数,定义日历视图的时间窗口。 - 函数
select
将每个事件返回的字段限定为应用将实际使用的字段。 - 函数
orderby
按开始时间对结果进行排序。 - 函数
top
将结果限制到前 50 个事件。
- 将调用的 URL 为
打开 GraphTu一l/views/CalendarScreen.tsx ,并将其全部内容替换为以下代码。
import React from 'react'; import { ActivityIndicator, Alert, FlatList, Modal, Platform, ScrollView, StyleSheet, Text, View, } from 'react-native'; import {createStackNavigator} from '@react-navigation/stack'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import moment from 'moment-timezone'; import {findIana} from 'windows-iana'; import {UserContext} from '../UserContext'; import {GraphManager} from '../graph/GraphManager'; const Stack = createStackNavigator(); const CalendarState = React.createContext<CalendarScreenState>({ loadingEvents: true, events: [], }); type CalendarScreenState = { loadingEvents: boolean; events: MicrosoftGraph.Event[]; }; // Temporary JSON view const CalendarComponent = () => { const calendarState = React.useContext(CalendarState); return ( <View style={styles.container}> <Modal visible={calendarState.loadingEvents}> <View style={styles.loading}> <ActivityIndicator color={Platform.OS === 'android' ? '#276b80' : undefined} animating={calendarState.loadingEvents} size='large' /> </View> </Modal> <ScrollView> <Text>{JSON.stringify(calendarState.events, null, 2)}</Text> </ScrollView> </View> ); }; export default class CalendarScreen extends React.Component { static contextType = UserContext; state: CalendarScreenState = { loadingEvents: true, events: [], }; async componentDidMount() { try { const tz = this.context.userTimeZone || 'UTC'; // Convert user's Windows time zone ("Pacific Standard Time") // to IANA format ("America/Los_Angeles") // Moment.js needs IANA format const ianaTimeZone = findIana(tz)[0]; // Get midnight on the start of the current week in the user's // time zone, but in UTC. For example, for PST, the time value // would be 07:00:00Z const startOfWeek = moment .tz(ianaTimeZone!.valueOf()) .startOf('week') .utc(); const endOfWeek = moment(startOfWeek).add(7, 'day'); const events = await GraphManager.getCalendarView( startOfWeek.format(), endOfWeek.format(), tz, ); this.setState({ loadingEvents: false, events: events.value, }); } catch (error) { Alert.alert( 'Error getting events', JSON.stringify(error), [ { text: 'OK', }, ], {cancelable: false}, ); } } render() { return ( <CalendarState.Provider value={this.state}> <Stack.Navigator> <Stack.Screen name='Calendar' component={CalendarComponent} options={{ headerShown: false, }} /> </Stack.Navigator> </CalendarState.Provider> ); } } const styles = StyleSheet.create({ container: { flex: 1, }, loading: { flex: 1, justifyContent: 'center', alignItems: 'center', }, eventItem: { padding: 10, }, eventSubject: { fontWeight: '700', fontSize: 18, }, eventOrganizer: { fontWeight: '200', }, eventDuration: { fontWeight: '200', }, });
现在,你可以运行应用、登录并点击菜单中的 "日历 "导航项。 你应该会看到应用中事件的 JSON 转储。
显示结果
现在,可以将 JSON 转储替换为某些内容,以用户友好的方式显示结果。 在此部分中,您将向日历 FlatList
屏幕添加 以呈现事件。
在类声明 上方添加
CalendarScreen
以下方法。const convertDateTime = (dateTime: string): string => { return moment(dateTime).format('MMM Do H:mm a'); };
ScrollView
将 方法中的CalendarComponent
替换为以下内容。<FlatList data={calendarState.events} renderItem={({item}) => ( <View style={styles.eventItem}> <Text style={styles.eventSubject}>{item.subject}</Text> <Text style={styles.eventOrganizer}> {item.organizer!.emailAddress!.name} </Text> <Text style={styles.eventDuration}> {convertDateTime(item.start!.dateTime!)} -{' '} {convertDateTime(item.end!.dateTime!)} </Text> </View> )} />
运行应用、登录,然后点击 "日历" 导航项。 你应该会看到事件列表。
创建新事件
在此部分中,您将添加在用户日历上创建事件的能力。
创建新事件屏幕
打开 ./graph/GraphManager.ts ,将以下函数添加到
GraphManager
类。static createEvent = async(newEvent: any) => { // POST /me/events await graphClient.api('/me/events') .post(newEvent); }
此函数使用 Graph SDK 创建新事件。
在名为 NewEventScreen.tsx 的 ./screens 中创建新文件并添加以下代码。
import React from 'react'; import { ActivityIndicator, Alert, Button, Modal, Platform, ScrollView, StyleSheet, Text, View, } from 'react-native'; import DateTimePicker from '@react-native-community/datetimepicker'; import { createStackNavigator } from '@react-navigation/stack'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import moment from 'moment-timezone'; import { UserContext } from '../UserContext'; import { GraphManager } from '../graph/GraphManager'; import { TextInput } from 'react-native-gesture-handler'; const Stack = createStackNavigator(); const NewEventState = React.createContext<NewEventState>({ isCreating: false, subject: '', attendees: '', body: '', timeZone: '', startDate: new Date(), endDate: new Date(), disableCreate: () => { return true }, updateValue: () => {} }); type NewEventState = { isCreating: boolean; subject: string; attendees: string; body: string; timeZone: string; startDate: Date; endDate: Date; disableCreate: () => boolean; updateValue: (newValue: string | Date | boolean, fieldName: string) => void; } type DateTimeInputProps = { value: Date; onChange: (event: Event, newValue: Date | undefined) => void; } // The picker acts very differently on Android and iOS // iOS can use a single picker for both date and time, // where Android requires two. Also the iOS version can // be displayed all the time, while the Android version is a // modal pop-up. Encapsulating this into a reusable component const DateTimeInput = (props: DateTimeInputProps) => { const [showDatePicker, setShowDatePicker] = React.useState(false); const [showTimePicker, setShowTimePicker] = React.useState(Platform.OS === 'ios'); return ( <View style={Platform.OS === 'android' ? styles.dateTime : null}> { Platform.OS === 'android' && <Text style={styles.time} onPress={()=>{setShowTimePicker(true)}}> {formatTime(props.value)} </Text> } { showTimePicker && <DateTimePicker mode={Platform.OS === 'ios' ? 'datetime' : 'time'} value={props.value} onChange={(e, d) => { setShowTimePicker(Platform.OS === 'ios'); if (d) props.onChange(e,d); }} /> } { Platform.OS === 'android' && <Text style={styles.date} onPress={()=>{setShowDatePicker(true)}}> {formatDate(props.value)} </Text> } { showDatePicker && Platform.OS === 'android' && <DateTimePicker mode='date' value={props.value} onChange={(e, d) => { setShowDatePicker(Platform.OS === 'ios'); if (d) props.onChange(e,d); }} /> } </View> ) } const NewEventComponent = () => { const newEventState = React.useContext(NewEventState); const createEvent = async () => { newEventState.updateValue(true, 'isCreating'); // Create a new Event object with the // required fields const newEvent: MicrosoftGraph.Event = { subject: newEventState.subject, start: { dateTime: moment(newEventState.startDate).format('YYYY-MM-DDTHH:mm:ss'), timeZone: newEventState.timeZone }, end: { dateTime: moment(newEventState.endDate).format('YYYY-MM-DDTHH:mm:ss'), timeZone: newEventState.timeZone } }; // Only add attendees if the user specified them if (newEventState.attendees.length > 0) { newEvent.attendees = []; // Value should be a ;-delimited list of email addresses // NOTE: The app does no validation of this const emails = newEventState.attendees.split(';') emails.forEach((email) => { newEvent.attendees!.push({ emailAddress: { address: email } }); }); } // Only add body if the user specified one if (newEventState.body.length > 0) { newEvent.body = { content: newEventState.body, // For simplicity, add it as a plain-text body contentType: 'text' }; } await GraphManager.createEvent(newEvent); Alert.alert('Success', 'Event created', [ { text: 'OK', onPress: () => { newEventState.updateValue(false, 'isCreating'); } } ] ); } return ( <ScrollView style={styles.container}> <Modal visible={newEventState.isCreating}> <View style={styles.loading}> <ActivityIndicator color={Platform.OS === 'android' ? '#276b80' : undefined} animating={newEventState.isCreating} size='large' /> </View> </Modal> <View style={styles.formField}> <Text style={styles.fieldLabel}>Subject</Text> <TextInput style={styles.textInput} value={newEventState.subject} onChangeText={(text) => newEventState.updateValue(text, 'subject')} /> </View> <View style={styles.formField}> <Text style={styles.fieldLabel}>Attendees</Text> <TextInput style={styles.textInput} placeholder="Email (separate multiple with ';')" value={newEventState.attendees} onChangeText={(text) => newEventState.updateValue(text, 'attendees')} /> </View> <View style={styles.formField}> <Text style={styles.fieldLabel}>Start</Text> <DateTimeInput value={newEventState.startDate} onChange={(e, date) => newEventState.updateValue(date!, 'startDate')} /> </View> <View style={styles.formField}> <Text style={styles.fieldLabel}>End</Text> <DateTimeInput value={newEventState.endDate} onChange={(e, date) => newEventState.updateValue(date!, 'endDate')} /> </View> <View style={styles.formField}> <TextInput style={styles.multiLineTextInput} multiline={true} textAlignVertical='top' placeholder='Body' value={newEventState.body} onChangeText={(text) => newEventState.updateValue(text, 'body')} /> </View> <View style={styles.formField}> <Button title="Create" disabled={newEventState.disableCreate()} onPress={createEvent}/> </View> </ScrollView> ); } const formatTime = (dateTime: Date): string => { return moment(dateTime).format('h:mm A'); } const formatDate = (dateTime: Date): string => { return moment(dateTime).format('MMM D, YYYY'); } // When first loading the form, set the start time // to the nearest hour or half-hour const getDefaultStart = (): Date => { const now = moment().startOf('minute'); const offset = 30 - (now.minute() % 30); return now.add(offset, 'minutes').toDate(); } // When first loading the form, set the end time // to start + 30 min const getDefaultEnd = (): Date => { return moment(getDefaultStart()).add(30, 'minutes').toDate(); } export default class NewEventScreen extends React.Component { static contextType = UserContext; // Disable the create button if: // - App is waiting for the result of create request // - Subject is empty // - Start time is after end time disableCreate = () => { return this.state.isCreating || this.state.subject.length <= 0 || moment(this.state.startDate).isAfter(this.state.endDate); } onStateValueChange = (newValue: string | Date | boolean, fieldName: string) => { this.setState({ [fieldName]: newValue }); } state: NewEventState = { isCreating: false, subject: '', attendees: '', body: '', timeZone: this.context.userTimeZone, startDate: getDefaultStart(), endDate: getDefaultEnd(), disableCreate: this.disableCreate, updateValue: this.onStateValueChange }; render() { return ( <NewEventState.Provider value={this.state}> <Stack.Navigator> <Stack.Screen name='NewEvent' component={ NewEventComponent } options={{ headerShown: false }} /> </Stack.Navigator> </NewEventState.Provider> ); } } const styles = StyleSheet.create({ container: { flex: 1 }, loading: { flex: 1, justifyContent: 'center', alignItems: 'center' }, formField: { paddingHorizontal: 10, paddingVertical: 5 }, fieldLabel: { fontWeight: '700', marginBottom: 10 }, textInput: { borderColor: 'gray', borderWidth: 1, height: 40, padding: 10 }, multiLineTextInput: { borderColor: 'gray', borderWidth: 1, height: 200, padding: 10 }, time: { padding: 10, backgroundColor: '#e6e6e6', color: '#147efb', marginRight: 10 }, date: { padding: 10, backgroundColor: '#e6e6e6', color: '#147efb' }, dateTime: { flexDirection: 'row' } });
考虑函数
createEvent
的功能。 它使用表单MicrosoftGraph.Event
中的值创建对象,然后将该对象传递给GraphManager.createEvent
函数。打开 ./menus/DrawerMenu.tsx ,
import
在文件顶部添加以下语句。import NewEventScreen from '../screens/NewEventScreen';
在 元素内添加以下
<Drawer.Navigator>
代码,位于 行的正</Drawer.Navigator>
上方。{userLoaded && ( <Drawer.Screen name='NewEvent' component={NewEventScreen} options={{drawerLabel: 'New event'}} /> )}
保存更改并重新启动或刷新应用。 选择 菜单上的 "新建事件"选项以访问新事件表单。
填写表单并选择"创建 "。
恭喜!
已完成 Microsoft React Native Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,你可以试验和添加新功能。 请访问Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。