欢迎来到你的第三个 MERN 项目,在这里你使用 MERN 框架构建了一个很棒的消息应用。后端托管在 Heroku,前端站点托管在 Firebase。
Material-UI 提供了项目中的图标。使用 Pusher 是因为 MongoDB 不是像 Firebase 那样的实时数据库,聊天应用需要实时数据。这是一个带有谷歌认证的功能性聊天应用,不同的用户可以使用他们的谷歌账户登录聊天。图 4-1 显示了一个全功能托管和完成的应用。
图 4-1
最终托管的应用
转到您的终端并创建一个messaging-app-mern文件夹。在里面,使用 create-react-app 创建一个名为 messaging-app-frontend 的新应用。
mkdir messaging-app-mern
cd messaging-app-mern
npx create-react-app messaging-app-frontend由于前端站点是通过 Firebase 托管的,所以可以在 create-react-app 创建 React app 的同时创建基本设置。按照第 1 章的设置说明,我在 Firebase 控制台中创建了消息应用。
让我们返回到 React 项目,将cd返回到messaging-app-frontend目录。用npm start启动 React 应用。
cd messaging-app-frontend
npm start在index.js、App.js和App.css中删除文件和基本设置就像在第 2 章中所做的一样。遵循这些指示。
图 4-2 显示了该应用在 localhost 上的外观。
图 4-2
初始应用
让我们创建一个侧边栏组件,显示登录用户的头像和其他图标,包括一个搜索栏。在创建侧边栏组件之前,在App.js文件中添加基本样式。在App.js,中创建一个包含所有代码的app__body类。更新的内容用粗体标记。
import './App.css';
function App() {
return (
<div className="app">
<div className="app__body">
</div>
</div>
);
}
export default App;接下来,在App.css中设置容器的样式,得到一个带阴影的居中容器。
.app{
display: grid;
place-items: center;
height: 100vh;
background-color: #dadbd3;
}
.app__body{
display: flex;
background-color: #ededed;
margin-top: -50px;
height: 90vh;
width: 90vw;
box-shadow: -1px 4px 20px -6px rgba(0, 0, 0, 0.75);
}转到本地主机。您应该会看到如图 4-3 所示的大阴影框。
图 4-3
初始背景
接下来,在src文件夹中创建一个components文件夹。然后在components文件夹中创建两个文件——Sidebar.js和Sidebar.css。将内容放在Sidebar.js文件中。以下是Sidebar.js文件的内容。
import React from 'react'
import './Sidebar.css'
const Sidebar = () => {
return (
<div className="sidebar">
<div className="sidebar__header"></div>
<div className="sidebar__search"></div>
<div className="sidebar__chats"></div>
</div>
)
}
export default Sidebar接下来安装 Material-UI ( https://material-ui.com )得到图标。根据 Material-UI 文档进行两次 npm 安装。通过messaging-app-frontend文件夹中的集成端子安装铁芯。
npm i @material-ui/core @material-ui/icons接下来,让我们在Sidebar.js文件中使用这些图标。导入它们,然后在sidebar__header类中使用它们。更新的内容用粗体标记。
import React from 'react'
import './Sidebar.css'
import DonutLargeIcon from '@material-ui/icons/DonutLarge'
import ChatIcon from '@material-ui/icons/Chat'
import MoreVertIcon from '@material-ui/icons/MoreVert'
import { Avatar, IconButton } from '@material-ui/core'
const Sidebar = () => {
return (
<div className="sidebar">
<div className="sidebar__header">
<Avatar />
<div className="sidebar__headerRight">
<IconButton>
<DonutLargeIcon />
</IconButton>
<IconButton>
<ChatIcon />
</IconButton>
<IconButton>
<MoreVertIcon />
</IconButton>
</div>
</div>
<div className="sidebar__search"></div>
<div className="sidebar__chats"></div>
</div>
)
}
export default Sidebar让我们在Sidebar.css文件中添加侧边栏标题样式。flexbox 用于实现这一点。
.sidebar {
display: flex;
flex-direction: column;
flex: 0.35;
}
.sidebar__header {
display: flex;
justify-content: space-between;
padding: 20px;
border-right: 1px solid lightgray;
}
.sidebar__headerRight {
display: flex;
align-items: center;
justify-content: space-between;
min-width: 10vw;
}
.sidebar__headerRight > .MuiSvgIcon-root{
margin-right: 2vw;
font-size: 24px !important;
}接下来,让我们导入App.js中的侧边栏组件,让它显示在 localhost 上。更新的内容用粗体标记。
import './App.css';
import Sidebar from './components/Sidebar';
function App() {
return (
<div className="app">
<div className="app__body">
<Sidebar />
</div>
</div>
);
}
export default App;图 4-4 显示了本地主机上对齐的图标。
接下来,在Sidebar.js中创建搜索栏。从 Material-UI 导入SearchOutlined并与sidebar__searchContainer类一起使用。在旁边放一个输入框。
import { SearchOutlined } from '@material-ui/icons'
const Sidebar = () => {
return (
<div className="sidebar">
<div className="sidebar__header">
<Avatar src="https://pbs.twimg.com/profile_img/1020939891457241088/fcbu814K_400x400.jpg"/>
<div className="sidebar__headerRight">
...
</div>
</div>
<div className="sidebar__search">
<div className="sidebar__searchContainer">
<SearchOutlined />
<input placeholder="Search or start new chat" type="text" />
</div>
</div>
<div className="sidebar__chats"></div>
</div>
)
}
export default Sidebar图 4-4
图标对齐
我用我的推特账户上的一张图片作为头像。更新的内容用粗体标记。
搜索栏的样式在Searchbar.css文件中。很多 flexboxes 都是用来做造型的。将新内容添加到现有内容中。
.sidebar__search {
display: flex;
align-items: center;
background-color: #f6f6f6;
height: 39px;
padding: 10px;
}
.sidebar__searchContainer{
display: flex;
align-items: center;
background-color: white;
width: 100%;
height: 35px;
border-radius: 20px;
}
.sidebar__searchContainer > .MuiSvgIcon-root{
color: gray;
padding: 10px;
}
.sidebar__searchContainer > input {
border: none;
outline-width: 0;
margin-left: 10px;
}图 4-5 显示了本地主机上的所有内容。
图 4-5
搜索栏
现在让我们构建侧边栏聊天组件。在components文件夹中,创建两个文件——SidebarChat.js和SidebarChat.css。在Sidebar.js文件中使用它们。更新的内容用粗体标记。
...
import SidebarChat from './SidebarChat'
const Sidebar = () => {
return (
<div className="sidebar">
<div className="sidebar__header">
...
</div>
<div className="sidebar__search">
...
</div>
<div className="sidebar__chats">
<SidebarChat />
<SidebarChat />
<SidebarChat />
</div>
</div>
)
}
export default Sidebar在编写侧边栏聊天组件之前,让我们设计一下sidebar__chats div 的样式,它包含了Sidebar.css文件中的SidebarChat组件。将新内容添加到现有内容中。
.sidebar__chats{
flex: 1;
background-color: white;
overflow: scroll;
}在SidebarChat.js文件中,有一个简单的功能组件。如果你给一个 API 端点传递随机的字符串,它会提供随机的化身。使用种子状态变量;它每次都随着useEffect中的随机字符串而改变。
import React, { useEffect, useState } from 'react'
import { Avatar } from '@material-ui/core'
import './SidebarChat.css'
const SidebarChat = () => {
const [seed, setSeed] = useState("")
useEffect(() => {
setSeed(Math.floor(Math.random() * 5000))
}, [])
return (
<div className="sidebarChat">
<Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
<div className="sidebarChat__info">
<h2>Room name</h2>
<p>Last message...</p>
</div>
</div>
)
}
export default SidebarChat接下来,让我们在SidebarChat.css文件中设计一些房间的样式。这里,您再次使用 flexbox 和一些衬垫。
.sidebarChat{
display: flex;
padding: 20px;
cursor: pointer;
border-bottom: 1px solid #f6f6f6;
}
.sidebarChat:hover{
background-color: #ebebeb;
}
.sidebarChat__info > h2 {
font-size: 16px;
margin-bottom: 8px;
}
.sidebarChat__info {
margin-left: 15px;
}图 4-6 显示了 localhost 上的侧边栏聊天组件。
图 4-6
边栏聊天
让我们开始研究聊天组件。在components文件夹中创建两个文件Chat.js和Chat.css。把这个基本结构放到Chat.js文件里。随机字符串用于显示随机头像图标。
import React, { useEffect, useState } from 'react'
import { Avatar, IconButton } from '@material-ui/core'
import { AttachFile, MoreVert, SearchOutlined } from '@material-ui/icons'
import './Chat.css'
const Chat = () => {
const [seed, setSeed] = useState("")
useEffect(() => {
setSeed(Math.floor(Math.random() * 5000))
}, [])
return (
<div className="chat">
<div className="chat__header">
<Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
<div className="chat__headerInfo">
<h3>Room Name</h3>
<p>Last seen at...</p>
</div>
<div className="chat__headerRight">
<IconButton>
<SearchOutlined />
</IconButton>
<IconButton>
<AttachFile />
</IconButton>
<IconButton>
<MoreVert />
</IconButton>
</div>
</div>
<div className="chat__body"></div>
<div className="chat__footer"></div>
</div>
)
}
export default Chat接下来,在Chat.css文件中设置聊天标题的样式,并在chat__body类中添加一个漂亮的背景图片。
.chat{
display: flex;
flex-direction: column;
flex: 0.65;
}
.chat__header{
padding: 20px;
display: flex;
align-items: center;
border-bottom: 1px solid lightgray;
}
.chat__headerInfo {
flex: 1;
padding-left: 20px;
}
.chat__headerInfo > h3 {
margin-bottom: 3px;
font-weight: 500;
}
.chat__headerInfo > p {
color: gray;
}
.chat__body{
flex: 1;
background-image: url("https://user-images.githubusercontent.com/15075759/28719144-86dc0f70-73b1-11e7-911d-60d70fcded21.png");
background-repeat: repeat;
background-position: center;
padding: 30px;
overflow: scroll;
}从App.js文件呈现聊天组件。更新的内容用粗体标记。
import './App.css';
import Sidebar from './components/Sidebar';
import Chat from './components/Chat';
function App() {
return (
<div className="app">
<div className="app__body">
<Sidebar />
<Chat />
</div>
</div>
);
}
export default App;前往本地主机。图 4-7 显示聊天的标题已经完成,并且显示了一个漂亮的背景图像。
图 4-7
聊天组件
接下来,返回到Chat.js文件,将硬编码的消息放在chat__message类的p标签中。两个 span 标记用于名称和时间戳。
注意聊天用户的chat__receiver类。更新的内容用粗体标记。
...
const Chat = () => {
const [seed, setSeed] = useState("")
useEffect(() => {
setSeed(Math.floor(Math.random() * 5000))
}, [])
return (
<div className="chat">
<div className="chat__header">
...
</div>
<div className="chat__body">
<p className="chat__message">
<span className="chat__name">Nabendu</span>
This is a message
<span className="chat__timestamp">
{new Date().toUTCString()}
</span>
</p>
<p className="chat__message chat__receiver">
<span className="chat__name">Parag</span>
This is a message back
<span className="chat__timestamp">
{new Date().toUTCString()}
</span>
</p>
<p className="chat__message">
<span className="chat__name">Nabendu</span>
This is a message again again
<span className="chat__timestamp">
{new Date().toUTCString()}
</span>
</p>
</div>
<div className="chat__footer"></div>
</div>
)
}
export default Chat在Chat.css文件中添加样式。
.chat__message{
position: relative;
font-size: 16px;
padding: 10px;
width: fit-content;
border-radius: 10px;
background-color: #ffffff;
margin-bottom: 30px;
}
.chat__receiver{
margin-left: auto;
background-color: #dcf8c6;
}
.chat__timestamp{
margin-left: 10px;
font-size: xx-small;
}
.chat__name{
position: absolute;
top: -15px;
font-weight: 800;
font-size: xx-small;
}图 4-8 显示了本地主机上的三条消息。
图 4-8
聊天消息
让我们完成chat__footer div。表单中还有两个图标和一个输入框。Chat.js 的更新代码用粗体标记。
...
import { AttachFile, MoreVert, SearchOutlined, InsertEmoticon } from '@material-ui/icons'
import MicIcon from '@material-ui/icons/Mic'
import './Chat.css'
...
const Chat = () => {
...
return (
<div className="chat">
<div className="chat__header">
...
</div>
<div className="chat__body">
...
</div>
<div className="chat__footer">
<InsertEmoticon />
<form>
<input
placeholder="Type a message"
type="text"
/>
<button type="submit">Send a message</button>
</form>
<MicIcon />
</div>
</div>
)
}
export default Chat是时候设计这个chat__footer div 了。注意按钮的display: none。因为它被包装在一个表单中,所以您可以在其中使用 enter。在Chat.css文件中添加以下内容。
.chat__footer{
display: flex;
justify-content: space-between;
align-items:center;
height: 62px;
border-top: 1px solid lightgray;
}
.chat__footer > form {
flex: 1;
display: flex;
}
.chat__footer > form > input {
flex: 1;
outline-width: 0;
border-radius: 30px;
padding: 10px;
border: none;
}
.chat__footer > form > button {
display: none;
}
.chat__footer > .MuiSvgIcon-root {
padding: 10px;
color: gray;
}图 4-9 显示了本地主机上的页脚。
图 4-9
页脚完成
让我们转到后端,从 Node.js 代码开始。打开一个新的终端窗口,在根目录下创建一个新的messaging-app-backend文件夹。移动到messaging-app-backend目录后,输入git init命令,这是 Heroku 稍后需要的。
mkdir messaging-app-backend
cd messaging-app-backend
git init接下来,通过在终端中输入npm init命令来创建package.json文件。你被问了一堆问题;对于大多数情况,只需按下回车键。你可以提供描述和作者,但不是强制的。你一般在server.js做进入点,这是标准的(见图 4-10 )。
图 4-10
初始后端设置
一旦package.json被创建,您需要创建包含node_modules的.gitignore文件,因为您不想以后将 node_modules 推送到 Heroku。以下是.gitignore文件内容。
node_modules接下来,打开package.json.需要在 Node.js 中启用类似 React 的导入,包括一个启动脚本来运行server.js文件。更新的内容用粗体标记。
{
"name": "messaging-app-backend",
"version": "1.0.0",
"description": "Messaging app backend",
"main": "server.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"author": "Nabendu Biswas",
"license": "ISC"
}最后,您需要在启动之前安装两个软件包。打开终端,在messaging-app-backend文件夹中安装 Express 和 Mongoose。
npm i express mongooseMongoDB 的设置与第 1 章中描述的相同。按照这些说明,创建一个名为 messaging-app-mern 的新项目。
在继续之前,将nodemon安装在messaging-app-backend文件夹中。它帮助 server.js 中的更改即时重启 Node 服务器。
npm i nodemon在messaging-app-backend文件夹中创建一个server.js文件,在这里导入 Express 和 Mongoose 包。然后使用 Express 创建一个运行在端口 9000 上的port变量。
第一个 API 端点是一个由app.get()创建的简单 GET 请求,如果成功,它会显示文本 Hello TheWebDev 。
然后,用app.listen()监听端口。
import express from 'express'
import mongoose from 'mongoose'
//App Config
const app = express()
const port = process.env.PORT || 9000
//Middleware
//DB Config
//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))在终端输入 nodemon server.js 查看监听 localhost: 9000 控制台日志。为了检查路线是否正常工作,转到http://localhost:9000/查看终点文本,如图 4-11 所示。
图 4-11
初始路线
在 MongoDB 中,您需要创建一个数据库用户并授予网络访问权限。该过程与第 1 章中的解释相同。遵循这些说明,然后获取用户凭证和连接 URL。
在server.js文件中,创建一个connection_url变量,并将 URL 粘贴到 MongoDB 的字符串中。您需要提供之前保存的密码和数据库名称。
更新后的代码用粗体标记。
...
//App Config
const app = express()
const port = process.env.PORT || 9000
const connection_url = ' mongodb+srv://admin:<password>@cluster0.ew283.mongodb.net/messagingDB?retryWrites=true&w=majority'
//Middleware
//DB Config
mongoose.connect(connection_url, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
})
//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
...现在让我们创建 MongoDB 所需的模式文件。它告诉您字段在 MongoDB 中的存储方式。在messaging-app-backend文件夹中创建一个dbMessages.js文件。
这里,messagingmessages被认为是一个集合名,您在数据库中存储一个类似于messagingSchema的值。它由一个带有消息、名称、时间戳和接收密钥的对象组成。
import mongoose from 'mongoose'
const messagingSchema = mongoose.Schema({
message: String,
name: String,
timestamp: String,
received: Boolean
})
export default mongoose.model('messagingmessages', messagingSchema)现在,您可以使用该模式来创建向数据库添加数据的端点。
在server.js中,创建一个到/messages/new端点的 POST 请求。负载在req.body到 MongoDB。然后用create()发送dbMessage。如果成功,您会收到状态 201;否则,您会收到状态 500。
接下来,创建/messages/sync的 GET 端点,从数据库中获取数据。你在这里用的是find()。如果成功,您将收到状态 200(否则,状态 500)。
更新后的代码用粗体标记。
import express from 'express'
import mongoose from 'mongoose'
import Messages from './dbMessages.js'
...
//API Endpoints
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
app.post('/messages/new', (req, res) => {
const dbMessage = req.body
Messages.create(dbMessage, (err, data) => {
if(err)
res.status(500).send(err)
else
res.status(201).send(data)
})
})
app.get('/messages/sync', (req, res) => {
Messages.find((err, data) => {
if(err) {
res.status(500).send(err)
} else {
res.status(200).send(data)
}
})
})
//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))要查看路线,请使用 Postman 应用。下载并安装它。
向http://localhost:9000发送 GET 请求,检查是否是邮递员发送的,如图 4-12 所示。
图 4-12
初始 GET 请求
在处理 POST 请求之前,您需要完成两件事情。第一,实行 First 否则,在部署应用时会出现跨来源错误。打开终端,在messaging-app-backend文件夹中安装 CORS。
npm i cors在server.js中,导入 CORS,然后配合app.use()使用。你还需要使用express.json()中间件。更新后的代码用粗体标记。
import express from 'express'
import mongoose from 'mongoose'
import Cors from 'cors'
import Messages from './dbMessages.js'
...
//Middleware
app.use(express.json())
app.use(Cors())
...在 Postman 中,您需要将请求更改为 POST,然后添加http://localhost:9000/messages/new端点。
接下来,点击车身并选择 raw 。从下拉菜单中选择 JSON(应用/json) 。在文本编辑器中,输入如图 4-13 所示的数据。通过在关键字中添加双引号来生成数据 JSON。
图 4-13
发布请求
接下来,点击发送按钮。如果一切正确,你得到状态:201 已创建,如图 4-13 所示。
我同样地插入了其他数据,但是用收到的作为真的。您需要测试 GET /messages/sync端点。将请求更改为 GET 并点击发送按钮。如果一切正常,您将获得状态:200 OK ,如图 4-14 所示。
图 4-14
获取请求
有时,POST 请求会出现服务器错误。错误为UnhandledPromiseRejectionWarning:MongooseServerSelectionError:connection。如果你得到这个错误,去你的网络访问标签,点击添加 IP 地址按钮。之后点击添加当前 IP 地址按钮,然后点击确认,如图 4-15 所示。
图 4-15
网络错误修复
既然 MongoDB 不是实时数据库,那就该给 app 加一个 pusher 来获取实时数据了。前往 https://pusher.com 报名。推杆 app 仪表盘如图 4-16 所示。点击管理按钮。
图 4-16
推杆仪表板
在下一个界面,点击创建 app 按钮,如图 4-17 所示。
图 4-17
在 Pusher 中创建应用
在弹出窗口中,将应用命名为 messaging-app-mern 。前端是 React,后端是 Node.js,如图 4-18 所示。
图 4-18
前端和后端
在下一个屏幕中,您将获得推杆前端和后端的代码,如图 4-19 所示。
图 4-19
后端代码
如前一节所述,您需要停止服务器并安装 Pusher。在messaging-app-backend文件夹中,用下面的命令安装它。
npm i pusher在server.js文件中,导入它,然后使用推动器初始化代码。从 Pusher 网站获取初始化代码( https://pusher.com )。要添加代码,用db.once打开一个数据库连接。然后用watch()观看来自 MongoDB 的消息集合。
在changeStream里面,如果operationType被插入,你把数据插入到推动器里。更新后的代码用粗体标记。
...
import Pusher from 'pusher'
...
//App Config
const app = express()
const port = process.env.PORT || 9000
const connection_url = ' mongodb+srv://admin:<password>@cluster0.ew283.mongodb.net/messagingDB?retryWrites=true&w=majority'
const pusher = new Pusher({
appId: "11xxxx",
key: "9exxxxxxxxxxxxx",
secret: "b7xxxxxxxxxxxxxxx",
cluster: "ap2",
useTLS: true
});
//API Endpoints
const db = mongoose.connection
db.once("open", () => {
console.log("DB Connected")
const msgCollection = db.collection("messagingmessages")
const changeStream = msgCollection.watch()
changeStream.on('change', change => {
console.log(change)
if(change.operationType === "insert") {
const messageDetails = change.fullDocument
pusher.trigger("messages", "inserted", {
name: messageDetails.name,
message: messageDetails.message,
timestamp: messageDetails.timestamp,
received: messageDetails.received
})
} else {
console.log('Error trigerring Pusher')
}
})
})
app.get("/", (req, res) => res.status(200).send("Hello TheWebDev"))
...
//Listener
app.listen(port, () => console.log(`Listening on localhost: ${port}`))为了测试这一点,您需要从 Postman 发送一个 POST 请求。同时,你需要在调试控制台中推料。
图 4-20 显示了调试控制台日志中显示的消息。
图 4-20
推送器中的消息
在服务器中,控制台日志显示相同,如图 4-21 所示。
图 4-21
服务器日志
是时候回到前端使用 Pusher 了。首先,你需要在messaging-app-frontend文件夹中安装pusher-js包。
npm i pusher-js使用以下代码,并在App.js文件的前端插入新数据。更新的内容用粗体标记。
...
import React, { useEffect, useState } from 'react'
import Pusher from 'pusher-js'
function App() {
const [messages, setMessages] = useState([])
useEffect(() => {
const pusher = new Pusher('9exxxxxxxxxxxx', {
cluster: 'ap2'
});
const channel = pusher.subscribe('messages');
channel.bind('inserted', (data) => {
setMessages([...messages, data])
});
return () => {
channel.unbind_all()
channel.unsubscribe()
}
}, [messages])
console.log(messages)
return (
<div className="app">
...
</div>
);
}
export default App;去找邮递员并发送另一个邮寄请求。图 4-22 显示了本地主机上控制台日志的数据。
图 4-22
控制台日志
你想在应用初始加载时获取所有消息,然后推送消息。您必须达到 GET 端点,为此您需要 Axios。打开messaging-app-frontend文件夹并安装。
npm i axios接下来,在components文件夹中创建一个新的axios.js文件,并创建一个axios的实例。基础 URL 是http://localhost:9000。
import axios from 'axios'
const instance = axios.create({
baseURL: "http://localhost:9000"
})
export default instance接下来,返回到App.js,首先包含本地axios。然后使用useEffect钩子中的axios从/messages/sync端点获取所有数据。收到消息后,通过setMessages()进行设置。最后,将消息作为道具传递给聊天组件。
更新的内容用粗体标记。
...
import axios from './components/axios'
function App() {
const [messages, setMessages] = useState([])
useEffect(() => {
axios.get("/messages/sync").then(res => {
setMessages(res.data)
})
}, [])
useEffect(() => {
...
}, [messages])
return (
<div className="app">
<div className="app__body">
<Sidebar />
<Chat messages={messages} />
</div>
</div>
);
}
export default App;在Chat.js文件中,使用这条消息的道具并通过它映射到屏幕上显示。
如果消息包含received键,则添加chat__receiver类。更新的内容用粗体标记。
...
const Chat = ({ messages }) => {
const [seed, setSeed] = useState("")
useEffect(() => {
setSeed(Math.floor(Math.random() * 5000))
}, [])
return (
<div className="chat">
<div className="chat__header">
...
</div>
<div className="chat__body">
{messages.map(message => (
<p className={`chat__message ${message.received && 'chat__receiver'}`}>
<span className="chat__name">{message.name}</span>
{message.message}
<span className="chat__timestamp">
{message.timestamp}
</span>
</p>
))}
</div>
<div className="chat__footer">
...
</div>
</div>
)
}
export default Chat你可以在 localhost 上看到所有的消息。如果你通过 Postman 发布了一条新消息,你会在聊天中得到它,如图 4-23 所示。
图 4-23
新消息
添加直接从消息框发布的逻辑。首先,导入局部axios,然后创建一个输入状态变量。
然后在输入上做值和 onChange React 的事情,并在按钮的onClick事件处理程序上附加一个sendMessage函数。
在sendMessage函数中,使用所需的数据对/messages/new端点进行 POST 调用。Chat.js中更新的内容用粗体标出。
import axios from './axios'
...
const Chat = ({ messages }) => {
const [seed, setSeed] = useState("")
const [input, setInput] = useState("")
const sendMessage = async (e) => {
e.preventDefault()
await axios.post('/messages/new', {
message: input,
name: "thewebdev",
timestamp: new Date().toUTCString(),
received: true
})
setInput("")
}
useEffect(() => {
setSeed(Math.floor(Math.random() * 5000))
}, [])
return (
<div className="chat">
<div className="chat__header">
...
</div>
<div className="chat__body">
...
</div>
<div className="chat__footer">
<InsertEmoticon />
<form>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Type a message"
type="text"
/>
<button onClick={sendMessage} type="submit">Send a message</button>
</form>
<MicIcon />
</div>
</div>
)
}
export default Chat您可以在输入框中键入文本,当您按下 Enter 键时,该消息会立即显示在聊天中,如图 4-24 所示。
图 4-24
来自输入的消息
接下来,让我们将 Google 身份验证添加到项目中,以便用户可以使用他们的 Google 帐户登录。
对于 Google 身份验证,您需要在 Firebase 控制台中进行额外的设置。点击屏幕右上角的设置图标。之后点击项目设置按钮,如图 4-25 所示。
图 4-25
附加设置
在下一页中,点击页面底部的 web 图标,如图 4-26 所示。
图 4-26
网络图标
在下一页,输入应用的名称(在我的例子中是 messaging-app-mern )。选中 Firebase hosting 复选框。点击注册 app 按钮(见图 4-27 )。
图 4-27
Firebase 托管
在下一页,点击下一个按钮(见图 4-28 )。
图 4-28
下一个屏幕
在下一页,从终端运行firebase-tools全局安装 Firebase。注意,这是机器上的一次性设置,因为它与-g选项一起使用(见图 4-29 )。
图 4-29
全局安装
忽略下一组命令,点击继续到控制台按钮(见图 4-30 )。
图 4-30
继续
接下来,向下滚动页面并选择配置单选按钮。然后复制firebaseConfig数据,如图 4-31 所示。
图 4-31
配置详细信息
在 Visual Studio 代码中打开代码,并在src文件夹中创建一个firebase.js文件。粘贴 VSCode 中的内容。
初始化 Firebase 应用并使用数据库。使用 Firebase 中的auth, provider。以下是firebase.js内容。
import firebase from 'firebase/app';
import 'firebase/auth'; // for authentication
import 'firebase/storage'; // for storage
import 'firebase/database'; // for realtime database
import 'firebase/firestore'; // for cloud firestore
const firebaseConfig = {
apiKey: "Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authDomain: "messaging-xxxxxxxxxxxxxxxx.com",
projectId: "messaging-xxxxx",
storageBucket: "messaging-app-xxxxxxxxxxxxxxxxx",
messagingSenderId: "83xxxxxxxxxxxx",
appId: "1:836xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};
const firebaseApp = firebase.initializeApp(firebaseConfig)
const db = firebaseApp.firestore()
const auth = firebase.auth()
const provider = new firebase.auth.GoogleAuthProvider()
export { auth, provider }
export default db在终端中,您需要在messaging-app-frontend文件夹中安装所有 Firebase 依赖项。
npm i firebase在components文件夹中创建两个文件Login.js和Login.css。在Login.js文件中,有一个简单的功能组件,显示一个徽标和一个用 Google 按钮登录的**。以下是Login.js的内容。**
import React from 'react'
import { Button } from '@material-ui/core'
import './Login.css'
const Login = () => {
const signIn = () => {
}
return (
<div className="login">
<div className="login__container">
<img src="logo512.png" alt="whatsapp" />
<div className="login__text">
<h1>Sign in to Messaging App</h1>
</div>
<Button onClick={signIn}>Sign In with Google</Button>
</div>
</div>
)
}
export default Login让我们在Login.css文件中创建样式。以下是Login.css内容。
.login{
background-color: #f8f8f8;
height: 100vh;
width: 100vw;
display: grid;
place-items: center;
}
.login__container{
padding: 100px;
text-align: center;
background-color: white;
border-radius: 10px;
box-shadow: -1px 4px 20px -6px rgba(0, 0, 0, 0.75);
}
.login__container > img {
object-fit: contain;
height: 100px;
margin-bottom: 40px;
}
.login__container > button {
margin-top: 50px;
text-transform: inherit !important;
background-color: #0a8d48 !important;
color: white;
}接下来,让我们展示一个没有用户的登录组件。创建一个临时状态变量,并将其显示在App.js文件中。更新的内容用粗体标记。
...
import Login from './components/Login';
function App() {
const [messages, setMessages] = useState([])
const [user, setUser] = useState(null)
...
return (
<div className="app">
{ !user ? <Login /> : (
<div className="app__body">
<Sidebar />
<Chat messages={messages} />
</div>
)}
</div>
);
}
export default App;图 4-32 显示了本地主机上的登录屏幕。
图 4-32
登录屏幕
使用登录方式前,返回 Firebase,点击认证选项卡,然后点击开始按钮,如图 4-33 所示。
图 4-33
开始
在下一个界面中,点击谷歌认证的编辑配置图标,如图 4-34 所示。
图 4-34
谷歌登录
在弹出窗口中,点击启用按钮。接下来,输入你的 Gmail id,点击保存按钮(见图 4-35 )。
图 4-35
启用 Google 登录
...
import { auth, provider } from '../firebase'
const Login = () => {
const signIn = () => {
auth.signInWithPopup(provider)
.then(result => console.log(result))
.catch(error => alert(error.message))
}
return (
<div className="login">
...
</div>
)
}
export default Login接下来,在Login.js文件中,需要从本地 Firebase 文件导入auth, provider。之后,使用signInWithPopup()方法得到结果。更新的内容用粗体标记。
点击 localhost 上的用 Google 按钮登录。将打开一个 Gmail 身份验证弹出窗口。点击用户名后,在控制台中可以看到登录用户的所有信息,如图 4-36 所示。
图 4-36
Google 认证成功
让我们将用户数据分派到数据层,这里 Redux/Context API 开始发挥作用。
您希望用户信息存储在全局状态中。首先,创建一个新的StateProvider.js文件。使用 useContext API 创建一个StateProvider函数。以下是内容。你可以在 www.youtube.com/watch?v=oSqqs16RejM 的我的 React hooks YouTube 视频中了解更多关于useContext钩子的信息。
import React, { createContext, useContext, useReducer } from "react"
export const StateContext = createContext()
export const StateProvider = ({ reducer, initialState, children }) => (
<StateContext.Provider value={useReducer(reducer, initialState)}>
{children}
</StateContext.Provider>
)
export const useStateValue = () => useContext(StateContext)接下来,在components文件夹中创建一个reducer.js文件。这是一个类似于 Redux 组件中的 reducer 的概念。您可以在 www.youtube.com/watch?v=m0G0R0TchDY 了解更多信息。以下是内容。
export const initialState = { user: null }
export const actionTypes = {
SET_USER: "SET_USER"
}
const reducer = (state, action) => {
console.log(action)
switch(action.type) {
case actionTypes.SET_USER:
return {
...state,
user: action.user
}
default:
return state
}
}
export default reducer在index.js文件中,导入所需文件后,用StateProvider组件包装 app 组件。更新的内容用粗体标记。
...
import { StateProvider } from './components/StateProvider';
import reducer, { initialState } from './components/reducer';
ReactDOM.render(
<React.StrictMode>
<StateProvider initialState={initialState} reducer={reducer}>
<App />
</StateProvider>
</React.StrictMode>,
document.getElementById('root')
);当你从 Google 取回用户数据时,你在Login.js文件中将它调度到 reducer,它存储在数据层。
这里,useStateValue是一个钩子。事实上,它是一个自定义钩子的例子。更新的内容用粗体标记。
...
import { actionTypes } from './reducer'
import { useStateValue } from './StateProvider'
const Login = () => {
const [{}, dispatch] = useStateValue()
const signIn = () => {
auth.signInWithPopup(provider)
.then(result => {
dispatch({
type: actionTypes.SET_USER,
user: result.user
})
})
.catch(error => alert(error.message))
}
return (
<div className="login">
...
</div>
)
}
export default Login在App.js文件中,使用useStateValue钩子,从中提取全局用户。然后,你基于它登录。更新的内容用粗体标记。
...
import { useStateValue } from './components/StateProvider';
function App() {
const [messages, setMessages] = useState([])
const [{ user }, dispatch] = useStateValue()
...
return (
<div className="app">
...
</div>
);
}
export default App;如果你在 localhost 上登录,你会被带到应用,如图 4-37 所示。
图 4-37
已登录
你可以访问用户的数据,所以你可以在任何地方使用它。让我们使用用户的 Google 图片作为Sidebar.js文件中的头像。让我们去掉多余的房间,因为这个项目只有一个房间,每个人都可以聊天。
更新的内容用粗体标记。
...
import { useStateValue } from './StateProvider';
const Sidebar = () => {
const [{ user }, dispatch] = useStateValue()
return (
<div className="sidebar">
<div className="sidebar__header">
<Avatar src={user?.photoURL} />
<div className="sidebar__headerRight">
...
</div>
</div>
<div className="sidebar__search">
...
</div>
<div className="sidebar__chats">
<SidebarChat />
</div>
</div>
)
}
export default Sidebar图 4-38 在 localhost 的页面左上角显示了登录用户的 Google 图片。
图 4-38
登录映像
在Chat.js,中,使用useStateValue钩子获取用户的显示名称。然后检查 message.name 是否等于user.displayName以显示chat__receiver类。修复上次出现的硬编码**...Chat.js文件中chat__header的消息;更新以显示最后一个人发信息的时间。同时将房间名称更改为开发帮助**。
更新的内容用粗体标记。
...
import { useStateValue } from './StateProvider';
const Chat = ({ messages }) => {
...
const [{ user }, dispatch] = useStateValue()
const sendMessage = async (e) => { e.preventDefault()
await axios.post('/messages/new', {
message: input,
name: user.displayName,
timestamp: new Date().toUTCString(),
received: true
})
setInput("")
}
...
return (
<div className="chat">
<div className="chat__header">
<Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
<div className="chat__headerInfo">
<h3>Dev Help</h3>
<p>Last seen at {" "}
{messages[messages.length -1]?.timestamp}
</p>
</div>
</div>
<div className="chat__body">
{messages.map(message => (
<p className={`chat__message ${message.name === user.displayName && 'chat__receiver'}`}>
...
</p>
))}
</div>
<div className="chat__footer">
...
</div>
</div>
)
}
export default Chat键入一些内容,然后单击 Enter。您可以看到消息已收到。图 4-39 显示场景已经更新。
图 4-39
时间更新
最后要改变的是侧边栏中的硬编码消息。你需要在这里显示最后一条消息。首先,将消息从App.js文件发送到侧栏组件。
更新的内容用粗体标记。
...
function App() {
...
return (
<div className="app">
{ !user ? <Login /> : (
<div className="app__body">
<Sidebar messages={messages} />
<Chat messages={messages} />
</div>
)}
</div>
);
}
export default App;之后,从Sidebar.js文件到SidebarChat组件。更新的内容用粗体标记。
...
const Sidebar = ({ messages }) => {
const [{ user }, dispatch] = useStateValue()
return (
<div className="sidebar">
<div className="sidebar__header">
...
</div>
<div className="sidebar__search">
...
</div>
<div className="sidebar__chats">
<SidebarChat messages={messages} />
</div>
</div>
)
}
export default Sidebar最后,在SidebarChat.js文件中,显示最后一条消息而不是硬编码的消息,并将房间名改为 Dev Help 。
更新的内容用粗体标记。
...
const SidebarChat = ({ messages }) => {
...
return (
<div className="sidebarChat">
<Avatar src={`https://avatars.dicebear.com/api/human/b${seed}.svg`} />
<div className="sidebarChat__info">
<h2>Dev Help</h2>
<p>{messages[messages.length -1]?.message}</p>
</div>
</div>
)
}
export default SidebarChat应用已完成。图 4-40 显示了侧边栏中的最新消息。我还在不同的谷歌账户中测试了我的登录。
图 4-40
应用完成
转到 www.heroku.com 部署后端。按照你在第 1 章中所做的相同步骤,创建一个名为消息传递-应用-后端的应用。
部署成功后,进入 https://messaging-app-backend.herokuapp.com 。图 4-41 显示了正确的文本。
图 4-41
初始路线检查
在axios.js中,将端点改为 https://messaging-app-backend.herokuapp.com 。如果一切正常,你的应用应该可以运行了。
import axios from 'axios'
const instance = axios.create({
baseURL: " https://messaging-app-backend.herokuapp.com "
})
export default instance是时候在 Firebase 中部署前端了。遵循与第 1 章相同的程序。完成此过程后,站点应处于活动状态并正常工作,如图 4-42 所示。
图 4-42
最终应用
在这一章中,你创建了一个简单而实用的聊天应用。Firebase 在网上主办的。您学习了添加 Google 身份验证,通过它您可以使用 Google 帐户登录。您还学习了使用 Node.js 创建的 API 路由将聊天存储在 MongoDB 数据库中。









































