

# IVS 聊天功能客户端消息收发 SDK：React Native 教程第 1 部分：聊天室
<a name="chat-sdk-react-tutorial-chat-rooms"></a>

这是一个由两部分组成的教程的第 1 部分。您将通过使用 React Native 构建功能齐全的应用程序，了解使用 Amazon IVS 聊天功能客户端消息收发 JavaScript SDK 的基本知识。我们把这个应用程序称为 *Chatterbox*。

目标受众是刚接触 Amazon IVS Chat 消息收发 SDK 的经验丰富的开发人员。您需要熟悉 TypeScript 或 JavaScript 编程语言以及 React Native 库。

为简洁起见，我们将 Amazon IVS Chat 客户端消息收发 JavaScript SDK 称为 Chat JS SDK。

**注意**：在某些情况下，JavaScript 和 TypeScript 的代码示例是相同的，因此我们将它们合并在一起。

本教程的第 1 部分分为几个章节：

1. [设置本地身份验证/授权服务器](#chat-react-rooms-auth-server)

1. [创建 Chatterbox 项目](#chat-react-rooms-chatterbox)

1. [连接到聊天室](#chat-react-rooms-connect)

1. [构建令牌提供程序](#chat-react-rooms-token-provider)

1. [观察连接更新](#chat-react-rooms-connection-state)

1. [创建发送按钮组件](#chat-react-rooms-send-button)

1. [创建消息输入](#chat-react-rooms-message-input)

1. [后续步骤](#chat-react-rooms-next-steps)

## 先决条件
<a name="chat-react-rooms-prerequisites"></a>
+ 熟悉 TypeScript 或 JavaScript 以及 React Native 库。如果您不熟悉 React Native，请访问 [Intro to React Native](https://reactnative.dev/docs/tutorial)（React Native 简介）学习基础知识。
+ 阅读并理解 [Amazon IVS Chat 入门](getting-started-chat.md)。
+ 使用现有的 IAM policy 中定义的 CreateChatToken 和 CreateRoom 功能创建 AWS IAM 用户。（请参见 [Amazon IVS Chat 入门](getting-started-chat.md)。）
+ 确保该用户的私有密钥/访问密钥存储在 Amazon 凭证文件中。有关说明，请参阅 [Amazon CLI 用户指南](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)（尤其是[配置和凭证文件设置](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)部分）。
+ 创建聊天室并保存其 ARN。请参阅[Amazon IVS Chat 入门](getting-started-chat.md)。（如果不保存 ARN，您可以稍后使用控制台或 Chat API 查找 ARN。）
+ 使用 NPM 或 Yarn 程序包管理器安装 Node.js 14\$1 环境。

## 设置本地身份验证/授权服务器
<a name="chat-react-rooms-auth-server"></a>

您的后端应用程序负责创建聊天室和生成 Chat JS SDK 所需的聊天令牌，以便在您的客户端连接聊天室时进行身份验证和授权。您必须使用自己的后端，因为您无法在移动应用程序中安全地存储 Amazon 密钥；老练的攻击者可以提取这些密钥并获得对您的 Amazon 账户的访问权限。

请参阅*《Amazon IVS Chat 入门》*中的[创建聊天令牌](getting-started-chat-auth.md)。如其中的流程图所示，您的服务器端应用程序负责创建聊天令牌。这意味着您的应用程序必须通过向服务器端应用程序请求聊天令牌，来提供自己生成聊天令牌的方法。

在本节中，您将学习在后端创建令牌提供程序的基础知识。我们使用 Express 框架创建一个实时本地服务器，该服务器使用您的本地 Amazon 环境管理聊天令牌的创建。

使用 NPM 创建一个空的 `npm` 项目。创建一个用于存放应用程序的目录，并将其设为您的工作目录：

```
$ mkdir backend & cd backend
```

使用 `npm init` 为您的应用程序创建 `package.json` 文件：

```
$ npm init
```

此命令会提示您某些信息，包括您的应用程序的名称和版本。现在，只需按下 **RETURN** 即可接受其中大多数的默认值，但以下情况除外：

```
entry point: (index.js)
```

按下 **RETURN** 接受建议的默认文件名 `index.js`，或者输入任何您想要的主文件名。

现在安装所需的依赖项：

```
$ npm install express aws-sdk cors dotenv
```

`aws-sdk` 需要配置环境变量，这些变量会自动从根目录中名为 `.env` 的文件加载。要对其进行配置，请创建一个名为 `.env` 的新文件并填写缺少的配置信息：

```
# .env

# The region to send service requests to.
AWS_REGION=us-west-2

# Access keys use an access key ID and secret access key
# that you use to sign programmatic requests to AWS.

# AWS access key ID.
AWS_ACCESS_KEY_ID=...

# AWS secret access key.
AWS_SECRET_ACCESS_KEY=...
```

现在，我们在根目录中创建一个入口点文件，其名称与您在上面的 `npm init` 命令中输入的名称相同。在本例中，我们使用 `index.js` 并导入所有必需的程序包：

```
// index.js
import express from 'express';
import AWS from 'aws-sdk';
import 'dotenv/config';
import cors from 'cors';
```

现在创建一个新的 `express` 实例：

```
const app = express();
const port = 3000;

app.use(express.json());
app.use(cors({ origin: ['http://127.0.0.1:5173'] }));
```

之后，您可以为令牌提供程序创建您的第一个端点 POST 方法。从请求正文中获取所需的参数（`roomId`、`userId`、`capabilities` 和 `sessionDurationInMinutes`）：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};
});
```

添加必填字段的验证：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`' });
    return;
  }
});
```

准备好 POST 方法后，我们将 `createChatToken` 与 `aws-sdk` 进行集成，以实现身份验证/授权的核心功能：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId || !capabilities) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`, `capabilities`' });
    return;
  }

  ivsChat.createChatToken({ roomIdentifier, userId, capabilities, sessionDurationInMinutes }, (error, data) => {
    if (error) {
      console.log(error);
      res.status(500).send(error.code);
    } else if (data.token) {
      const { token, sessionExpirationTime, tokenExpirationTime } = data;
      console.log(`Retrieved Chat Token: ${JSON.stringify(data, null, 2)}`);

      res.json({ token, sessionExpirationTime, tokenExpirationTime });
    }
  });
});
```

在文件末尾，为您的 `express` 应用程序添加一个端口侦听器：

```
app.listen(port, () => {
  console.log(`Backend listening on port ${port}`);
});
```

现在，您可以从项目的根目录使用以下命令运行服务器：

```
$ node index.js
```

**提示**：此服务器在 https://localhost:3000 接受 URL 请求。

## 创建 Chatterbox 项目
<a name="chat-react-rooms-chatterbox"></a>

首先创建名为 `chatterbox` 的 React Native 项目。运行以下命令：

```
npx create-expo-app
```

或者使用 TypeScript 模板创建一个 expo 项目。

```
npx create-expo-app -t expo-template-blank-typescript
```

您可以通过[节点程序包管理器](https://www.npmjs.com/)或 [Yarn 程序包管理器](https://yarnpkg.com/)集成 Chat 客户端消息收发 JS SDK：
+ Npm：`npm install amazon-ivs-chat-messaging`
+ Yarn：`yarn add amazon-ivs-chat-messaging`

## 连接到聊天室
<a name="chat-react-rooms-connect"></a>

在这里，您可以创建 `ChatRoom` 并使用异步方法对其进行连接。`ChatRoom` 类负责管理您的用户与 Chat JS SDK 的连接。要成功连接到聊天室，您必须在 React 应用程序中提供一个 `ChatToken` 实例。

导航到在默认 `chatterbox` 项目中创建的 `App` 文件，并删除功能组件返回的所有内容。不需要任何预填充的代码。此时，我们的 `App` 还很空。

**TypeScript/JavaScript**：

```
// App.tsx / App.jsx

