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.
您是否想过聊天应用程序在后台如何工作? 好吧,今天,我将向您介绍如何使用MongoDB在NodeJS / 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.
我一直在研究本文的内容已有一个多星期了,我真的希望它能对那里的人们有所帮助。
Set up Mongodb on your machine [Installation guide written here]
在您的计算机上设置Mongodb [ 在此处编写安装指南 ]
For windows users, you can find the installation guide [here]
对于Windows用户,您可以找到安装指南[ 这里 ]
For macOS users, you can find the installation guide [here][To the point installation that I wrote]
对于macOS用户,您可以找到安装指南[ 这里 ] [ 至我编写的安装点 ]
For Linux users, you can find the installation guide [here]
对于Linux用户,您可以找到安装指南[ 这里 ]
Install Node/NPM on your machine [Installation link here] (I am using Node version v12.18.0)
在您的机器上安装Node / NPM [ 安装链接在此处 ](我使用的是Node版本v12.18.0)
Before we begin, I wanted to touch on some basics in the following videos.
在开始之前,我想介绍以下视频中的一些基础知识。
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。 它包含了我在视频中提到的所有相关学习链接,以及有关邮递员的令人惊叹的半小时教程。
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的基础知识以及如何验证用户响应。
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:
我们最终做了以下工作:
Added cross-origin-resource (CORS) to our server.js
在我们的server.js
添加了跨源资源(CORS)
Added a logger to our server.js
在我们的server.js
添加了一个记录器
Nothing fancy so far that I haven't covered in the videos above.
到目前为止,上面的视频都还没有介绍我。
Before we add MongoDB to our code base, make sure it is installed in your machine by running one of the following:
在将MongoDB添加到我们的代码库之前,请通过运行以下操作之一确保它已安装在您的计算机中:
For Windows users installation guide [here]
对于Windows用户安装指南[ 这里 ]
For macOS users installation guide [here][To the point installation that I wrote]
对于macOS用户,安装指南[ 这里 ] [ 到我写的要点安装 ]
For Linux users installation guide [here]
对于Linux用户安装指南[ 这里 ]
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告诉我,我将尽力为您解决。 :)
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的设置将没有身份验证令牌,因为我的主要重点是在此处向您介绍聊天应用程序。
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.
如果您被困在任何地方,请看一下源代码 。
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,因为没有用户就不会聊天(除非我们有机器人,但机器人也是用户)。
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。
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.
我继续前进,最终创建了更多用户。
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都将通过身份验证。
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 /:
defined in our routes section.
我们从req.params
获取userId
。 如果您还记得前面的视频,则req.params
是我们的路线部分中定义的/:
。
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
. Want to read more on this? Check out this nice thread on quora.
它被空格split(' ')
,然后我们通过访问数组的[1]
索引来获取数组的第二个索引,因为约定是authorization: Bearer
。 想了解更多吗? 在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)。
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/:
[POST] route. If everything goes smoothly the user will get an authorization
token.
因此,我们要做的就是将encode
中间件添加到我们的http://localhost:3000/login/:
[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/
:
让我们运行邮递员。 创建一个新的POST请求http://localhost:3000/login/
:
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.
好多 但是现在我们只能专注于聊天路线。
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类在这里有三大方面:
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
并向其中添加一些事件
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.
如果您迷路了,这里是此聊天应用程序的全部源代码 。 另外,请随时给我您的反馈信息,我将尝试改进本教程的内容。
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
常量,只有两种类型
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:
此函数接受三个参数:
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
我们正在检查以查找聊天室文档,其中该聊天室文档在我们的聊天室集合中存在某项
the userIds
are the same as the one we are passing to this function (irrespective of the user ids order), and
userIds
与我们传递给该函数的userIds
相同(与用户ID顺序无关),并且
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):
让我们运行它,看看它是否有效(这是我做的一个视频):
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:
该视频中讨论的操作员是:
$match
$ match
$last
$ last
$addToSet
$ addToSet
$lookup
$ lookup
$unwind
$放开
$group
$组
Now that we have
现在我们有了
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.
所有源代码都可在此处获得 。
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:
查询本身分为两个步骤:
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/
:
打开邮递员并创建一个新的PUT
请求以测试此路线ocalhost:3000/room/
:
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入门