与查克·诺里斯 (Chuck Norris) 进行 React Native 聊天
我是Stream的开发者布道师,负责 Feeds 和 Chat 的 API 开发。我有幸体验了我们各种工具、精美的 UI 套件和各种新产品!多年来,Stream 一直是 Feeds 即服务的领先提供商,为超过 5 亿终端用户提供 Feeds 服务。过去几个月,团队一直在努力开发一款新产品——Chat。
在本教程中,我将引导您了解如何使用 React Native、Gifted Chat(Stream 的 React Native Chat Components 目前处于测试阶段)、Serverless以及Chat by Stream构建一个简单的即时通讯应用。此外,我还将介绍一个后端 Lambda 函数,该函数将使用第三方 API 提供的 Chuck Norris 笑话和事实进行自动回复。
对于最繁重的工作,我们将使用Stream Chat JS,这是一个可以直接与 Stream API 通信的 JavaScript SDK。如果您有兴趣查看关于 Stream Chat 的精彩教程,请点击此处。
我还应该指出,我将使用 macOS 和 iOS,因此如果您使用的是 Windows 或 Linux,本教程可能会略有偏差。
要继续阅读本文,请确保您已安装和/或设置以下内容:
正在寻找代码库?您可以在这里找到开源代码。想要快速演示一下吗?我也有!可以看看Appetize上的演示。
让我们玩得开心点!🏂
1. 按流配置聊天
前往https://getstream.io,点击网站右上角的“注册”按钮。然后按照提示操作。
跳转后,前往https://getstream.io/chat/#pricing并点击“开始试用”按钮——这将为您的帐户启用聊天功能(14 天)。试用期设置完成后,您就快完成了!
返回仪表盘并点击您的应用程序。仪表盘默认显示“动态”,因此请点击顶部的“聊天”按钮。
从这里向下滚动,您将看到新启用聊天功能的应用的各种设置。请确保在新标签页中保持打开状态——您将需要此页面上的Key、Secret(底部)和App ID(顶部)。
如果您对 Stream Chat 的定价和产品比较感兴趣,请查看此处。
2. 设置无服务器
当您只需要几个端点时(类似于此构建),无服务器通常是最明智的选择。启动服务器可能需要相当长的时间,而且成本可能相当高昂。我并不是说无服务器环境适合所有人和所有事物,但我要说的是,如果您要构建一个很小的 API,那么无服务器环境绝对是最佳选择。
前往https://dashboard.serverless.com并创建一个新帐户。按照初始步骤操作(邮箱验证、用户名选择、应用创建等)。创建应用后,保存租户名称(例如 nick-chuck 用户名 = nickchuck 租户),并保存应用名称——我们在接下来的几个步骤中都需要这两个名称。
3. 使用 Expo 创建 React Native 应用
Expo 通过使用 Expo API 简化了构建 React Native 应用的过程。从技术上讲,我们根本不需要它;但是,如果您想快速构建一些应用,并有可能在 iOS 或 Android 上发布,Expo 将是最快的方法。毕竟,您可以随时退出 Expo 项目。
要创建你的应用,请打开终端并转到你选择的目录(我将在 中~/Code
)。一切准备就绪后,运行以下命令来搭建项目。
$ expo init react-native-chat-chucky
按照 Expo CLI 的提示,在 tabs vs. blank 问题中选择“blank”。完成这些问题后,Expo 将生成目录并使用 yarn 或 npm 安装必要的依赖项。你的终端应该如下所示:
一切就绪!👏
4. 将聊天 SDK 添加到 React Native Chat
接下来,让我们使用以下命令安装所有必需的依赖项。我将使用 yarn 来完成此操作,但如果您在初始设置时选择了 npm,请使用它来避免混淆锁文件。
$ yarn add axios md5 react-native-gifted-chat react-native-iphone-x-helper react-native-parsed-text react-router-native stream-chat
5.添加默认消息
为了显示聊天界面,我们使用了react-native-gifted-chat,这是一个专为处理聊天应用程序而设计的出色 UI 库。有了 Gifted Chat,我们可以将 UI 放在一边,快速启动并运行!
要启动初始消息,我们需要创建一个新目录和一个 messages 文件。UI 已经连接到此文件,因此只需创建它并放入自定义消息即可。
$ mkdir data && touch messages.js
完成该步骤后,将以下代码片段粘贴到文件中并保存。
module.exports = [
{
_id: Math.round(Math.random() * 1000000),
text: "Say something... I'll tell you some fun facts! 🤣",
createdAt: Date.now(),
user: {
_id: "chuck",
name: "Chuck Norris"
}
},
{
_id: Math.round(Math.random() * 1000000),
text: "Chat with Chuck!",
createdAt: Date.now(),
system: true
}
];
一切准备就绪!🚀
6. 向 React Native 添加路由和屏幕
我们已经具备了所有必要的依赖关系,所以让我们继续前进并将一切联系在一起!
修改您的App.js
文件以包含以下代码片段。
import React, { Component } from "react";
import { KeyboardAvoidingView, StyleSheet } from "react-native";
import { NativeRouter as Router, Route, Switch } from "react-router-native";
import Chat from "./screens/Chat";
import Login from "./screens/Login";
export default class App extends Component {
render() {
return (
<KeyboardAvoidingView behavior="padding" enabled style={styles.root}>
<Router>
<Switch>
<Route exact path="/chat" component={Chat} />
<Route path="/" component={Login} />
</Switch>
</Router>
</KeyboardAvoidingView>
);
}
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: "white"
}
});
创建一个名为 screens 的目录,并在其中创建两个文件 — Chat.js
和Login.js
。
$ mkdir screens && cd screens && touch Chat.js && touch Login.js
一旦这两个文件到位,我们就需要填充它们!将下面显示的代码放入相应的文件中。
Chat.js
import React, { Component } from "react";
import { Constants, LinearGradient } from "expo";
import {
ActivityIndicator,
Platform,
SafeAreaView,
StyleSheet,
Text,
View
} from "react-native";
import {
GiftedChat,
Bubble,
InputToolbar,
SystemMessage
} from "react-native-gifted-chat";
import { StreamChat } from "stream-chat";
import { isIphoneX, getBottomSpace } from "react-native-iphone-x-helper";
import axios from "axios";
import md5 from "md5";
const client = new StreamChat("<YOUR_STREAM_APP_ID>");
export default class Chat extends Component {
constructor(props) {
super(props);
this.state = {
messages: [],
typingText: null,
user: null,
token: null,
channel: null
};
this._isMounted = false;
this._isAlright = null;
}
componentWillMount() {
this._isMounted = true;
this.setState({
messages: require("../data/messages.js")
});
}
async componentDidMount() {
const { location } = this.props;
const user = location.state.user;
try {
const init = await axios.post("<YOUR_SERVERLESS_INVOCATION_URL>", {
name: user.name,
email: user.email
});
await client.setUser(init.data.user, init.data.token);
const channel = client.channel("messaging", md5(user.email), {
name: "Chat with Chuck Norris",
members: ["chuck", init.data.user.id]
});
await channel.create();
await channel.watch();
channel.on(event => this.incoming(event));
this.setState({
user: init.data.user,
token: init.data.token,
channel
});
} catch (error) {
console.log(error);
}
}
componentWillUnmount() {
this._isMounted = false;
}
incoming(evt) {
if (evt.type === "message.new" && evt.user.id !== this.state.user.id) {
this.onReceive(evt);
}
}
onSend = async (messages = []) => {
try {
await this.state.channel.sendMessage({
text: messages[0].text
});
this.setState(previousState => {
return {
messages: GiftedChat.append(previousState.messages, messages),
typingText: "Chuck Norris is typing..." // mock typing indicator
};
});
} catch (error) {
console.log(error);
}
};
onReceive = data => {
this.setState(previousState => {
return {
messages: GiftedChat.append(previousState.messages, {
_id: data.message.id,
text: data.message.text,
createdAt: data.message.created_at,
user: {
_id: data.message.user.id,
name: data.message.user.name
}
}),
typingText: null
};
});
};
renderBubble = props => {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: "#f0f0f0"
}
}}
/>
);
};
renderInputToolbar = props => {
if (isIphoneX()) {
return (
<SafeAreaView>
<InputToolbar {...props} />
</SafeAreaView>
);
}
return <InputToolbar {...props} />;
};
renderSystemMessage = props => {
return (
<SystemMessage
{...props}
containerStyle={{
marginBottom: 15
}}
textStyle={{
fontSize: 14
}}
/>
);
};
renderFooter = props => {
if (this.state.typingText) {
return (
<View style={styles.footerContainer}>
<Text style={styles.footerText}>{this.state.typingText}</Text>
</View>
);
}
return null;
};
render() {
if (!this.state.user) {
return (
<View style={styles.loader}>
<ActivityIndicator />
</View>
);
}
const { user } = this.state;
return (
<>
<GiftedChat
messages={this.state.messages}
onSend={this.onSend}
user={{
_id: user.id // sent messages should have same user._id
}}
renderBubble={this.renderBubble}
renderSystemMessage={this.renderSystemMessage}
renderInputToolbar={this.renderInputToolbar}
renderFooter={this.renderFooter}
listViewProps={this._listViewProps}
/>
<LinearGradient
pointerEvents="none"
colors={this._gradient}
style={styles.header}
/>
</>
);
}
get _gradient() {
return [
"rgba(255, 255, 255, 1)",
"rgba(255, 255, 255, 1)",
"rgba(255, 255, 255, 0)"
];
}
get _listViewProps() {
return {
style: styles.listViewStyle,
contentContainerStyle: styles.contentContainerStyle
};
}
}
const styles = StyleSheet.create({
footerContainer: {
marginTop: 5,
marginLeft: 10,
marginRight: 10,
marginBottom: 10
},
footerText: {
fontSize: 14,
color: "#aaa"
},
header: {
height: Constants.statusBarHeight + 64,
position: "absolute",
top: 0,
left: 0,
right: 0
},
listViewStyle: {
flex: 1,
marginBottom: isIphoneX() ? getBottomSpace() : 0
},
loader: {
flex: 1,
justifyContent: "center",
alignItems: "center"
},
contentContainerStyle: {
paddingTop: 24
}
});
Login.js
import React, { Component } from "react";
import {
Image,
SafeAreaView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { Link } from "react-router-native";
const hitSlop = { top: 24, right: 24, bottom: 24, left: 24 };
class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
name: ""
};
}
_handleChange = name => value => {
this.setState({
[name]: value
});
};
_renderLink = props => (
<TouchableOpacity disabled={!this._canLogin} hitSlop={hitSlop} {...props} />
);
render() {
const { email, name } = this.state;
return (
<SafeAreaView style={styles.root}>
<View style={styles.brand}>
<Image style={styles.logo} source={require("../images/chuck.png")} />
<Text style={styles.name}>Chuck Bot</Text>
</View>
<TextInput
style={styles.input}
placeholder="Name"
onChangeText={this._handleChange("name")}
value={name}
/>
<TextInput
autoCapitalize="none"
style={styles.input}
placeholder="Email"
onChangeText={this._handleChange("email")}
value={email}
/>
<View style={styles.btnWrapper}>
<Link to={this._to} component={this._renderLink}>
<Text style={this._labelStyle}>Chat with Chuck</Text>
</Link>
</View>
</SafeAreaView>
);
}
get _to() {
return {
pathname: "/chat",
state: {
user: this.state
}
};
}
get _canLogin() {
const { email, name } = this.state;
return Boolean(name) && Boolean(email);
}
get _labelStyle() {
return {
...styles.btnLabel,
color: this._canLogin ? "rgb(0, 122, 255)" : "#eeeeee"
};
}
}
export default Login;
const styles = StyleSheet.create({
btnLabel: {
fontSize: 16,
color: "rgb(0, 122, 255)"
},
btnWrapper: {
alignItems: "center",
justifyContent: "center",
paddingVertical: 32
},
brand: {
alignItems: "center",
justifyContent: "center",
marginBottom: 32
},
input: {
flexDirection: "row",
fontSize: 20,
fontWeight: "600",
marginVertical: 8,
borderRadius: 8,
borderColor: "#f9f9f9",
borderWidth: 2,
padding: 16,
width: 343
},
logo: {
width: 80,
height: 112
},
name: {
fontSize: 20,
fontWeight: "800",
marginTop: 16
},
root: {
flex: 1,
paddingHorizontal: 16,
justifyContent: "center",
alignItems: "center"
}
});
在第 22 行,从您的 Stream Dashboard 中输入 Stream App ID。在第 53 行,您需要添加由 AWS API Gateway 提供的 Lambda 端点——不用担心,您目前还没有这个端点,我们将在下一节中介绍。
哇!快到了!👨🚀
7. 添加无服务器目录和文件
现在您已经使用 Expo 成功搭建了 React Native 应用程序,请进入目录(例如 react-native-chat-chucky)。
$ cd react-native-chat-chucky
首先,让我们继续创建一个名为 serverless 的新目录,然后进入该目录,以便我们可以安装一些依赖项。
$ mkdir serverless && cd serverless
创建一个新package.json
文件。
$ touch package.json
然后,将下面的示例代码片段的内容复制到您的 package.json 文件中。
{
"name": "stream-react-chat",
"version": "1.0.0",
"description": "Facilitates communication between RN and Stream",
"main": "handler.js",
"license": "BSD-3-Clause",
"private": false,
"scripts": {
"start": "sls offline",
"deploy": "sls deploy"
},
"dependencies": {
"@babel/runtime": "^7.3.1",
"axios": "^0.18.0",
"babel-runtime": "^6.26.0",
"stream-chat": "^0.9.0",
"uuid": "^3.3.2"
},
"devDependencies": {
"babel-loader": "^8.0.5",
"eslint": "^5.16.0",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"prettier": "^1.17.0",
"serverless-offline": "^4.9.4",
"serverless-webpack": "^5.2.0",
"webpack": "^4.30.0",
"webpack-node-externals": "^1.7.2"
}
}
添加 package.json 文件后,请确保在 serverless 目录中运行 yarn 以正确安装模块。
完成上述步骤后,您需要将以下文件内容复制/粘贴到无服务器目录中。
.eslintrc.json
{
"plugins": ["babel"],
"extends": ["eslint:recommended"],
"rules": {
"no-console": 0,
"no-mixed-spaces-and-tabs": 1,
"comma-dangle": 0,
"no-unused-vars": 1,
"eqeqeq": [2, "smart"],
"no-useless-concat": 2,
"default-case": 2,
"no-self-compare": 2,
"prefer-const": 2,
"no-underscore-dangle": [2, { "allowAfterThis": true }],
"object-shorthand": 1,
"babel/no-invalid-this": 2,
"array-callback-return": 2,
"valid-typeof": 2,
"arrow-body-style": 2,
"require-await": 2,
"react/prop-types": 0,
"no-var": 2,
"linebreak-style": [2, "unix"],
"semi": [1, "always"]
},
"env": {
"es6": true
},
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2018,
"ecmaFeatures": {
"modules": true
}
}
}
.prettierrc
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"singleQuote": true
}
webpack.config.js
const path = require("path");
const slsw = require("serverless-webpack");
const nodeExternals = require("webpack-node-externals");
module.exports = {
entry: slsw.lib.entries,
target: "node",
devtool: "source-map",
mode: "production",
externals: [nodeExternals()],
optimization: {
minimize: false
},
performance: {
hints: false
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader"
}
]
}
]
}
};
serverless.yml
service: <YOUR_APP_NAME> # e.g. chuck-norris
app: <YOUR_APP_NAME> # e.g. chuck-norris
tenant: <YOUR_TENANT_NAME> # e.g. tenant
frameworkVersion: ">=1.32.0 <2.0.0"
provider:
name: aws
runtime: nodejs8.10
stage: prod
region: us-east-1
memorySize: 3008
timeout: 30
environment:
STREAM_KEY: "<YOUR_STREAM_KEY>"
STREAM_SECRET: "<YOUR_STREAM_SECRET>"
STREAM_APP_ID: "<YOUR_STREAM_APP_ID>"
functions:
init:
handler: handler.init
events:
- http:
path: /init
method: post
reply:
handler: handler.reply
events:
- http:
path: /reply
method: post
plugins:
- serverless-webpack
- serverless-offline
custom:
webpack:
packager: "yarn"
webpackConfig: "webpack.config.js"
includeModules:
forceInclude:
- "@babel/runtime"
serverless-offline:
port: 8000
文件的顶部
serverless.yml
是定义租户名称和应用名称的地方,这些名称是从 Serverless Dashboard 保存的。在本例中,我使用nickchuck作为租户名称,使用chuck-norris作为应用名称。
干得好!👊
8. 设置AWS
网上有很多关于如何做到这一点的信息。我会提供必要的步骤,但我强烈建议你仔细阅读!
需要注意的一点是,无服务器需要几种不同的 AWS IAM 权限,例如 S3、CloudFormation、API Gateway 和 Lambda。有一些方法可以限定 IAM 权限的范围,使其仅满足必要的要求,具体方法可在此处找到。我发现,虽然这是最安全的方法,但肯定不是最快的。我建议,如果这是个人 AWS 账户,请节省一些时间,并从 IAM 授予您的账户“AdministratorAccess”权限。
设置您的 AWS 账户和 IAM 权限后,使用 aws-cli 在命令行中指定您的凭证(凭证可以在 IAM 下找到)。
$ aws configure
aws-cli 会要求您输入访问密钥 ID 和秘密访问密钥。它还会询问您几个(可选)问题,以便 AWS CLI 能够正确配置您的配置文件。
首次安装 aws-cli 需要注意什么?在 macOS 上超级简单。只需运行 brew install aws-cli,Homebrew 会帮你搞定剩下的事情!
干得好!🕺
9. 部署无服务器构建
配置好 AWS 并将 AWS 凭证安全地存储在您的计算机上后,就可以将无服务器构建部署到 Lambda 了!导航到 React Native 代码库中的无服务器目录,然后运行以下命令!
如果您尚未安装 Serverless CLI,可以使用 npm install -g serverless 进行安装。
$ sls deploy
构建将开始,您应该开始看到实时日志!
遇到问题了吗?请在文章末尾的评论区留下详细信息,我会尽力帮助您!
轰!💥
10.启用 API 网关开始服务请求
无服务器架构帮你完成了一些繁重的工作,自动化了大部分构建工作。在无服务器架构出现之前,我们只能使用 Lambda,有时确实会有些不稳定和繁琐。使用纯 Lambda 的工作流程时,我们过去需要压缩代码库(包括 Node 模块),然后手动将压缩文件上传到 AWS。真讨厌!😫
要验证您的 Lambda 是否与 API Gateway 一起配置,请登录 AWS 并搜索 API Gateway。
点击“API 网关”,您将被重定向到仪表板,其中包含您的 API 列表。我的帐户下有几个 API,因此显示的内容可能会有所不同。理想情况下,您应该查找名为 的 API prod-<YOUR_APP_NAME>
。就我而言,它是prod-react-native-app
。点击正确的 API 即可查看您的资源。
选择顶级根资源后,单击“操作”下拉菜单并选择“部署 API”。
系统会显示一个模态框,供您指定“阶段”。如果您在下拉菜单中还没有“阶段”,请创建一个新的阶段,并随意命名。我选择使用“prod”这个名称,因为当我推送到 Lambda 时,它通常已经使用Serverless-Offline进行了测试,并且已准备好投入生产。
点击“部署”,您的 API 将被部署到 API 网关!现在,只需捕获调用 URL 并保存以备下一步使用!
快到了!🏎
11. 在 Chat.js 中指定 Init 端点
/init
保存调用 URL 后,初始化处理程序将通过附加到其末尾来调用。转到Chat.js
第 53 行,并从 AWS API 网关插入您的调用 URL。这将处理流聊天所需的服务器端令牌的获取和生成。
有了正确的 URL,代码会/init
在用户登录后(也就是你!)向服务器发送一个 POST 请求,请求中包含用户信息。POST 请求会返回一个序列化的对象,其中包含用户信息以及生成的用户 token。
您的终端节点目前未受 API 密钥保护。如果您想启用 API 密钥,可以在 API 网关内逐个路由地进行操作。
答对了!🎲
12.设置 Webhook 回复 URL
与上述步骤类似,该 URL 将是您的调用 URL,/reply
并附加到末尾。在 Stream Chat 仪表板中,向下滚动到“聊天事件”部分,并通过滑动开关直至其变为绿色来启用 Webhook。然后将您的 URL 放入输入框中,然后点击“保存”。
设置好 URL 并激活 Webhook 后,任何通过 UI 发送并发送到 Stream 的聊天事件都将通过 POST 转发到您的 Lambda。消息正文包含许多有用信息,包括聊天标识符 (CID)、发出请求的用户、消息正文等等。
如果你查看 serverless/handler.js 中的回复处理程序,你会注意到,只有当事件来自“chuck”(查克·诺里斯的预设用户)以外的用户时,我们才会返回聊天消息。这是一个相当简单的逻辑,应该不会太令人困惑。
Stream CLI 还提供了设置 Webhook URL 的功能——您可以在此处下载 Stream CLI 。请参阅此处的文档。
再一步!🚶
13. 本地聊天火爆!
你已经取得了很大进展。到目前为止,我们已经使用 Expo 和 Gifted Chat 构建了一个自定义的 React Native 聊天 UI,绑定到 Stream,使用无服务器搭建了一个 AWS Lambda,并在 Stream 聊天仪表板上配置了一个 Webhook。哇,真是不少啊。
现在是时候和查克·诺里斯一起享受乐趣并聆听有关他一生的所有激动人心的故事了。
使用命令行启动 iOS 模拟器(从您的项目目录)。
$ expo start — ios
或者
进入 React Native 的根目录并运行命令yarn start
。Expo 将为您打开一个新的调试器窗口。接下来,请按照以下步骤操作。
- 打开 Xcode
- 导航到 Xcode 菜单栏,直到它下拉
- 找到“打开开发者工具”,然后点击“模拟器”
iOS 模拟器将会启动并显示在窗口右上角。接下来,将焦点放在 Expo 打开的调试器窗口上。在左下角,点击“在 iOS 模拟器上运行”。
应用程序应该在 iOS 模拟器中加载,并且您应该看到登录屏幕!
输入您的姓名和电子邮件地址,然后点击底部的“与 Chuck 聊天”按钮。应用将向服务器发出请求,并检索您的应用在 Stream 上提供的有效用户令牌。从现在开始,一切尽在掌握!
构建步骤卡住了?请在下方评论区留言,我很乐意帮你解决!
如果您希望进一步定制应用程序或将其转换为 APK(Android)或 IPA(iOS),我建议您查看以下链接:
好了!Stream Chat提供了从零开始构建聊天产品所需的后端基础架构,您只需完成前端即可!查看这些基于 React 构建的 Stream Chat 教程。如果您有兴趣使用其他语言/框架进行构建,Stream 提供了多个SDK以及一个iOS (Swift) SDK,适合那些喜欢在 iOS 上进行原生开发的用户。
鏂囩珷鏉ユ簮锛�https://dev.to/nickparsons/react-native-chat-with-chuck-norris-3h7m