import * as React from 'react';
import { Text } from 'react-native';

export default function App() {
  return <Text>Hello!</Text>;
}
```

创建一个新的 `ChatRoom` 实例并使用 `useState` 钩子将其传递给状态。该实例需要传递 `regionOrUrl`（托管聊天室的亚马逊云科技区域）和 `tokenProvider`（用于后续步骤中创建的后端身份验证/授权流程）。

**重要提示**：您必须使用与您在 [Amazon IVS Chat 入门](getting-started-chat-create-room.md)中创建聊天室的区域相同的亚马逊云科技区域。该 API 是一项亚马逊云科技区域服务。有关支持的区域和 Amazon IVS Chat HTTPS 服务终端节点的列表，请参阅 [Amazon IVS Chat 区域](https://docs.aws.amazon.com/general/latest/gr/ivs.html#ivs_region)页面。

**TypeScript/JavaScript**：

```
// App.jsx / App.tsx

import React, { useState } from 'react';
import { Text } from 'react-native';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [room] = useState(() =>
    new ChatRoom({
      regionOrUrl: process.env.REGION,
      tokenProvider: () => {},
    }),
  );

  return <Text>Hello!</Text>;
}
```

## 构建令牌提供程序
<a name="chat-react-rooms-token-provider"></a>

下一步，我们需要构建 `ChatRoom` 构造函数所需的无参数 `tokenProvider` 函数。首先，我们将创建一个 `fetchChatToken` 函数，该函数将向您在 [设置本地身份验证/授权服务器](#chat-react-rooms-auth-server) 中设置的后端应用程序发出 POST 请求。聊天令牌包含该 SDK 成功建立聊天室连接所需的信息。Chat API 使用这些令牌作为验证用户身份、聊天室中的功能和会话持续时间的安全方式。

在项目导航器中，创建一个名为 `fetchChatToken` 的新 TypeScript/JavaScript 文件。构建对 `backend` 应用程序的提取请求，并从响应中返回 `ChatToken` 对象。添加创建聊天令牌所需的请求正文属性。使用为 [Amazon 资源名称 (ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) 定义的规则。这些属性记录在 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 操作中。

**注意**：您在此处使用的 URL 与运行后端应用程序时本地服务器创建的 URL 相同。

------
#### [ TypeScript ]

```
// fetchChatToken.ts

