您的位置:首页 > Web前端 > Node.js

nodejs入门_如何在NodeJS中使用套接字创建专业的Chat API解决方案[入门级]

2020-08-21 01:56 986 查看

nodejs入门

Have you ever wondered how chat applications work behind the scenes? Well, today I am going to walk you through how to make a REST + Sockets-based application built on top of NodeJS/ExpressJS using MongoDB.

您是否想过聊天应用程序在后台如何工作? 好吧,今天,我将向您介绍如何使用MongoDBNodeJS / ExpressJS之上构建基于REST +套接字的应用程序。

I have been working on the content for this article for over a week now – I really hope it helps someone out there.

我一直在研究本文的内容已有一个多星期了,我真的希望它能对那里的人们有所帮助。

先决条件 (Prerequisites)

我们将讨论的主题 (Topics we'll cover)

一般 (General)

  • Create an express server

    创建快递服务器
  • How to do API validations

    如何进行API验证
  • Create basic skeleton for the entire application

    为整个应用程序创建基本框架
  • Setting up MongoDB (installation, setup in express)

    设置MongoDB(安装,在Express中设置)
  • Creating users API + Database (Create a user, Get a user by id, Get all users, Delete a user by id)

    创建用户API +数据库(创建用户,按ID获取用户,获取所有用户,按ID删除用户)
  • Understanding what a middleware is

    了解什么是中间件
  • JWT (JSON web tokens) authentication (decode/encode) - Login middleware

    JWT(JSON Web令牌)认证(解码/编码)-登录中间件
  • Web socket class that handles events when a user disconnects, adds its identity, joins a chat room, wants to mute a chat room

    Web套接字类,可在用户断开连接时处理事件,添加其身份,加入聊天室,想要使聊天室静音
  • Discussing chat room & chat message database model

    讨论聊天室和聊天消息数据库模型

对于API (For the API)

  • Initiate a chat between users

    在用户之间发起聊天
  • Create a message in chat room

    在聊天室中创建消息
  • See conversation for a chat room by its id

    通过ID查看聊天室的对话
  • Mark an entire conversation as read (similar to Whatsapp)

    将整个对话标记为已读(类似于Whatsapp)
  • Get recent conversation from all chats (similar to Facebook messenger)

    从所有聊天中获取最近的对话(类似于Facebook Messenger)

奖金-API (Bonus  - API    )

  • Delete a chat room by id along with all its associated messages

    按ID删除聊天室及其所有关联消息
  • Delete a message by id

    按ID删除邮件

Before we begin, I wanted to touch on some basics in the following videos.

在开始之前,我想介绍以下视频中的一些基础知识。

了解ExpressJS的基础 (Understanding the basics of ExpressJS)

What are routes? Controllers? How do we allow for CORS (cross origin resource sharing)? How do we allow enduser to send data in JSON format in API request?

什么是路线? 控制器? 我们如何允许CORS(跨源资源共享)? 我们如何允许最终用户在API请求中以JSON格式发送数据?

I talk about all this and more (including REST conventions) in this video:

我在视频中谈到了所有这些以及更多内容(包括REST约定):

Also, here's a GitHub link to the entire source code of this video [Chapter 0]

另外,这是该视频的完整源代码的GitHub链接 [第0章]

Do have a look at the README.md for "Chapter 0" source code. It has all the relevant learning links I mention in the video along with an amazing half hour tutorial on postman.

请查看“第0章”源代码的README.md。 它包含了我在视频中提到的所有相关学习链接,以及有关邮递员的令人惊叹的半小时教程。

将API验证添加到您的API端点 (Adding API validation to your API end-point )

In the below video, you'll learn how to write your own custom validation using a library called "make-validation":

在下面的视频中,您将学习如何使用名为“ make-validation”的库编写自己的自定义验证:

Here's the GitHub link to the entire source code of this video [Chapter 0].

是该视频的完整源代码GitHub链接 [第0章]。

And here's the make-validation library link [GitHub][npm][example].

这是制作验证库链接[G itHub ] [ npm ] [ 示例 ]。

The entire source code of this tutorial can be found here. If you have any feedback, please just reach out to me on http://twitter.com/adeelibr. If you like this tutorial kindly leave a star on the github repository.

本教程的完整源代码可以在这里找到。 如果您有任何反馈意见,请访问http://twitter.com/adeelibr与我联系。 如果您喜欢本教程,请在github存储库上加一个星号

Let's begin now that you know the basics of ExpressJS and how to validate a user response.

现在,让我们开始了解ExpressJS的基础知识以及如何验证用户响应。

入门 (Getting started)

Create a folder called

chat-app
:

创建一个名为

chat-app
的文件夹:

mkdir chat-app;
cd chat-app;

Next initialize a new npm project in your project root folder by typing the following:

接下来,通过键入以下命令在项目根文件夹中初始化一个新的npm项目:

npm init -y

and install the following packages:

并安装以下软件包:

npm i cors @withvoid/make-validation express jsonwebtoken mongoose morgan socket.io uuid --save;
npm i nodemon --save-dev;

And in your

package.json
scripts
section add the following 2 scripts:

然后在您的

package.json
scripts
部分中添加以下2个脚本:

"scripts": {
"start": "nodemon server/index.js",
"start:server": "node server/index.js"
},

Your

package.json
now should look something like this:

您的

package.json
现在应如下所示:

{
"name": "chapter-1-chat",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "nodemon server/index.js",
"start:server": "node server/index.js"
},
"dependencies": {
"@withvoid/make-validation": "1.0.5",
"cors": "2.8.5",
"express": "4.16.1",
"jsonwebtoken": "8.5.1",
"mongoose": "5.9.18",
"morgan": "1.9.1",
"socket.io": "2.3.0",
"uuid": "8.1.0"
},
"devDependencies": {
"nodemon": "2.0.4"
}
}

Awesome!

太棒了!

Now in your project's root folder create a new folder called

server
:

现在,在项目的根文件夹中,创建一个名为

server
的新文件夹:

cd chat-app;
mkdir server;
cd server;

Inside your

server
folder create a file called
index.js
and add the following content to it:

server
文件夹中,创建一个名为
index.js
的文件,并向其中添加以下内容:

import http from "http";
import express from "express";
import logger from "morgan";
import cors from "cors";
// routes
import indexRouter from "./routes/index.js";
import userRouter from "./routes/user.js";
import chatRoomRouter from "./routes/chatRoom.js";
import deleteRouter from "./routes/delete.js";
// middlewares
import { decode } from './middlewares/jwt.js'

const app = express();

/** Get port from environment and store in Express. */
const port = process.env.PORT || "3000";
app.set("port", port);

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use("/", indexRouter);
app.use("/users", userRouter);
app.use("/room", decode, chatRoomRouter);
app.use("/delete", deleteRouter);

/** catch 404 and forward to error handler */
app.use('*', (req, res) => {
return res.status(404).json({
success: false,
message: 'API endpoint doesnt exist'
})
});

/** Create HTTP server. */
const server = http.createServer(app);
/** Listen on provided port, on all network interfaces. */
server.listen(port);
/** Event listener for HTTP server "listening" event. */
server.on("listening", () => {
console.log(`Listening on port:: http://localhost:${port}/`)
});

Let's add the routes for

indexRouter
userRouter
chatRoomRouter
&
deleteRouter
.

让我们为

indexRouter
userRouter
chatRoomRouter
deleteRouter
添加路由。

In your project's root folder create a folder called

routes
. Inside the
routes
folder add the following files:

在项目的根文件夹中,创建一个名为

routes
的文件夹。 在
routes
文件夹内,添加以下文件:

  • index.js

    index.js

  • user.js

    user.js

  • chatRoom.js

    chatRoom.js

  • delete.js

    delete.js

Let's add content for

routes/index.js
first:

让我们首先添加

routes/index.js
内容:

import express from 'express';
// controllers
import users from '../controllers/user.js';
// middlewares
import { encode } from '../middlewares/jwt.js';

const router = express.Router();

router
.post('/login/:userId', encode, (req, res, next) => { });

export default router;

Let's add content for

routes/user.js
next:

接下来让我们为

routes/user.js
添加内容:

import express from 'express';
// controllers
import user from '../controllers/user.js';

const router = express.Router();

router
.get('/', user.onGetAllUsers)
.post('/', user.onCreateUser)
.get('/:id', user.onGetUserById)
.delete('/:id', user.onDeleteUserById)

export default router;

And now let's add content for

routes/chatRoom.js
:

现在让我们为

routes/chatRoom.js
添加内容:

import express from 'express';
// controllers
import chatRoom from '../controllers/chatRoom.js';

const router = express.Router();

router
.get('/', chatRoom.getRecentConversation)
.get('/:roomId', chatRoom.getConversationByRoomId)
.post('/initiate', chatRoom.initiate)
.post('/:roomId/message', chatRoom.postMessage)
.put('/:roomId/mark-read', chatRoom.markConversationReadByRoomId)

export default router;

Finally, let's add content for

routes/delete.js
:

最后,让我们为

routes/delete.js
添加内容:

import express from 'express';
// controllers
import deleteController from '../controllers/delete.js';

const router = express.Router();

router
.delete('/room/:roomId', deleteController.deleteRoomById)
.delete('/message/:messageId', deleteController.deleteMessageById)

export default router;

Awesome now that our routes are in place let's add the controllers for each route.

现在我们的路由已经到位,让我们为每个路由添加控制器。

Create a new folder called

controllers
. Inside that folder create the following files:

创建一个名为

controllers
的新文件夹。 在该文件夹中,创建以下文件:

  • user.js

    user.js

  • chatRoom.js

    chatRoom.js

  • delete.js

    delete.js

Let's start of with

controllers/user.js
:

让我们从

controllers/user.js