import { ChatToken } from 'amazon-ivs-chat-messaging';

type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE';

export async function fetchChatToken(
  userId: string,
  capabilities: UserCapability[] = [],
  attributes?: Record<string, string>,
  sessionDurationInMinutes?: number,
): Promise<ChatToken> {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------
#### [ JavaScript ]

```
// fetchChatToken.js

export async function fetchChatToken(
  userId,
  capabilities = [],
  attributes,
  sessionDurationInMinutes) {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------

## 观察连接更新
<a name="chat-react-rooms-connection-state"></a>

对聊天室连接状态的变化做出反应是制作聊天应用程序的重要组成部分。让我们从订阅相关事件开始：

**TypeScript/JavaScript**：

```
// App.tsx / App.jsx

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {});
    const unsubscribeOnConnected = room.addListener('connect', () => {});
    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {});

    return () => {
      // Clean up subscriptions.
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <Text>Hello!</Text>;
}
```

接下来，我们需要提供读取连接状态的功能。我们使用 `useState` 钩子在 `App` 中创建一些本地状态，并在每个侦听器内设置连接状态。

**TypeScript/JavaScript**：

```
// App.tsx / App.jsx

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {  
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <Text>Hello!</Text>;
}
```

订阅连接状态后，显示连接状态并使用 `useEffect` 钩子内的 `room.connect` 方法连接到聊天室：

**TypeScript/JavaScript**：

```
// App.tsx / App.jsx

// ...

useEffect(() => {
  const unsubscribeOnConnecting = room.addListener('connecting', () => {
    setConnectionState('connecting');
  });

  const unsubscribeOnConnected = room.addListener('connect', () => {
    setConnectionState('connected');
  });

  const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
    setConnectionState('disconnected');
  });

  room.connect();

  return () => {
    unsubscribeOnConnecting();
    unsubscribeOnConnected();
    unsubscribeOnDisconnected();
  };
}, [room]);

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  }
});

// ...
```

您已成功实现聊天室连接。

## 创建发送按钮组件
<a name="chat-react-rooms-send-button"></a>

在本节中，您将创建一个发送按钮，该按钮针对每种连接状态都有不同的设计。该发送按钮有助于在聊天室中发送消息。它还可以直观地指示是否/何时可以发送消息；例如，在连接断开或聊天会话过期的情况下。

首先，在 Chatterbox 项目的 `src` 目录中创建一个新文件并将其命名为 `SendButton`。然后，创建一个组件，该组件将显示聊天应用程序的按钮。导出您的 `SendButton` 并将其导入 `App`。在空的 `<View></View>` 中，添加 `<SendButton />`。

------
#### [ TypeScript ]

```
// SendButton.tsx