export default {
onGetAllUsers: async (req, res) => { },
onGetUserById: async (req, res) => { },
onCreateUser: async (req, res) => { },
onDeleteUserById: async (req, res) => { },
}

Next let's add content in

controllers/chatRoom.js
:

接下来,让我们在

controllers/chatRoom.js
添加内容:

export default {
initiate: async (req, res) => { },
postMessage: async (req, res) => { },
getRecentConversation: async (req, res) => { },
getConversationByRoomId: async (req, res) => { },
markConversationReadByRoomId: async (req, res) => { },
}

And finally let's add content for

controllers/delete.js
:

最后,让我们为

controllers/delete.js
添加内容:

export default {
deleteRoomById: async (req, res) => {},
deleteMessageById: async (req, res) => {},
}

So far we have added empty controllers for each route, so they don't do much yet. We'll add functionality in a bit.

到目前为止,我们已经为每个路由添加了空控制器,因此它们还没有做太多事情。 我们将稍后添加功能。

Just one more thing – let's add a new folder called

middlewares
and inside that folder create a file called
jwt.js
. Then add the following content to it:

只是一件事-让我们添加一个名为

middlewares
的新文件夹,并在该文件夹内创建一个名为
jwt.js
的文件。 然后向其中添加以下内容:

import jwt from 'jsonwebtoken';

export const decode = (req, res, next) => {}

export const encode = async (req, res, next) => {}

I will talk about what this file does in a bit, so for now let's just ignore it.

我将稍后讨论该文件的功能,所以现在让我们忽略它。

We have ended up doing the following:

我们最终做了以下工作:

  • Created an Express server that listens on port 3000

    创建了一个侦听端口3000的Express服务器
  • Added cross-origin-resource (CORS) to our

    server.js

    在我们的

    server.js
    添加了跨源资源(CORS)

  • Added a logger to our

    server.js

    在我们的

    server.js
    添加了一个记录器

  • And also added route handlers with empty controllers.

    并且还添加了带有空控制器的路由处理程序。

Nothing fancy so far that I haven't covered in the videos above.

到目前为止,上面的视频都还没有介绍我。

让我们在应用程序中设置MongoDB (Let's setup MongoDB in our application)

Before we add MongoDB to our code base, make sure it is installed in your machine by running one of the following:

在将MongoDB添加到我们的代码库之前,请通过运行以下操作之一确保它已安装在您的计算机中:

If you are having issues installing MongoDB, just let me know at https://twitter.com/adeelibr and I'll write a custom guide for you or make an installation video guide. :)

如果您在安装MongoDB时遇到问题,请通过https://twitter.com/adeelibr告诉我,我将为您编写自定义指南或制作安装视频指南。 :)

I am using Robo3T as my MongoDB GUI.

我正在使用Robo3T 作为我的MongoDB GUI。

Now you should have your MongoDB instance running and Robo3T installed. (You can use any GUI client that you like for this. I like Robo3T a lot so I'm using it. Also, it's open source.)

现在您应该运行MongoDB实例并运行Robo3T 已安装。 (您可以为此使用任何GUI客户端。我喜欢Robo3T 很多,所以我正在使用它。 此外,它是开源的。)

Here is a small video I found on YouTube that gives a 6 minute intro to Robo3T:

这是我在YouTube上找到的一个小视频,向您介绍了Robo3T的6分钟介绍:

Once your MongoDB instance is up and running let's begin integrating MongoDB in our code as well.

一旦您的MongoDB实例启动并运行,让我们也开始将MongoDB集成到我们的代码中。

In your root folder create a new folder called

config
. Inside that folder create a file called
index.js
and add the following content:

在您的根文件夹中,创建一个名为

config
的新文件夹。 在该文件夹中,创建一个名为
index.js
的文件,并添加以下内容:

const config = {
db: {
url: 'localhost:27017',
name: 'chatdb'
}
}

export default config

Usually the default port that

MongoDB
instances will run on is
27017
.

通常,

MongoDB
实例将在其上运行的默认端口是
27017

Here we set info about our database URL (which is in

db
) and the
name
of database which is
chatdb
(you can call this whatever you want).

在这里,我们设置有关数据库URL的信息(位于

db
)和
name
chatdb
的数据库的
name
(您可以随意调用此名称)。

Next create a new file called

config/mongo.js
and add the following content:

接下来,创建一个名为

config/mongo.js
的新文件,并添加以下内容:

import mongoose from 'mongoose'
import config from './index.js'

const CONNECTION_URL = `mongodb://${config.db.url}/${config.db.name}`

mongoose.connect(CONNECTION_URL, {
useNewUrlParser: true,
useUnifiedTopology: true
})

mongoose.connection.on('connected', () => {
console.log('Mongo has connected succesfully')
})
mongoose.connection.on('reconnected', () => {
console.log('Mongo has reconnected')
})
mongoose.connection.on('error', error => {
console.log('Mongo connection has an error', error)
mongoose.disconnect()
})
mongoose.connection.on('disconnected', () => {
console.log('Mongo connection is disconnected')
})

Next import

config/mongo.js
in your
server/index.js
file like this:

接下来像这样在您的

server/index.js
文件中导入
config/mongo.js

.
.
// mongo connection
import "./config/mongo.js";
// routes
import indexRouter from "./routes/index.js";

If you get lost at any time, the entire source code for this tutorial is right here.

如果您随时迷路,本教程的整个源代码都在这里

Let's discuss what we are doing here step by step:

让我们一步一步地讨论我们在做什么:

We first import our

config.js
file in
config/mongo.js
. Next we pass in the value to our
CONNECTION_URL
like this:

我们首先将

config.js
文件导入
config/mongo.js
。 接下来,我们将值传递给我们的
CONNECTION_URL
如下所示:

const CONNECTION_URL = `mongodb://${config.db.url}/${config.db.name}`

Then using the

CONNECTION_URL
we form a Mongo connection, by doing this:

然后使用

CONNECTION_URL
我们通过以下步骤形成一个Mongo连接:

mongoose.connect(CONNECTION_URL, {
useNewUrlParser: true,
useUnifiedTopology: true
})

This tells

mongoose
to make a connection with the database with our Node/Express application.

这告诉

mongoose
通过我们的Node / Express应用程序与数据库建立连接。

The options we are giving Mongo here are:

我们在这里为Mongo提供的选项有:

  • useNewUrlParser
    : MongoDB driver has deprecated their current connection string parser.
    useNewUrlParser: true
    tells mongoose to use the new parser by Mongo. (If it's set to true, we have to provide a database port in the
    CONNECTION_URL
    .)

    useNewUrlParser
    :MongoDB驱动程序已弃用其当前的连接字符串解析器。
    useNewUrlParser: true
    告诉猫鼬使用Mongo的新解析器。 (如果将其设置为true,则必须在
    CONNECTION_URL
    提供数据库端口。)

  • useUnifiedTopology
    : False by default. Set to
    true
    to opt in to using MongoDB driver's new connection management engine. You should set this option to
    true
    , except for the unlikely case that it prevents you from maintaining a stable connection.

    useUnifiedTopology
    :默认情况下为False。 设置为
    true
    以选择使用MongoDB驱动程序的新连接管理引擎 。 您应该将此选项设置为
    true
    ,除非极少数情况会阻止您保持稳定的连接。

Next we simply add

mongoose
event handlers like this:

接下来,我们简单地添加

mongoose
事件处理程序,如下所示:

mongoose.connection.on('connected', () => {
console.log('Mongo has connected succesfully')
})
mongoose.connection.on('reconnected', () => {
console.log('Mongo has reconnected')
})
mongoose.connection.on('error', error => {
console.log('Mongo connection has an error', error)
mongoose.disconnect()
})
mongoose.connection.on('disconnected', () => {
console.log('Mongo connection is disconnected')
})
  • connected
    will be called once the database connection is established

    建立数据库连接后将调用

    connected

  • disconnected
    will be called when your Mongo connection is disabled

    Mongo连接被禁用时,将

    disconnected
    连接

  • error
    is called if there is an error connecting to your Mongo database

    如果连接到您的Mongo数据库有

    error
    则调用error

  • reconnected
    event is called when the database loses connection and then makes an attempt to successfully reconnect.

    当数据库断开连接,然后尝试成功重新连接时,将调用

    reconnected
    事件。

Once you have this in place, simply go in your

server/index.js
file and import
config/mongo.js
. And that is it. Now when you start up your server by typing this:

完成此操作后,只需进入

server/index.js
文件并导入
config/mongo.js
。 就是这样。 现在,当您通过键入以下内容启动服务器时:

npm start;

You should see something like this:

您应该会看到以下内容:

If you see this you have successfully added Mongo to your application.

如果看到此消息,则说明您已成功将Mongo添加到您的应用程序中。

Congratulations!

恭喜你!

If you got stuck here for some reason, let me know at twitter.com/adeelibr and I will try to sort it out for you. :)

如果您由于某种原因而被卡在这里, 请通过twitter.com/adeelibr告诉我,我将尽力为您解决。 :)

让我们为用户/设置第一个API部分/ (Let's setup our first API section for users/)

The setup of our API for

users/
will have no authentication token for this tutorial, because my main focus is to teach you about the Chat application here.

在本教程中,针对

users/
的API的设置将没有身份验证令牌,因为我的主要重点是在此处向您介绍聊天应用程序。

用户模态方案 (User Modal Scheme)

Let's create our first model (database scheme) for the

user
collection.

让我们为

user
集合创建第一个模型(数据库方案)。

Create a new folder called

models
. Inside that folder create a file called
User.js
and add the following content:

创建一个名为

models
的新文件夹。 在该文件夹中,创建一个名为
User.js
的文件,并添加以下内容:

import mongoose from "mongoose";
import { v4 as uuidv4 } from "uuid";

export const USER_TYPES = {
CONSUMER: "consumer",
SUPPORT: "support",
};

const userSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
firstName: String,
lastName: String,
type: String,
},
{
timestamps: true,
collection: "users",
}
);

export default mongoose.model("User", userSchema);

Let's break this down into pieces:

让我们将其分解为几部分:

export const USER_TYPES = {
CONSUMER: "consumer",
SUPPORT: "support",
};

We are basically going to have 2 types of users,

consumer
and
support
. I have written it this way because I want to programmatically ensure API and DB validation, which I will talk about later.

我们基本上将拥有两种类型的用户:

consumer
support
。 我之所以这样写,是因为我想以编程方式确保API和DB验证,这将在后面讨论。

Next we create a schema on how a single

document
(object/item/entry/row) will look inside our
user
collection (a collection is equivalent to a MySQL table). We define it like this:

接下来,我们创建一个模式,说明单个

document
(对象/项目/条目/行)在
user
集合中的外观(一个集合等效于一个MySQL表)。 我们这样定义它:

const userSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
firstName: String,
lastName: String,
type: String,
},
{
timestamps: true,
collection: "users",
}
);

Here we are telling

mongoose
that for a single document in our
users
collection we want the structure to be like this:

在这里,我们告诉

mongoose
,对于我们的
users
集中的单个文档,我们希望结构如下所示:

{
id: String // will get random string by default thanks to uuidv4
firstName: String,
lastName: String,
type: String // this can be of 2 types consumer/support
}

In the second part of the schema we have something like this:

在模式的第二部分,我们有如下内容:

{
timestamps: true,
collection: "users",
}

Setting

timestamps
to
true
will add 2 things to my schema: a
createdAt
and a
updatedAt
date value. Every time when we create a new entry the
createdAt
will be updated automatically and
updatedAt
will update once we update an entry in the database using mongoose. Both of these are done automatically by
mongoose
.

设置

timestamps
true
将增加2个东西我的架构:一个
createdAt
updatedAt
日期值。 当我们创建一个新条目每次
createdAt
将自动更新和
updatedAt
一旦我们更新使用猫鼬数据库中的条目将更新。 这两种都是
mongoose
自动完成的。

The second part is

collection
. This shows what my collection name will be inside my database. I am assigning it the name of
users
.

第二部分是

collection
。 这显示了我的集合名称将在数据库中。 我给它分配了
users
名。

And then finally we'll export the object like this:

最后,我们将像这样导出对象:

export default mongoose.model("User", userSchema);

So

mongoose.model
takes in 2 parameters here.

因此

mongoose.model
在这里接受2个参数。

  • The name of the model, which is

    User
    here

    模型的名称,即

    User
    此处

  • The schema associated with that model, which is

    userSchema
    in this case

    与该模型关联的模式,在这种情况下为

    userSchema

Note: Based on the name of the model, which is

User
in this case, we don't add
collection
key in the schema section. It will take this
User
name and append an
s
to it and create a collection by its name, which becomes
user
.