import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';

interface Props {
  onPress?: () => void;
  disabled: boolean;
  loading: boolean;
}

export const SendButton = ({ onPress, disabled, loading }: Props) => {
  return (
    <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}>
      {loading ? <Text>Send</Text> : <ActivityIndicator />}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  root: {
    width: 50,
    height: 50,
    borderRadius: 30,
    marginLeft: 10,
    justifyContent: 'center',
    alignContent: 'center',
  }
});

// App.tsx

import { SendButton } from './SendButton';

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton />
  </SafeAreaView>
);
```

------
#### [ JavaScript ]

```
// SendButton.jsx

import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';

export const SendButton = ({ onPress, disabled, loading }) => {
  return (
    <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}>
      {loading ? <Text>Send</Text> : <ActivityIndicator />}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  root: {
    width: 50,
    height: 50,
    borderRadius: 30,
    marginLeft: 10,
    justifyContent: 'center',
    alignContent: 'center',
  }
});

// App.jsx

import { SendButton } from './SendButton';

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton />
  </SafeAreaView>
);
```

------

接下来，在 `App` 中定义一个名为 `onMessageSend` 的函数，并将其传递给 `SendButton onPress` 属性。定义另一个名为 `isSendDisabled` 的变量（该变量可防止在未连接聊天室时发送消息），并将其传递给 `SendButton disabled` 属性。

**TypeScript/JavaScript**：

```
// App.jsx / App.tsx

// ...

const onMessageSend = () => {};

const isSendDisabled = connectionState !== 'connected';

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </SafeAreaView>
);

// ...
```

## 创建消息输入
<a name="chat-react-rooms-message-input"></a>

Chatterbox 消息栏是您将与之交互以向聊天室发送消息的组件。通常，它包含用于编写消息的文本输入和用于发送消息的按钮。

要创建 `MessageInput` 组件，首先在 `src` 目录中创建一个新文件并将其命名为 `MessageInput`。然后创建一个输入组件，这将会显示聊天应用程序的输入。导出您的 `MessageInput` 并将其导入 `App`（在 `<SendButton />` 上方）。

使用 `useState` 钩子创建一个名为 `messageToSend` 的新状态，其默认值为空字符串。在应用程序正文中，将 `messageToSend` 传递给 `MessageInput` 的 `value`，并将 `setMessageToSend` 传递给 `onMessageChange` 属性：

------
#### [ TypeScript ]

```
// MessageInput.tsx

import * as React from 'react';

interface Props {
  value?: string;
  onValueChange?: (value: string) => void;
}

export const MessageInput = ({ value, onValueChange }: Props) => {
  return (
    <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" />
  );
};

const styles = StyleSheet.create({
  input: {
    fontSize: 20,
    backgroundColor: 'rgb(239,239,240)',
    paddingHorizontal: 18,
    paddingVertical: 15,
    borderRadius: 50,
    flex: 1,
  }
})

// App.tsx

// ...

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  messageBar: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: 'rgb(160,160,160)',
    flexDirection: 'row',
    padding: 16,
    alignItems: 'center',
    backgroundColor: 'white',
  }
});
```

------
#### [ JavaScript ]

```
// MessageInput.jsx

import * as React from 'react';

export const MessageInput = ({ value, onValueChange }) => {
  return (
    <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" />
  );
};

const styles = StyleSheet.create({
  input: {
    fontSize: 20,
    backgroundColor: 'rgb(239,239,240)',
    paddingHorizontal: 18,
    paddingVertical: 15,
    borderRadius: 50,
    flex: 1,
  }
})

// App.jsx

// ...

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  messageBar: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: 'rgb(160,160,160)',
    flexDirection: 'row',
    padding: 16,
    alignItems: 'center',
    backgroundColor: 'white',
  }
});
```

------

## 后续步骤
<a name="chat-react-rooms-next-steps"></a>

现在，您已经为 Chatterbox 构建了一个消息栏，请继续阅读本 React Native 教程第 2 部分：[消息和事件](chat-sdk-react-tutorial-messages-events.md)。