注意:基于模型的名称(在这种情况下为

User
,我们不会在模式部分中添加
collection
键。 它将使用该
User
名并在其后附加
s
,并通过其名称创建一个集合,该集合将成为
user

Great, now we have our first model.

太好了,现在我们有了第一个模型。

If you've gotten stuck anywhere, just have a look at the source code.

如果您被困在任何地方,请看一下源代码

创建一个新的用户API [POST请求] (Create a new user API [POST request] )

Next let's write our first controller for this route:

.post('/', user.onCreateUser)
.

接下来,让我们为该路由编写第一个控制器:

.post('/', user.onCreateUser)

Go inside

controllers/user.js
and import 2 things at the top:

进入

controllers/user.js
并在顶部导入两件事:

// utils
import makeValidation from '@withvoid/make-validation';
// models
import UserModel, { USER_TYPES } from '../models/User.js';

Here we are importing the validation library that I talked about in the video at the very top. We are also importing our user modal along with the

USER_TYPES
from the same file.

在这里,我们将导入我在视频最顶部讨论过的验证库。 我们还将从同一文件中导入用户模式以及

USER_TYPES

This is what

USER_TYPES
represents:

这是

USER_TYPES
代表的:

export const USER_TYPES = {
CONSUMER: "consumer",
SUPPORT: "support",
};

Next find the controller

onCreateUser
and add the following content to it:

接下来找到控制器

onCreateUser
并向其中添加以下内容:

onCreateUser: async (req, res) => {
try {
const validation = makeValidation(types => ({
payload: req.body,
checks: {
firstName: { type: types.string },
lastName: { type: types.string },
type: { type: types.enum, options: { enum: USER_TYPES } },
}
}));
if (!validation.success) return res.status(400).json(validation);

const { firstName, lastName, type } = req.body;
const user = await UserModel.createUser(firstName, lastName, type);
return res.status(200).json({ success: true, user });
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},

Let's divide this into 2 sections.

让我们将其分为2个部分。

First we validate the user response by doing this:

首先,我们通过执行以下操作来验证用户响应:

const validation = makeValidation(types => ({
payload: req.body,
checks: {
firstName: { type: types.string },
lastName: { type: types.string },
type: { type: types.enum, options: { enum: USER_TYPES } },
}
}));
if (!validation.success) return res.status(400).json({ ...validation });

Please make sure that you have seen the video (above) on

validate an API request in Node using custom validation or by using make-validation library
.

请确保您已观看视频(以上),该视频

validate an API request in Node using custom validation or by using make-validation library

Here we are using the

make-validation
library (that I ended up making while writing this tutorial). I talk about it's usage in the video at the start of this tutorial.

在这里,我们使用了

make-validation
库(我在编写本教程时最终完成了该库)。 我将在本教程开始的视频中谈论它的用法。

All we are doing here is passing

req.body
to
payload
. Then in the checks we're adding an object where against each
key
we are telling what are the requirements for each type, for example:

我们在这里所做的只是将

req.body
传递给
payload
。 然后在检查中添加一个对象,针对每个
key
在其中告诉每种类型的要求,例如:

firstName: { type: types.string },

Here we are telling it that

firstName
is of type string. If the user forgets to add this value while hitting the API, or if the type is not string, it will throw an error.

在这里,我们告诉它

firstName
是字符串类型。 如果用户在点击API时忘记添加此值,或者类型不是字符串,则将引发错误。

The

validation
variable will return an object with 3 things:
{success: boolean, message: string, errors: object}
.

validation
变量将返回一个包含3个对象的对象:
{success: boolean, message: string, errors: object}

If

validation.success
is false we simply return everything from the validation and give it to the user with a status code of
400
.

如果

validation.success
为false,我们只需返回验证中的所有内容,并将状态代码为
400
给予用户。

Once our validation is in place and we know that the data we are getting are valid, then we do the following:

验证到位并且我们知道所获取的数据有效之后,我们将执行以下操作:

const { firstName, lastName, type } = req.body;
const user = await UserModel.createUser(firstName, lastName, type);
return res.status(200).json({ success: true, user });

Then we destruct

firstName, lastName, type
from
req.body
and pass those values to our
UserModel.createUser
. If everything goes right, it simply returns
success: true
with the new
user
created along with a status
200
.

然后,我们销毁

firstName, lastName, type
req.body
firstName, lastName, type
并将这些值传递给我们的
UserModel.createUser
。 如果一切顺利,则只返回
success: true
创建新
user
以及状态为
200
success: true

If anywhere in this process anything goes wrong, it throws an error and goes to the catch block:

如果此过程中的任何地方出了问题,它将引发错误并转到catch块:

catch (error) {
return res.status(500).json({ success: false, error: error })
}

There we simply return an error message along with the HTTP status

500
.

在那里,我们仅返回一条错误消息以及HTTP状态

500

The only thing we are missing here is the

UserModel.createUser()
method.

我们在这里唯一缺少的是

UserModel.createUser()
方法。

So let's go back into our

models/User.js
file and add it:

因此,让我们回到我们的

models/User.js
文件并添加它:

userSchema.statics.createUser = async function (
firstName,
lastName,
type
) {
try {
const user = await this.create({ firstName, lastName, type });
return user;
} catch (error) {
throw error;
}
}

export default mongoose.model("User", userSchema);

So all we are doing here is adding a static method to our

userSchema
called
createUser
that takes in 3 parameters:
firstName, lastName, type
.

因此,我们在此处所做的就是在

userSchema
添加一个名为
createUser
的静态方法,该方法
userSchema
3个参数:
firstName, lastName, type

Next we use this:

接下来我们使用这个:

const user = await this.create({ firstName, lastName, type });

Here the

this
part is very important, since we are writing a static method on
userSchema
. Writing
this
will ensure that we are using performing operations on the
userSchema
object

在这里,

this
部分非常重要,因为我们正在
userSchema
上编写静态方法。 编写
this
将确保我们正在使用对
userSchema
对象执行操作

One thing to note here is that

userSchema.statics.createUser = async function (firstName, lastName, type) => {}
won't work. If you use an
=>
arrow function the
this
context will be lost and it won't work.

这里要注意的一件事是

userSchema.statics.createUser = async function (firstName, lastName, type) => {}
将不起作用。 如果使用
=>
箭头函数,则
this
上下文将丢失并且将无法使用。

If you want to learn more about

static
methods in mongoose, see this very short but helpful doc example here.

如果您想了解有关Mongoose中

static
方法的更多信息,请在此处查看此简短但有用的文档示例。

Now that we have everything set up, let's start our terminal by running the following command in the project's root folder:

现在我们已经完成了所有设置,让我们在项目的根文件夹中运行以下命令来启动终端:

npm start;

Go into postman, set up a

POST
request on this API
http://localhost:3000/users
, and add the following body to the API:

进入邮递员,在此API

http://localhost:3000/users
上设置
POST
请求,并将以下正文添加到API:

{
firstName: 'John'
lastName: 'Doe',
type: 'consumer'
}

Like this:

像这样:

You can also get the entire postman API collection from here so that you don't have to write the APIs again and again.

您还可以从此处获取整个邮递员API集合,这样就不必一次又一次地编写API。

Awesome – we just ended up creating our first API. Let's create a couple more user APIs before we move to the chat part because there is no chat without users (unless we have robots, but robots are users as well 🧐).

太棒了–我们刚刚创建了第一个API。 在转到聊天部分之前,让我们创建更多的用户API,因为没有用户就不会聊天(除非我们有机器人,但机器人也是用户🧐)。

通过其ID API获取用户[获取请求] (Get a user by its ID API [GET request] )

Next we need to write an API that gets us a user by its ID. So for our route

.get('/:id', user.onGetUserById)
let's write down its controller.

接下来,我们需要编写一个API,通过其ID为我们吸引用户。 因此,对于我们的路由

.get('/:id', user.onGetUserById)
我们写下它的控制器。

Go to

controllers/user.js
and for the method
onGetUserById
write this:

转到

controllers/user.js
并为
onGetUserById
方法编写以下代码:

onGetUserById: async (req, res) => {
try {
const user = await UserModel.getUserById(req.params.id);
return res.status(200).json({ success: true, user });
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},

Cool, this looks straightforward. Let's add

UserModel.getUserById()
in our
models/User.js
file.

很酷,这看起来很简单。 让我们在

models/User.js
文件中添加
UserModel.getUserById()

Add this method below the last

static
method you wrote:

将此方法添加到您最后编写的

static
方法下面:

userSchema.statics.getUserById = async function (id) {
try {
const user = await this.findOne({ _id: id });
if (!user) throw ({ error: 'No user with this id found' });
return user;
} catch (error) {
throw error;
}
}

We pass in an

id
parameter and we wrap our function in
try/catch
. This is very important when you are using
async/await
. The lines to focus on here are these 2:

我们传入一个

id
参数,然后将函数包装在
try/catch
。 当您使用
async/await
时,这非常重要。 这里重点介绍以下几行:

const user = await this.findOne({ _id: id });
if (!user) throw ({ error: 'No user with this id found' });

We use

mongoose
's  
findOne
method to find an entry by
id
. We know that only one item exists in the collection by this
id
because the
id
is unique. If no user is found we simply throw an error with the message
No user with this id found
.

我们使用

mongoose
findOne
方法通过
id
查找条目。 我们知道,此
id
中的集合中仅存在一项,因为该
id
是唯一的。 如果未找到用户,我们将简单地引发错误,并显示消息“
No user with this id found

And that is it! Let's start up our server:

就是这样! 让我们启动服务器:

npm start;

Open up postman and create a

GET
request
http://localhost:3000/users/:id
.

打开邮递员并创建

GET
请求
http://localhost:3000/users/:id

Note: I am using the ID of the last user we just created.

注意:我使用的是我们刚创建的最后一个用户的ID。

Nicely done! Good job.

做得很好! 做得好。

Two more API's to go for our user section.

我们的用户部分还有两个API。

获取所有用户API [GET请求] (Get all users API [GET request])

For our router in

.get('/', user.onGetAllUsers)
let's add information to its controller.

对于

.get('/', user.onGetAllUsers)
的路由器,让我们向其控制器添加信息。

Go to

controllers/user.js
and add code in the
onGetAllUsers()
method:

转到

controllers/user.js
并在
onGetAllUsers()
方法中添加代码:

onGetAllUsers: async (req, res) => {
try {
const users = await UserModel.getUsers();
return res.status(200).json({ success: true, users });
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},

Next let's create the static method for

getUsers()
in the
models/User.js
file. Below the last static method you wrote in that file, type:

接下来,让我们在

models/User.js
文件中为
getUsers()
创建静态方法。 在您在该文件中编写的最后一个静态方法下面,键入:

userSchema.statics.getUsers = async function () {
try {
const users = await this.find();
return users;
} catch (error) {
throw error;
}
}

We use the

mongoose
method called
await this.find();
to get all the records for our
users
collection and return it.

我们使用称为

await this.find();
mongoose
方法
await this.find();
获取我们
users
收集的所有记录并返回。

Note: I am not handling pagination in our users API because that's not the main focus here. I'll talk about pagination once we move towards our chat APIs.

注意:我不在我们的用户API中处理分页,因为这不是这里的主要重点。 一旦我们使用聊天API,我将谈论分页。

Let's start our server:

让我们启动服务器:

npm start;

Open up postman and create a

GET
request for this route
http://localhost:3000/users
:

打开邮递员,并为此路由

http://localhost:3000/users
创建一个
GET
请求:

I went ahead and ended up creating a couple more users. 😄

我继续前进,最终创建了更多用户。 😄

通过ID API [删除请求]删除用户(更多奖金部分,如果需要,您可以跳过此部分) (Delete a user by ID API [DELETE request] (More of a bonus section, you can skip this if you want))

Let's create our final route to delete a user by their ID. For the route

.delete('/:id', user.onDeleteUserById)
go to its controller in
controllers/user.js
and write this code in the
onDeleteUserById()
method:

让我们创建最终路线以通过用户ID删除用户。 对于路由

.delete('/:id', user.onDeleteUserById)
转到其在
controllers/user.js
中的
controllers/user.js
并在
onDeleteUserById()
方法中编写以下代码:

onDeleteUserById: async (req, res) => {
try {
const user = await UserModel.deleteByUserById(req.params.id);
return res.status(200).json({
success: true,
message: `Deleted a count of ${user.deletedCount} user.`
});
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},

Let's add the static method

deleteByUserById
in
models/User.js
:

让我们在

models/User.js
添加静态方法
deleteByUserById

userSchema.statics.deleteByUserById = async function (id) {
try {
const result = await this.remove({ _id: id });
return result;
} catch (error) {
throw error;
}
}

We pass in the

id
here as a parameter and then use the
mongoose
method called
this.remove
to delete a record item from a specific collection. In this case, it's the
users
collection.

我们在此处传递

id
作为参数,然后使用名为
this.remove
mongoose
方法从特定集合中删除记录项。 在这种情况下,它是
users
集合。

Let's start up our server:

让我们启动服务器:

npm start;

Go to postman and create a new

DELETE
route:

转到邮递员并创建新的

DELETE
路线:

With this we'll conclude our USER API section.

这样,我们将结束USER API部分。

Next we will cover how to authenticate routes with an authentication token. This is the last thing I want to touch on before moving on to the chat section – because all of the chat APIs will be authenticated.

接下来,我们将介绍如何使用身份验证令牌对路由进行身份验证。 这是我继续讨论聊天部分之前要做的最后一件事–因为所有聊天API都将通过身份验证。

ExpressJS中的中间件是什么? (What are middlewares in ExpressJS? )

How can we write them? By adding JWT middleware in your application:

我们该怎么写? 通过在您的应用程序中添加JWT中间件:

And here's the GitHub link to the entire source code of this video [Chapter 0].

是该视频的完整源代码GitHub链接 [第0章]。

And again, all the relevant info can be found in the READ.ME.

同样,所有相关信息都可以在READ.ME中找到。

Coming back to our code base, let's create a JWT middleware to authenticate our routes. Go to

middlewares/jwt.js
and add the following:

回到我们的代码库,让我们创建一个JWT中间件来验证我们的路由。 转到

middlewares/jwt.js
并添加以下内容:

import jwt from 'jsonwebtoken';
// models
import UserModel from '../models/User.js';

const SECRET_KEY = 'some-secret-key';

export const encode = async (req, res, next) => {
try {
const { userId } = req.params;
const user = await UserModel.getUserById(userId);
const payload = {
userId: user._id,
userType: user.type,
};
const authToken = jwt.sign(payload, SECRET_KEY);
console.log('Auth', authToken);
req.authToken = authToken;
next();
} catch (error) {
return res.status(400).json({ success: false, message: error.error });
}
}

export const decode = (req, res, next) => {
if (!req.headers['authorization']) {
return res.status(400).json({ success: false, message: 'No access token provided' });
}
const accessToken = req.headers.authorization.split(' ')[1];
try {
const decoded = jwt.verify(accessToken, SECRET_KEY);
req.userId = decoded.userId;
req.userType = decoded.type;
return next();
} catch (error) {

return res.status(401).json({ success: false, message: error.message });
}
}

Let's discuss the

encode
method first:

让我们首先讨论

encode
方法:

export const encode = async (req, res, next) => {
try {
const { userId } = req.params;
const user = await UserModel.getUserById(userId);
const payload = {
userId: user._id,
userType: user.type,
};
const authToken = jwt.sign(payload, SECRET_KEY);
console.log('Auth', authToken);
req.authToken = authToken;
next();
} catch (error) {
return res.status(400).json({
success: false, message: error.error
});
}
}

Let's go through it step by step.

让我们逐步进行。

We get the

userId
from our
req.params
. If you remember from the video earlier,
req.params
is the
/:<identifier>
defined in our routes section.

我们从

req.params
获取
userId
。 如果您还记得前面的视频,则
req.params
是我们的路线部分中定义的
/:<identifier>

Next we use the

const user = await UserModel.getUserById(userId);
method we just created recently to get user information. If it exists, that is – otherwise this line will throw an error and it will directly go to the
catch
block where we will return the user with a
400
response and and an error message.

接下来,我们使用

const user = await UserModel.getUserById(userId);
我们最近创建的用于获取用户信息的方法。 如果存在,则为-否则,此行将引发错误,并将直接转到
catch
块,在此我们将为用户返回
400
响应和一条错误消息。

But if we get a response from the

getUserById
method we then make a payload:

但是,如果我们从

getUserById
方法获得响应,则将创建有效负载:

const payload = {
userId: user._id,
userType: user.type,
};

Next we sign that payload in JWT using the following:

接下来,我们使用以下方法在JWT中对该有效负载进行签名:

const authToken = jwt.sign(payload, SECRET_KEY);

Once we have the JWT signed we then do this:

一旦JWT签名,我们就可以这样做:

req.authToken = authToken;
next();

Set it to our

req.authToken
and then forward this information as
next()
.

将其设置为我们的

req.authToken
,然后将此信息作为
next()
转发。

Next let's talk about the

decode
method:

接下来让我们讨论一下

decode
方法:

export const decode = (req, res, next) => {
if (!req.headers['authorization']) {
return res.status(400).json({ success: false, message: 'No access token provided' });
}
const accessToken = req.headers.authorization.split(' ')[1];
try {
const decoded = jwt.verify(accessToken, SECRET_KEY);
req.userId = decoded.userId;
req.userType = decoded.type;
return next();
} catch (error) {

return res.status(401).json({ success: false, message: error.message });
}
}

Let's break this down:

让我们分解一下:

if (!req.headers['authorization']) {
return res.status(400).json({
success: false,
message: 'No access token provided'
});
}

First we check if the

authorization
header is present or not. If not we simply return an error message to user.

首先,我们检查

authorization
标头是否存在。 如果没有,我们只是向用户返回一条错误消息。

Then we do this:

然后我们这样做:

const accessToken = req.headers.authorization.split(' ')[1];

It's being

split(' ')
by space and then we are getting the second index of the array by accessing its
[1]
index because the convention is
authorization: Bearer <auth-token>
. Want to read more on this? Check out this nice thread on quora.

它被空格

split(' ')
,然后我们通过访问数组的
[1]
索引来获取数组的第二个索引,因为约定是
authorization: Bearer <auth-token>
。 想了解更多吗? 在quora上查看这个漂亮的线程

Then we try to decode our token:

然后,我们尝试对令牌进行解码:

try {
const decoded = jwt.verify(accessToken, SECRET_KEY);
req.userId = decoded.userId;
req.userType = decoded.type;
return next();
} catch (error) {
return res.status(401).json({
success: false, message: error.message
});
}

If this is not successful

jwt.verify(accessToken, SECRET_KEY)
will simply throw an error and our code will go in the
catch
block immediately. If it is successful, then we can decode it. We get
userId
and
type
from the token and save it as
req.userId, req.userType
and simply hit
next()
.

如果这不成功,则

jwt.verify(accessToken, SECRET_KEY)
只会引发错误,我们的代码将立即进入
catch
块。 如果成功,则可以对其进行解码。 我们从令牌中获取
userId
并进行
type
,然后将其另存为
req.userId, req.userType
req.userId, req.userType
然后直接单击
next()

Now, moving forward, every route that goes through this

decode
middleware will have the current user's
id & it's type
.

现在,继续前进,通过此

decode
中间件的每条路由都将具有当前用户的
id & it's type

This was it for the middleware section. Let's create a

login
route so that we can ask a user for their information and give a token in return (because moving forward they'll need a token to access the rest of chat APIs).

中间件部分就是这样。 让我们创建一个

login
路径,以便我们可以向用户询问他们的信息并提供令牌作为回报(因为向前移动,他们将需要令牌来访问其余的聊天API)。

创建登录路径[POST请求] (Creating a login route [POST request])

Go to your

routes/index.js
file and paste the following content:

转到您的

routes/index.js
文件并粘贴以下内容:

import express from 'express';
// middlewares
import { encode } from '../middlewares/jwt.js';

const router = express.Router();

router
.post('/login/:userId', encode, (req, res, next) => {
return res
.status(200)
.json({
success: true,
authorization: req.authToken,
});
});

export default router;

So all we are doing is adding the

encode
middleware to our
http://localhost:3000/login/:<user-id>
[POST] route. If everything goes smoothly the user will get an
authorization
token.

因此,我们要做的就是将

encode
中间件添加到我们的
http://localhost:3000/login/:<user-id>
[POST]路由中。 如果一切顺利,用户将获得
authorization
令牌。

Note: I am not adding a login/signup flow, but I still wanted to touch on JWT/middleware in this tutorial.

注意:我没有添加登录/注册流程,但是我仍然想在本教程中介绍JWT /中间件。

Usually authentication is done in a similar way. The only addition here is that the user doesn't provide their ID. They provide their username, password (which we verify in the database), and if everything checks out we give them an authorization token.

通常,身份验证是通过类似的方式完成的。 这里唯一的补充是用户不提供其ID。 他们提供了用户名,密码(我们在数据库中进行了验证),如果一切都签出了,我们会给他们一个授权令牌。

If you got stuck anywhere up to this point, just write to me at twitter.com/adeelibr, so that way I can improve the content. You can also write to me if you would like to learn something else.

如果您到现在为止还停留在任何地方,只需在twitter.com/adeelibr上给我写信,这样我就可以改善内容。 如果您想学习其他内容,也可以给我写信。

As a reminder, the entire source code is available here. You don't have to code along with this tutorial, but if you do the concepts will stick better.

提醒一下,此处是完整的源代码。 您不必随本教程一起编写代码,但如果您这样做,这些概念将更好地坚持。

Let's just check our

/login
route now.

现在让我们检查

/login
路由。

Start your server:

启动服务器:

npm start;

Let's run postman. Create a new POST request

http://localhost:3000/login/<user-id>
:

让我们运行邮递员。 创建一个新的POST请求

http://localhost:3000/login/<user-id>

With this we are done with our login flow as well.

这样,我们也完成了登录流程。

This was a lot. But now we can focus only on our chat routes.

好多 但是现在我们只能专注于聊天路线。

创建一个Web套接字类 (Create a web socket class )

This web socket class will handle events when a user disconnects, joins a chat room, or wants to mute a chat room.

当用户断开连接,加入聊天室或想要使聊天室静音时,此Web套接字类将处理事件。

So let's create a web-socket class that will manage sockets for us. Create a new folder called

utils
. Inside that folder create a file called
WebSockets.js
and add the following content:

因此,让我们创建一个Web-socket类,它将为我们管理套接字。 创建一个名为

utils
的新文件夹。 在该文件夹中,创建一个名为
WebSockets.js
的文件,并添加以下内容:

class WebSockets {
users = [];
connection(client) {
// event fired when the chat room is disconnected
client.on("disconnect", () => {
this.users = this.users.filter((user) => user.socketId !== client.id);
});
// add identity of user mapped to the socket id
client.on("identity", (userId) => {
this.users.push({
socketId: client.id,
userId: userId,
});
});
// subscribe person to chat & other user as well
client.on("subscribe", (room, otherUserId = "") => {
this.subscribeOtherUser(room, otherUserId);
client.join(room);
});
// mute a chat room
client.on("unsubscribe", (room) => {
client.leave(room);
});
}

subscribeOtherUser(room, otherUserId) {
const userSockets = this.users.filter(
(user) => user.userId === otherUserId
);
userSockets.map((userInfo) => {
const socketConn = global.io.sockets.connected(userInfo.socketId);
if (socketConn) {
socketConn.join(room);
}
});
}
}

export default new WebSockets();

The WebSockets class has three major things here:

WebSockets类在这里有三大方面:

  • users array

    用户数组
  • connection method

    连接方式
  • subscribing members of a chat room to it.

    subscribeOtherUser

    订阅聊天室的成员。

    subscribeOtherUser

Let's break this down.

让我们分解一下。

We have a class:

我们有一堂课:

class WebSockets {

}

export default new WebSocket();

We create a class and export an instance of that class.

我们创建一个类并导出该类的实例。

Inside the class we have an empty

users
array. This array will hold a list of all the active users that are online using our application.

在类内部,我们有一个空的

users
数组。 该数组将包含使用我们的应用程序在线的所有活动用户的列表。

Next we have a

connection
method, the core of this class:

接下来,我们有一个

connection
方法,该类的核心:

connection(client) {
// event fired when the chat room is disconnected
client.on("disconnect", () => {
this.users = this.users.filter((user) => user.socketId !== client.id);
});
// add identity of user mapped to the socket id
client.on("identity", (userId) => {
this.users.push({
socketId: client.id,
userId: userId,
});
});
// subscribe person to chat & other user as well
client.on("subscribe", (room, otherUserId = "") => {
this.subscribeOtherUser(room, otherUserId);
client.join(room);
});
// mute a chat room
client.on("unsubscribe", (room) => {
client.leave(room);
});
}

The

connection
method takes in a parameter called
client
(client here will be our server instance, I will talk more about this in a bit).

connection
方法接受一个名为
client
的参数(这里的client将是我们的服务器实例,稍后我将详细讨论)。

We take the param

client
and add some event to it

我们使用param

client
并向其中添加一些事件

  • client.on('disconnect') // when a user connection is lost this method will be called

    client.on('disconnect')//当失去用户连接时,将调用此方法
  • client.on('identity') // when user logs in from the front end they will make a connection with our server by giving their identity

    client.on('identity')//当用户从前端登录时,他们将通过提供其身份与我们的服务器建立连接
  • client.on('subscribe') // when a user joins a chat room this method is called

    client.on('subscribe')//当用户加入聊天室时,此方法称为
  • client.on('unsubscribe') // when a user leaves or wants to mute a chat room

    client.on('unsubscribe')//用户离开或想让聊天室静音

Let's talk about

disconnect
:

让我们来谈谈

disconnect

client.on("disconnect", () => {
this.users = this.users.filter((user) => user.socketId !== client.id);
});

As soon as the connection is disconnected, we run a filter on users array. Where we find

user.id === client.id
we remove it from our sockets array. (
client
here is coming from the function param.)

一旦连接断开,我们就对用户数组运行筛选器。 在找到

user.id === client.id
我们将其从套接字数组中删除。 (这里的
client
来自功能参数。)

Let's talk about

identity
:

让我们谈谈

identity

client.on("identity", (userId) => {
this.users.push({
socketId: client.id,
userId: userId,
});
});

When a user logs in through he front end application web/android/ios they will make a socket connection with our backend app and call this identity method. They'll also send their own user id.

当用户通过前端应用程序web / android / ios登录时,他们将与我们的后端应用程序建立套接字连接,并调用此标识方法。 他们还将发送自己的用户ID。

We will take that user id and the client id (the user's own unique socket id that socket.io creates when they make a connection with our BE).

我们将使用该用户ID和客户端ID(用户在与我们的BE建立连接时由socket.io创建的用户自己的唯一套接字ID)。

Next we have

unsubscribe
:

接下来,我们

unsubscribe

client.on("unsubscribe", (room) => {
client.leave(room);
});

The user passes in the

room
id and we just tell
client.leave()
to remove the current user calling this method from a particular chat room.

用户传入

room
ID,我们只是告诉
client.leave()
从当前聊天室中删除当前调用此方法的用户。

Next we have subscribe:

接下来,我们订阅:

client.on("subscribe", (room, otherUserId = "") => {
this.subscribeOtherUser(room, otherUserId);
client.join(room);
});

When a user joins a chat room, they will tell us about the room they want to join along with the other person who is part of that chat room.

当用户加入聊天室时,他们会告诉我们他们想与该聊天室中的其他人一起加入的房间。

Note: We will see later that when we initiate a chat room we get all the users associated with that room in the API response.

注意:稍后我们将看到,当我们启动聊天室时,会在API响应中获得与该聊天室关联的所有用户。

In my opinion: Another thing we could have done here was when the user sends in the room number, we can make a DB query to see all the members of the chat room and make them join if they are online at the moment (that is, in our users list).

我认为 :我们可以在这里做的另一件事是,当用户发送房间号时,我们可以进行数据库查询以查看聊天室的所有成员,并让他们在当前在线的情况下加入(即,在我们的用户列表中)。

The

subscribeOtherUser
method is defined like this:

subscribeOtherUser
方法的定义如下:

subscribeOtherUser(room, otherUserId) {
const userSockets = this.users.filter(
(user) => user.userId === otherUserId
);
userSockets.map((userInfo) => {
const socketConn = global.io.sockets.connected(userInfo.socketId);
if (socketConn) {
socketConn.join(room);
}
});
}

We pass in  

room
and
otherUserId
as params to this function.

我们将

room
otherUserId
作为参数传递给此函数。

Using the

otherUserId
we filter on our
this.users
array and all the results that match are stored in
userSockets
array.

使用

otherUserId
我们对
this.users
数组进行过滤,所有匹配的结果都存储在
userSockets
数组中。

You might be thinking – how can one user have multiple presences in the user array? Well, think of a scenario where the same user is logged in from both their web application and mobile phone. It will create multiple socket connections for the same user.

您可能在想–一个用户如何在用户阵列中具有多个状态? 好吧,请考虑一个场景,其中同一用户同时从其Web应用程序和移动电话登录。 它将为同一用户创建多个套接字连接。

Next we map on

userSockets
. For each item in this array we pass it into this method:  
const socketConn = global.io.sockets.connected(userInfo.socketId)

接下来,我们映射到

userSockets
。 对于此数组中的每个项目,我们将其传递给此方法:
const socketConn = global.io.sockets.connected(userInfo.socketId)

I will talk more about this

global.io.sockets.connected
in a bit. But what this initially does is it takes in
userInfo.socketId
and if it exists in our socket connection, it will return the connection, otherwise
null
.

我将

global.io.sockets.connected
讨论一下这个
global.io.sockets.connected
。 但是,此操作最初是将其
userInfo.socketId
,如果它存在于我们的套接字连接中,它将返回该连接,否则返回
null

Next we simply see if

socketConn
is available. If so, we take that
socketConn
and make this connection join the
room
passed in the function:

接下来,我们简单地看看

socketConn
是否可用。 如果是这样,我们采用该
socketConn
并使此连接加入函数中传递的
room

if (socketConn) {
socketConn.join(room);
}

And this is it for our WebSockets class.

这就是我们的WebSockets类。

Let's import this file in our

server/index.js
file:

让我们将此文件导入到

server/index.js
文件中:

import socketio from "socket.io";
// mongo connection
import "./config/mongo.js";
// socket configuration
import WebSockets from "./utils/WebSockets.js";

So just import

socket.io
and import
WebSockets
somewhere at the top.

因此,只需导入

socket.io
并在顶部的某个位置导入
WebSockets

Next where we are creating our server add the content below this:

接下来,在我们创建服务器的地方,在下面添加内容:

/** Create HTTP server. */
const server = http.createServer(app);
/** Create socket connection */
global.io = socketio.listen(server);
global.io.on('connection', WebSockets.connection)

The

server
was created and we do two things:

server
已创建,我们做两件事:

  • assign

    global.io
    to
    socketio.listen(server)
    (As soon as a port starts listening on the
    server
    , sockets starts listening for events happening on that port as well.)

    global.io
    分配给
    socketio.listen(server)
    (端口开始侦听
    server
    ,套接字也开始侦听该端口上发生的事件。)

  • then we assign

    global.io.on('connection', WebSockets.connection)
    method. Every time someone from the front end makes a socket connection, the
    connection
    method will be called which will invoke our
    Websockets
    class and inside that class the
    connection
    method.

    然后我们分配

    global.io.on('connection', WebSockets.connection)
    方法。 每次前端有人建立套接字连接时,都会调用该
    connection
    方法,该方法将调用我们的
    Websockets
    类,并在该类内部调用
    connection
    方法。

global.io
is equivalent to
windows
object in browser. But since we don't have
windows
in NodeJS we use
global.io
. Whatever we put in
global.io
is available in the entire application.

global.io
等效于浏览器中的
windows
对象。 但是由于
global.io
没有
windows
,因此我们使用
global.io
。 无论我们在
global.io
global.io
什么
global.io
都可以在整个应用程序中使用。

This is the same

global.io
we used in the
WebSockets
class inside the
subscribeOtherUser
method.

这是我们在

subscribeOtherUser
方法内的
WebSockets
类中使用的
global.io

If you got lost here is the entire source code of this chat application. Also free to drop me a message with your feedback and I will try to improve the content of this tutorial.

如果您迷路了,这里是此聊天应用程序全部源代码 。 另外,请随时给我您的反馈信息,我将尝试改进本教程的内容。

讨论聊天室和聊天消息数据库模型 (Discussing chat room & chat message database model)

Before starting off with Chat, I think it is really important to discuss the database model on which we will create our chat application. Have a look at the below video:

在开始聊天之前,我认为讨论在其上创建聊天应用程序的数据库模型非常重要。 看下面的视频:

Now that you have a clear idea about what our chat structure will be like, let's start off by making our chat room model.

既然您已经对我们的聊天结构有了一个清晰的了解,那么让我们开始制作我们的聊天室模型。

Go inside your

models
folder and create the following
ChatRoom.js
. Add the following content to it:

进入您的

models
文件夹并创建以下
ChatRoom.js
。 向其中添加以下内容:

import mongoose from "mongoose";
import { v4 as uuidv4 } from "uuid";

export const CHAT_ROOM_TYPES = {
CONSUMER_TO_CONSUMER: "consumer-to-consumer",
CONSUMER_TO_SUPPORT: "consumer-to-support",
};

const chatRoomSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
userIds: Array,
type: String,
chatInitiator: String,
},
{
timestamps: true,
collection: "chatrooms",
}
);

chatRoomSchema.statics.initiateChat = async function (
userIds, type, chatInitiator
) {
try {
const availableRoom = await this.findOne({
userIds: {
$size: userIds.length,
$all: [...userIds],
},
type,
});
if (availableRoom) {
return {
isNew: false,
message: 'retrieving an old chat room',
chatRoomId: availableRoom._doc._id,
type: availableRoom._doc.type,
};
}

const newRoom = await this.create({ userIds, type, chatInitiator });
return {
isNew: true,
message: 'creating a new chatroom',
chatRoomId: newRoom._doc._id,
type: newRoom._doc.type,
};
} catch (error) {
console.log('error on start chat method', error);
throw error;
}
}

export default mongoose.model("ChatRoom", chatRoomSchema);

We have three things going on here:

我们在这里进行三件事:

  • We have a const for

    CHAT_ROOM_TYPES
    which has only two types

    我们有一个

    CHAT_ROOM_TYPES
    常量,只有两种类型

  • We define our ChatRoom schema

    我们定义我们的ChatRoom模式
  • We add a static method to initiate chat

    我们添加了一个静态方法来发起聊天

与聊天相关的API (Chat related APIs)

在用户之间发起聊天(/房间/发起[POST请求]) (Initiate a chat between users (/room/initiate [POST request]))

Let's discuss our static method defined in

models/ChatRoom.js
called
initiateChat
:

让我们来讨论在定义我们的静态方法

models/ChatRoom.js
initiateChat

chatRoomSchema.statics.initiateChat = async function (userIds, type, chatInitiator) {
try {
const availableRoom = await this.findOne({
userIds: {
$size: userIds.length,
$all: [...userIds],
},
type,
});
if (availableRoom) {
return {
isNew: false,
message: 'retrieving an old chat room',
chatRoomId: availableRoom._doc._id,
type: availableRoom._doc.type,
};
}

const newRoom = await this.create({ userIds, type, chatInitiator });
return {
isNew: true,
message: 'creating a new chatroom',
chatRoomId: newRoom._doc._id,
type: newRoom._doc.type,
};
} catch (error) {
console.log('error on start chat method', error);
throw error;
}
}

This function takes in three parameters:

此函数接受三个参数:

  • userIds (array of users)

    userIds(用户数组)
  • type (type of chatroom)

    类型(聊天室类型)
  • chatInitiator (the user who created the chat room)

    chatInitiator(创建聊天室的用户)

Next we are doing two things here: either returning an existing chatroom document or creating a new one.

接下来,我们在这里做两件事:要么返回现有的聊天室文档,要么创建一个新的文档。

Let's break this one down:

让我们分解一下:

const availableRoom = await this.findOne({
userIds: {
$size: userIds.length,
$all: [...userIds],
},
type,
});
if (availableRoom) {
return {
isNew: false,
message: 'retrieving an old chat room',
chatRoomId: availableRoom._doc._id,
type: availableRoom._doc.type,
};
}

First using the

this.findOne()
API in mongoose, we find all the chatrooms where the following criteria is met:

首先在猫鼬中使用

this.findOne()
API,我们找到满足以下条件的所有聊天室:

userIds: { $size: userIds.length, $all: [...userIds] },
type: type,

You can read more on the $size operator here, and more on the $all operator here.

您可以在此处阅读有关$ size运算符的更多信息 ,并在此处了解有关$ all运算符的更多信息

We're checking to find a chatroom document where an item exists in our chatrooms collection where

我们正在检查以查找聊天室文档,其中该聊天室文档在我们的聊天室集合中存在某项

  1. the

    userIds
    are the same as the one we are passing to this function (irrespective of the user ids order), and

    userIds
    与我们传递给该函数的
    userIds
    相同(与用户ID顺序无关),并且

  2. the length of the

    userIds
    is the same as that my
    userIds.length
    that we are passing through the function.

    该长度

    userIds
    是一样的,我的
    userIds.length
    ,我们正在经历的功能。

Also we're checking that the chat room type should be the same.

另外,我们正在检查聊天室类型是否应该相同。

If something like this is found, we simply return the existing chatroom.

如果找到类似的内容,我们只需返回现有的聊天室即可。

Otherwise we create a new chat room and return it by doing this:

否则,我们将创建一个新的聊天室并通过执行以下操作将其返回:

const newRoom = await this.create({ userIds, type, chatInitiator });
return {
isNew: true,
message: 'creating a new chatroom',
chatRoomId: newRoom._doc._id,
type: newRoom._doc.type,
};

Create a new room and return the response.

创建一个新房间并返回响应。

We also have an

isNew
key where, if it's retrieving an old chatroom, we set it to
false
otherwise
true
.

我们还有一个

isNew
键,如果要检索旧的聊天室,则将其设置为
false
否则为
true

Next for your route created in

routes/chatRoom.js
called
post('/initiate', chatRoom.initiate)
go to its appropriate controller in
controllers/chatRoom.js
and add the following in the
initiate
method:

接下来,在

routes/chatRoom.js
创建的名为
post('/initiate', chatRoom.initiate)
routes/chatRoom.js
post('/initiate', chatRoom.initiate)
转到其在
controllers/chatRoom.js
合适的控制器,并在
initiate
方法中添加以下内容:

initiate: async (req, res) => {
try {
const validation = makeValidation(types => ({
payload: req.body,
checks: {
userIds: {
type: types.array,
options: { unique: true, empty: false, stringOnly: true }
},
type: { type: types.enum, options: { enum: CHAT_ROOM_TYPES } },
}
}));
if (!validation.success) return res.status(400).json({ ...validation });

const { userIds, type } = req.body;
const { userId: chatInitiator } = req;
const allUserIds = [...userIds, chatInitiator];
const chatRoom = await ChatRoomModel.initiateChat(allUserIds, type, chatInitiator);
return res.status(200).json({ success: true, chatRoom });
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},

We are using the

make-validation
library here to validate the user's request. For the initiate API, we expect the user to send an array of
users
and also define the type of the
chat-room
that is being created.

我们在这里使用

make-validation
库来验证用户的请求。 对于启动API,我们希望用户发送一组
users
,并定义正在创建的
chat-room
的类型。

Once the validation passes, then:

验证通过后,即可:

const { userIds, type } = req.body;
const { userId: chatInitiator } = req;
const allUserIds = [...userIds, chatInitiator];
const chatRoom = await ChatRoomModel.initiateChat(allUserIds, type, chatInitiator);
return res.status(200).json({ success: true, chatRoom });

One thing to notice here is

userIds, type
is coming from
req.body
while
userId
that is being aliased as
chatInitiatorId
is coming from
req
thanks to our
decode
middleware.

这里要注意的一件事是

req.body
userIds, type
来自
req.body
而别名为
chatInitiatorId
userId
来自
req
这要归功于我们的
decode
中间件。

If you remember, we attached

app.use("/room", decode, chatRoomRouter);
in our
server/index.js
file. This means this route
/room/initiate
is authenticated. So
const { userId: chatInitiator } = req;
is the id of the current user logged in.

如果您还记得,我们附加了

app.use("/room", decode, chatRoomRouter);
在我们的
server/index.js
文件中。 这意味着该路由
/room/initiate
已通过身份验证。 因此
const { userId: chatInitiator } = req;
是当前登录用户的ID。

We simply call our

initiateChat
method from
ChatRoomModel
and pass it
allUserIds, type, chatInitiator
. Whatever result comes we simply pass it to the user.

我们只需拨打我们的

initiateChat
从方法
ChatRoomModel
并把它传递
allUserIds, type, chatInitiator
。 无论结果如何,我们只要将其传递给用户即可。

Let's run this and see if it works (here is a video of me doing it):

让我们运行它,看看它是否有效(这是我做的一个视频):

在聊天室中创建一条消息(/:roomId / message)[POST请求] (Create a message in chat room (/:roomId/message) [POST request])

Let's create a message for the chat room we just created with

pikachu
.

让我们为刚刚使用

pikachu
创建的聊天室创建一条消息。

But before we create a message we need to create a model for our

chatmessages
. So let's do that first. In your
models
folder create a new file called
ChatMessage.js
and add the following content to it:

但是在创建消息之前,我们需要为

chatmessages
消息创建模型。 所以让我们先做。 在您的
models
文件夹中创建一个名为
ChatMessage.js
的新文件,
ChatMessage.js
其中添加以下内容:

import mongoose from "mongoose";
import { v4 as uuidv4 } from "uuid";

const MESSAGE_TYPES = {
TYPE_TEXT: "text",
};

const readByRecipientSchema = new mongoose.Schema(
{
_id: false,
readByUserId: String,
readAt: {
type: Date,
default: Date.now(),
},
},
{
timestamps: false,
}
);

const chatMessageSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
chatRoomId: String,
message: mongoose.Schema.Types.Mixed,
type: {
type: String,
default: () => MESSAGE_TYPES.TYPE_TEXT,
},
postedByUser: String,
readByRecipients: [readByRecipientSchema],
},
{
timestamps: true,
collection: "chatmessages",
}
);

chatMessageSchema.statics.createPostInChatRoom = async function (chatRoomId, message, postedByUser) {
try {
const post = await this.create({
chatRoomId,
message,
postedByUser,
readByRecipients: { readByUserId: postedByUser }
});
const aggregate = await this.aggregate([
// get post where _id = post._id
{ $match: { _id: post._id } },
// do a join on another table called users, and
// get me a user whose _id = postedByUser
{
$lookup: {
from: 'users',
localField: 'postedByUser',
foreignField: '_id',
as: 'postedByUser',
}
},
{ $unwind: '$postedByUser' },
// do a join on another table called chatrooms, and
// get me a chatroom whose _id = chatRoomId
{
$lookup: {
from: 'chatrooms',
localField: 'chatRoomId',
foreignField: '_id',
as: 'chatRoomInfo',
}
},
{ $unwind: '$chatRoomInfo' },
{ $unwind: '$chatRoomInfo.userIds' },
// do a join on another table called users, and
// get me a user whose _id = userIds
{
$lookup: {
from: 'users',
localField: 'chatRoomInfo.userIds',
foreignField: '_id',
as: 'chatRoomInfo.userProfile',
}
},
{ $unwind: '$chatRoomInfo.userProfile' },
// group data
{
$group: {
_id: '$chatRoomInfo._id',
postId: { $last: '$_id' },
chatRoomId: { $last: '$chatRoomInfo._id' },
message: { $last: '$message' },
type: { $last: '$type' },
postedByUser: { $last: '$postedByUser' },
readByRecipients: { $last: '$readByRecipients' },
chatRoomInfo: { $addToSet: '$chatRoomInfo.userProfile' },
createdAt: { $last: '$createdAt' },
updatedAt: { $last: '$updatedAt' },
}
}
]);
return aggregate[0];
} catch (error) {
throw error;
}
}

export default mongoose.model("ChatMessage", chatMessageSchema);

There are a couple of things happening here:

这里发生了几件事:

  • We have a

    MESSAGE_TYPES
    object which has only one type called
    text

    我们有一个

    MESSAGE_TYPES
    对象,该对象只有一种称为
    text
    类型

  • We are defining our schema for

    chatmessage
    and
    readByRecipient

    我们正在为

    chatmessage
    readByRecipient
    定义架构

  • Then we are writing our static method for

    createPostInChatRoom

    然后,我们为

    createPostInChatRoom
    编写静态方法

I know this is a lot of content, but just bear with me. Let's just write the controller for the route that creates this message.

我知道这是很多内容,但请多多包涵。 让我们只为创建此消息的路由编写控制器。

For the route defined in our

routes/chatRoom.js
API called
.post('/:roomId/message', chatRoom.postMessage)
let's go to its controller in
controllers/chatRoom.js
and define it:

对于在我们的

routes/chatRoom.js
API中定义的
routes/chatRoom.js
该路由名为
.post('/:roomId/message', chatRoom.postMessage)
让我们转到
controllers/chatRoom.js
中的
controllers/chatRoom.js
并对其进行定义:

postMessage: async (req, res) => {
try {
const { roomId } = req.params;
const validation = makeValidation(types => ({
payload: req.body,
checks: {
messageText: { type: types.string },
}
}));
if (!validation.success) return res.status(400).json({ ...validation });

const messagePayload = {
messageText: req.body.messageText,
};
const currentLoggedUser = req.userId;
const post = await ChatMessageModel.createPostInChatRoom(roomId, messagePayload, currentLoggedUser);
global.io.sockets.in(roomId).emit('new message', { message: post });
return res.status(200).json({ success: true, post });
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},

Cool, let's discuss what we are doing here:

很酷,让我们讨论一下我们在做什么:

Operators discussed in this video are:

该视频中讨论的操作员是:

通过ID查看聊天室的会话[获取请求] (See conversation for a chat room by it's id [Get request])

Now that we have

现在我们有了

  • Created a chat room

    创建了一个聊天室
  • Are able to add messages in that chat room

    能够在该聊天室中添加消息

Let's see the entire conversation for that chat as well (with pagination).

让我们同时查看该聊天的整个对话(分页)。

For your route

.get('/:roomId', chatRoom.getConversationByRoomId)
in
routes/chatRoom.js
open its controller in the file
controllers/chatRoom.js
and add the following content to the chat room:

为了您的路线

.get('/:roomId', chatRoom.getConversationByRoomId)
routes/chatRoom.js
在文件打开它的控制器,
controllers/chatRoom.js
,并添加以下内容到聊天室:

getConversationByRoomId: async (req, res) => {
try {
const { roomId } = req.params;
const room = await ChatRoomModel.getChatRoomByRoomId(roomId)
if (!room) {
return res.status(400).json({
success: false,
message: 'No room exists for this id',
})
}
const users = await UserModel.getUserByIds(room.userIds);
const options = {
page: parseInt(req.query.page) || 0,
limit: parseInt(req.query.limit) || 10,
};
const conversation = await ChatMessageModel.getConversationByRoomId(roomId, options);
return res.status(200).json({
success: true,
conversation,
users,
});
} catch (error) {
return res.status(500).json({ success: false, error });
}
},

Next let's create a new static method in our

ChatRoomModel
file called
getChatRoomByRoomId
in
models/ChatRoom.js
:

接下来,让我们在

models/ChatRoom.js
中的
ChatRoomModel
文件中创建一个名为
getChatRoomByRoomId
的新静态方法:

chatRoomSchema.statics.getChatRoomByRoomId = async function (roomId) {
try {
const room = await this.findOne({ _id: roomId });
return room;
} catch (error) {
throw error;
}
}

Very straightforward – we are getting the room by roomId here.

非常简单-我们在这里通过roomId获取房间。

Next in our

UserModel
, create a static method called
getUserByIds
in the file
models/User.js
:

接下来,在

UserModel
,在文件
models/User.js
创建一个名为
getUserByIds
的静态方法:

userSchema.statics.getUserByIds = async function (ids) {
try {
const users = await this.find({ _id: { $in: ids } });
return users;
} catch (error) {
throw error;
}
}

The operator used here is $in – I'll talk about this in a bit.

这里使用的运算符是$ in-我将稍作讨论。

And then at last, go to your

ChatMessage
model in
models/ChatMessage.js
and write a new static method called
getConversationByRoomId
:

最后,转到

models/ChatMessage.js
ChatMessage
模型,并编写一个名为
getConversationByRoomId
的新静态方法:

chatMessageSchema.statics.getConversationByRoomId = async function (chatRoomId, options = {}) {
try {
return this.aggregate([
{ $match: { chatRoomId } },
{ $sort: { createdAt: -1 } },
// do a join on another table called users, and
// get me a user whose _id = postedByUser
{
$lookup: {
from: 'users',
localField: 'postedByUser',
foreignField: '_id',
as: 'postedByUser',
}
},
{ $unwind: "$postedByUser" },
// apply pagination
{ $skip: options.page * options.limit },
{ $limit: options.limit },
{ $sort: { createdAt: 1 } },
]);
} catch (error) {
throw error;
}
}

Let's discuss all that we have done so far:

让我们讨论到目前为止我们已经做的所有事情:

All the source code is available here.

所有源代码都可在此处获得

将整个对话标记为已读(功能类似于WhatsApp) (Mark an entire conversation as read (feature similar to WhatsApp))

Once the other person is logged in and they view a conversation for a room id, we need to mark that conversation as read from their side.

对方登录后,他们查看了一个房间ID的对话后,我们需要将该对话标记为从对方那边读过。

To do this, in your

routes/chatRoom.js
for the route

为此,请在您的

routes/chatRoom.js
找到该路由

put('/:roomId/mark-read', chatRoom.markConversationReadByRoomId)

go to its appropriate controller in

controllers/chatRoom.js
and add the following content in the
markConversationReadByRoomId
controller.

请转到

controllers/chatRoom.js
相应的控制器,然后在
markConversationReadByRoomId
控制器中添加以下内容。

markConversationReadByRoomId: async (req, res) => {
try {
const { roomId } = req.params;
const room = await ChatRoomModel.getChatRoomByRoomId(roomId)
if (!room) {
return res.status(400).json({
success: false,
message: 'No room exists for this id',
})
}

const currentLoggedUser = req.userId;
const result = await ChatMessageModel.markMessageRead(roomId, currentLoggedUser);
return res.status(200).json({ success: true, data: result });
} catch (error) {
console.log(error);
return res.status(500).json({ success: false, error });
}
},

All we are doing here is first checking if the room exists or not. If it does, we proceed further. We take in the

req.user.id
as
currentLoggedUser
and pass it to the following function:

我们在这里要做的就是首先检查房间是否存在。 如果是这样,我们将继续进行。 我们将

req.user.id
作为
currentLoggedUser
并将其传递给以下函数:

ChatMessageModel.markMessageRead(roomId, currentLoggedUser);

Which in our

ChatMessage
model is defined like this:

在我们的

ChatMessage
模型中,哪个定义如下:

chatMessageSchema.statics.markMessageRead = async function (chatRoomId, currentUserOnlineId) {
try {
return this.updateMany(
{
chatRoomId,
'readByRecipients.readByUserId': { $ne: currentUserOnlineId }
},
{
$addToSet: {
readByRecipients: { readByUserId: currentUserOnlineId }
}
},
{
multi: true
}
);
} catch (error) {
throw error;
}
}

A possible use case is that the user might not have read the last 15 messages once they open up a specific room conversation. They should all be marked as read. So we're using the

this.updateMany
function by mongoose.

一个可能的用例是,一旦打开特定的会议室对话,用户可能就没有阅读最近的15条消息。 它们都应标记为已读。 因此,我们使用猫鼬的

this.updateMany
函数。

The query itself is defined in 2 steps:

查询本身分为两个步骤:

  • Find

  • Update

    更新资料

And there can be multiple statements be updated.

并且可以有多个语句被更新。

To find a section, do this:

要查找部分,请执行以下操作:

{
chatRoomId,
'readByRecipients.readByUserId': { $ne: currentUserOnlineId }
},

This says I want to find all the message posts in the

chatmessages
collection where
chatRoomId
matches and
readByRecipients
array does not. The
userId
that I am passing to this function is
currentUserOnlineId
.

这表示我想在

chatmessages
集合中找到所有与
chatRoomId
匹配而
readByRecipients
数组不匹配的消息。 我要传递给此函数的
userId
currentUserOnlineId

Once it has all those documents where the criteria matches, it's then time to update them:

一旦所有这些文档都符合条件,就可以更新它们了:

{
$addToSet: {
readByRecipients: { readByUserId: currentUserOnlineId }
}
},

$addToSet
will just push a new entry to the
readByRecipients
array. This is like
Array.push
but for mongo.

$addToSet
只会将一个新条目推送到
readByRecipients
数组。 这就像
Array.push
但适用于mongo。

Next we want to tell

mongoose
to not just update the first record it finds, but also to update all the records where the condition matches. So doing this:

接下来,我们要告诉

mongoose
不仅要更新它找到的第一条记录,还要更新条件匹配的所有记录。 这样做:

{
multi: true
}

And that is all – we return the data as is.

仅此而已–我们将按原样返回数据。

Let's run this API.

让我们运行此API。

Start up the server:

启动服务器:

npm start;

Open your postman and create a new

PUT
request to test this route
ocalhost:3000/room/<room=id-here>/mark-read
:

打开邮递员并创建一个新的

PUT
请求以测试此路线
ocalhost:3000/room/<room=id-here>/mark-read

奖金部分 (Bonus Section)

  • How to delete a chat room and all its related messages

    如何删除聊天室及其所有相关消息
  • How to delete a message by its message id

    如何通过消息ID删除消息

And we are done! Wow that was a lot of learning today.

我们完成了! 哇,今天有很多东西要学习。

You can find the source code of this tutorial here.

您可以在此处找到本教程的源代码。

Reach out to me on twitter with your feedback – I would love to hear if you have any suggestions for improvements: twitter.com/adeelibr

在Twitter上与我联系,提供您的反馈意见-如果您有任何改进建议,我很想听听: twitter.com/adeelibr

If you liked to this article, please do give the github repository a star and subscribe to my youtube channel.

如果您喜欢本文,请给github存储库加一个星号,然后订阅我的youtube频道

翻译自: https://www.freecodecamp.org/news/create-a-professional-node-express/

nodejs入门

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: