绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
使用socket io和rethinkdb构建一个聊天应用程序
2022-04-08 17:12:59

A lot of tutorials can be found that teach you how to build a chat app with Socket.io. However, have you ever wondered how to best persist those chat messages?

可以找到很多教程,它们教您如何使用Socket.io构建聊天应用程序。 但是,您是否曾经想过如何好地保留这些聊天消息?

Enter RethinkDB, a realtime schema-less database. You can store and handle documents easily, just like in MongoDB, but it has reactivity built into it. That means you can subscribe to queries and get notified when data changes, making it the perfect choice when it comes to storing chat messages.

输入RethinkDB,这是一个实时的无模式数据库。 您可以像在MongoDB中一样轻松地存储和处理文档,但是它内置了React性。 这意味着您可以订阅查询并在数据更改时得到通知,这使其成为存储聊天消息的理想选择。

In this article, you will learn how to create a simple chat app with Socket.io and persist the messages in RethinkDB. To show the usefulness of a reactive database, we will also add a simple bot that reacts when you address it.

在本文中,您将学习如何使用Socket.io创建一个简单的聊天应用程序并将消息保留在RethinkDB中。 为了展示React型数据库的有用性,我们还将添加一个简单的机器人,当您处理它时会做出React。

You can also try the running app, or check out the code repository.

您也可以尝试运行的应用程序 ,或签出代码存储库 。

应用程序设置 (Application setup)

We will build a Node.js app, so you need to have node and npm installed. If you want to deploy your app to Heroku, you will also need a Heroku account, as well having their CLI installed. To run your app locally, you need to install and run a RethinkDB instance.

我们将构建一个Node.js应用程序,因此您需要安装nodenpm 。 如果要将应用程序部署到Heroku,则还需要一个Heroku帐户 ,并安装其CLI 。 要在本地运行您的应用,您需要安装并运行RethinkDB实例 。

To create the application, run the following in a terminal.

要创建该应用程序,请在终端中运行以下命令。

$ mkdir rethink-chat && cd rethink-chat$ npm init -y$ npm install rethinkdb express morgan http socket.io lorem-ipsum

This will initialize a Node.js app and install all required dependencies.

这将初始化Node.js应用并安装所有必需的依赖项。

准备一个Heroku应用 (Prepare a Heroku app)

In order to deploy the application to Heroku we need to create a Heroku app:

为了将应用程序部署到Heroku,我们需要创建一个Heroku应用程序:

$ git init$ heroku create

We will also need a RethinkDB instance to store and subscribe to the chat messages sent between users. You can do this via the RethinkDB Cloud add-on as follows:

我们还将需要一个RethinkDB实例来存储和订阅用户之间发送的聊天消息。 您可以通过RethinkDB Cloud插件执行以下操作:

$ heroku addons:create rethinkdb

The RethinkDB Cloud add-on is currently in alpha. Request an invite for your Heroku account email.

RethinkDB Cloud附加组件当前处于Alpha状态。 请求邀请您的Heroku帐户电子邮件 。

构建服务器 (Building the server)

To begin, let us set up the Node.js server. Create an index.js file and add the following server skeleton. We use an Express.js server to handle http traffic and Socket.io to handle WebSocket connections with clients.

首先,让我们设置Node.js服务器。 创建一个index.js文件,并添加以下服务器框架。 我们使用Express.js服务器处理http流量,并使用Socket.io处理与客户端的WebSocket连接。

  1. // index.js
  2. // Setup Express and Socket.io servers
  3. var express = require("express");
  4. const app = express();
  5. var http = require("http").createServer(app);
  6. var io = require("socket.io")(http);
  7. // Logging middleware
  8. var morgan = require("morgan");
  9. app.use(morgan("combined"));
  10. // Serve frontend
  11. app.use(express.static("public"));
  12. // Keep track of room subscriptions in RethinkDB
  13. const watchedRooms = {};
  14. // Lazy RethinkDB connection
  15. // ...
  16. // Route to access a room
  17. // ...
  18. // Socket.io (listen for new messages in any room)
  19. // ...
  20. // HTTP server (start listening)
  21. const listenPort = process.env.PORT || "3000";
  22. http.listen(listenPort, () => {
  23. console.log("listening on *:" + listenPort);
  24. });

This skeleton serves a static frontend from the public folder. We will create the frontend code later. In addition our server needs to do three things:

这个骨架供应来自静态前端public文件夹。 稍后我们将创建前端代码。 另外,我们的服务器需要做三件事:

  1. Handle connections to the RethinkDB database

    处理与RethinkDB数据库的连接
  2. Create an Express.js route that will give a user access to the chat room

    创建一个Express.js路由,该路由将使用户能够访问聊天室
  3. Configure the Socket.io server to listen to incoming chat messages

    配置Socket.io服务器以侦听传入的聊天消息

RethinkDB连接 (RethinkDB connection)

We manage our RethinkDB connection lazily, i.e., we only create the (re-)connection when it is actually needed. The connection parameters are parsed from environment variables, or the defaults are used.

我们懒惰地管理我们的RethinkDB连接,即,仅在实际需要时才创建(重新)连接。 连接参数是从环境变量解析的,或者使用默认值。

  1. // index.js
  2. // ...
  3. // Lazy RethinkDB connection
  4. var r = require("rethinkdb");
  5. let rdbConn = null;
  6. const rdbConnect = async function () {
  7. try {
  8. const conn = await r.connect({
  9. host: process.env.RETHINKDB_HOST || "localhost",
  10. port: process.env.RETHINKDB_PORT || 28015,
  11. username: process.env.RETHINKDB_USERNAME || "admin",
  12. password: process.env.RETHINKDB_PASSWORD || "",
  13. db: process.env.RETHINKDB_NAME || "test",
  14. });
  15. // Handle close
  16. conn.on("close", function (e) {
  17. console.log("RDB connection closed: ", e);
  18. rdbConn = null;
  19. });
  20. console.log("Connected to RethinkDB");
  21. rdbConn = conn;
  22. return conn;
  23. } catch (err) {
  24. throw err;
  25. }
  26. };
  27. const getRethinkDB = async function () {
  28. if (rdbConn != null) {
  29. return rdbConn;
  30. }
  31. return await rdbConnect();
  32. };

On Heroku, the RethinkDB Cloud add-on will set the environment variables. For a locally running instance of RethinkDB, the defaults should work.

在Heroku上,RethinkDB Cloud插件将设置环境变量。 对于本地运行的RethinkDB实例,默认值应该起作用。

前往检修室的路线 (Route to access room)

As mentioned earlier, the frontend is static. We do however need a route to access a chat room. The route will return the message history of a given room, as well as a WebSocket handle to access it.

如前所述,前端是静态的。 但是,我们确实需要进入聊天室的路线。 该路由将返回给定房间的消息历史记录,以及用于访问它的WebSocket句柄。

  1. // index.js
  2. // ...
  3. // Route to access a room
  4. app.get("/chats/:room", async (req, res) => {
  5. const conn = await getRethinkDB();
  6. const room = req.params.room;
  7. let query = r.table("chats").filter({ roomId: room });
  8. // Subscribe to new messages
  9. if (!watchedRooms[room]) {
  10. query.changes().run(conn, (err, cursor) => {
  11. if (err) throw err;
  12. cursor.each((err, row) => {
  13. if (row.new_val) {
  14. // Got a new message, send it via Socket.io
  15. io.emit(room, row.new_val);
  16. }
  17. });
  18. });
  19. watchedRooms[room] = true;
  20. }
  21. // Return message history & Socket.io handle to get new messages
  22. let orderedQuery = query.orderBy(r.desc("ts"));
  23. orderedQuery.run(conn, (err, cursor) => {
  24. if (err) throw err;
  25. cursor.toArray((err, result) => {
  26. if (err) throw err;
  27. res.json({
  28. data: result,
  29. handle: room,
  30. });
  31. });
  32. });
  33. });

This is where the RethinkDB magic happens. The first time this route is called for a particular room (when the first person joins), we subscribe to a RethinkDB query to get notified whenever a new chat message is available. We send new chat messages via Socket.io to any clients listening for the room’s handle.

这就是RethinkDB魔术发生的地方。 次为特定房间(当个人加入时)调用此路由,我们订阅RethinkDB查询以在有新的聊天消息可用时得到通知。 我们通过Socket.io将新的聊天消息发送给所有监听房间句柄的客户端。

收听新消息 (Listen for new messages)

The last puzzle piece of the server is to listen and save all incoming chat messages. Whenever a message comes in via the chats handle of the Socket.io connection, we save it to the chats table in RethinkDB.

服务器的后一个难题是监听并保存所有传入的聊天消息。 每当通过Socket.io连接的chats句柄传入消息时,我们会将其保存到RethinkDB中的chats表中。

  1. // index.js
  2. // ...
  3. // Socket.io (listen for new messages in any room)
  4. io.on("connection", (socket) => {
  5. socket.on("chats", async (msg) => {
  6. const conn = await getRethinkDB();
  7. r.table("chats")
  8. .insert(Object.assign(msg, { ts: Date.now() }))
  9. .run(conn, function (err, res) {
  10. if (err) throw err;
  11. });
  12. });
  13. });

Saving a value in the chats table will trigger the subscription we added above, causing the message to be sent to all clients listening to this room, including the sender of the message.

chats表中保存一个值将触发我们在上面添加的订阅,从而导致该消息被发送给所有监听此会议室的客户端,包括该消息的发送者。

前端 (Frontend)

For our frontend we will use an embedded Vue.js app. This makes the frontend simple, but gives us access to all of Vue’s awesome features. The frontend consists of a layout file as well as JavaScript and CSS assets.

对于我们的前端,我们将使用嵌入式Vue.js应用程序。 这使前端变得简单,但使我们能够使用Vue的所有出色功能。 前端由布局文件以及JavaScript和CSS资产组成。

  • The layout file only serves as a mount point for the Vue app in addition to importing the dependencies.

    布局文件除了导入依赖项外,仅用作Vue应用程序的安装点。
  1. <!-- public/index.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8" />
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7. <title>RethinkDB Chat with SocketIO</title>
  8. <link href="/css/main.css" rel="stylesheet" />
  9. </head>
  10. <body>
  11. <div id="app">
  12. <router-view></router-view>
  13. </div>
  14. <script src="/socket.io/socket.io.js"></script>
  15. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  16. <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
  17. <script src="/js/app.js" language="javascript"></script>
  18. </body>
  19. </html>
  • The CSS asset contains the styling of the frontend. It is long, not very interesting, and can be found here.

    CSS资产包含前端的样式。 它很长,不是很有趣,可以在这里找到。

  • The JavaScript asset app.js contains the actual Vue app.

    JavaScript资产app.js包含实际的Vue应用程序。

  1. // public/js/app.js
  2. // Create random username
  3. let username = Math.random().toString(36).substring(2, 8);
  4. // Setup Socket.io
  5. var socket = io();
  6. // Main view
  7. // ...
  8. // Room view, holds a chat room component
  9. // ...
  10. // Chat room component
  11. // ...
  12. // Setup routes
  13. const router = new VueRouter({
  14. routes: [
  15. { path: "/", component: MainView },
  16. { path: "/:roomId", name: "room", component: RoomView },
  17. ],
  18. });
  19. // Mount Vue app
  20. var app = new Vue({
  21. router,
  22. }).$mount("#app");

The Vue app contains two routes. The/ path points to the main view and the /:roomId path points to the room view.

Vue应用程序包含两条路线。 /路径指向主视图, /:roomId路径指向房间视图。

主视图 (Main view)

The main view lets you choose a username (default is a random string) and allows you to join a room with a given name.

主视图允许您选择用户名(默认为随机字符串),并允许您使用给定名称加入房间。

  1. // public/js/app.js
  2. // ...
  3. // Main view
  4. const MainView = Vue.component("main-view", {
  5. data() {
  6. return {
  7. room: "lobby",
  8. user: username,
  9. };
  10. },
  11. methods: {
  12. gotoRoom() {
  13. username = this.user;
  14. this.$router.push({ name: "room", params: { roomId: this.room } });
  15. },
  16. },
  17. template: `
  18. <div class="main">
  19. <form class="main" v-on:submit.prevent="gotoRoom">
  20. <label>Username: <input v-model="user" type="text" /></label>
  21. <label>Room: <input v-model="room" type="text" /></label>
  22. <button>Join</button>
  23. </form>
  24. </div>
  25. `,
  26. });

Whenever you join a room, the Vue router will load the chat room view.

无论何时加入会议室,Vue路由器都会加载聊天室视图。

聊天室 (Chat room)

The chat room, a room view containing a chat room component. makes a request to the Express route to join the given room when it is created. It also registers a Socket.io handler that listens for incoming chat messages and adds them to the list of messages.

聊天室,包含聊天室组件的房间视图。 创建指定房间时,它会向Express路由发出请求以加入该房间。 它还注册一个Socket.io处理程序,该处理程序侦听传入的聊天消息并将其添加到消息列表中。

The chat room allows the user to type and send a message which will then be sent to the server via the WebSocket handled by Socket.io.

聊天室允许用户键入并发送一条消息,然后该消息将通过Socket.io处理的WebSocket发送到服务器。

  1. // public/js/app.js
  2. // ...
  3. // Room view, holds a chat room component
  4. const RoomView = Vue.component("room-view", {
  5. template: `<chat-room :roomId="$route.params.roomId"/>`,
  6. });
  7. // Chat room component
  8. const ChatRoom = Vue.component("chat-room", {
  9. props: ["roomId"],
  10. data() {
  11. return {
  12. chats: [],
  13. message: "",
  14. username: username,
  15. handle: null,
  16. };
  17. },
  18. async created() {
  19. const url = new URL(document.location.protocol + "//" + document.location.host + "/chats/" + this.roomId);
  20. const chatsResp = await fetch(url);
  21. const { data, handle } = await chatsResp.json();
  22. this.chats = data;
  23. this.handle = handle;
  24. socket.on(this.handle, (msg) => {
  25. this.chats.unshift(msg);
  26. });
  27. },
  28. beforeDestroy() {
  29. socket.off(this.handle);
  30. },
  31. methods: {
  32. sendMessage() {
  33. socket.emit("chats", {
  34. msg: this.message,
  35. user: this.username,
  36. roomId: this.roomId,
  37. });
  38. this.message = "";
  39. },
  40. },
  41. template: `
  42. <div class="chatroom">
  43. <ul id="chatlog">
  44. <li v-for="chat in chats">
  45. <span class="timestamp">
  46. {{ new Date(chat.ts).toLocaleString(undefined, {dateStyle: 'short', timeStyle: 'short'}) }}
  47. </span>
  48. <span class="user">{{ chat.user }}:</span>
  49. <span class="msg">{{ chat.msg }}</span>
  50. </li>
  51. </ul>
  52. <label id="username">Username:
  53. {{ username }}
  54. </label>
  55. <form v-on:submit.prevent="sendMessage">
  56. <input v-model="message" autocomplete="off" />
  57. <button>Send</button>
  58. </form>
  59. </div>
  60. `,
  61. });

Now we have a working server and frontend. The last thing we need is to make sure the chats table actually exists in the RethinkDB database when we run the app.

现在我们有一个工作的服务器和前端。 我们需要做的后一件事是在运行应用程序时确保chats表确实存在于RethinkDB数据库中。

数据库迁移 (Database migration)

The app does not work without a chats table. We thus need a database migration that adds the table.

没有chats表,该应用程序将无法运行。 因此,我们需要添加表的数据库迁移。

  1. // migrate.js
  2. var r = require("rethinkdb");
  3. r.connect(
  4. {
  5. host: process.env.RETHINKDB_HOST || "localhost",
  6. port: process.env.RETHINKDB_PORT || 28015,
  7. username: process.env.RETHINKDB_USERNAME || "admin",
  8. password: process.env.RETHINKDB_PASSWORD || "",
  9. db: process.env.RETHINKDB_NAME || "test",
  10. },
  11. function (err, conn) {
  12. if (err) throw err;
  13. r.tableList().run(conn, (err, cursor) => {
  14. if (err) throw err;
  15. cursor.toArray((err, tables) => {
  16. if (err) throw err;
  17. // Check if table exists
  18. if (!tables.includes("chats")) {
  19. // Table missing --> create
  20. console.log("Creating chats table");
  21. r.tableCreate("chats").run(conn, (err, _) => {
  22. if (err) throw err;
  23. console.log("Creating chats table -- done");
  24. conn.close();
  25. });
  26. } else {
  27. // Table exists --> exit
  28. conn.close();
  29. }
  30. });
  31. });
  32. },
  33. );

This migration checks if the chats table exists, and if it is missing, it creates it.

此迁移检查chats表是否存在,如果缺少,则创建它。

一个简单的聊天机器人 (A simple chat bot)

As we saw, one of RethinkDBs great features is the baked in reactivity that allows us to subscribe to queries. This feature also comes in handy when creating a simple chat bot. The bot simply needs to subscribe to changes in the chats table and react to them whenever appropriate.

正如我们所看到的,RethinkDB的强大功能之一是响应式烘焙,它使我们能够订阅查询。 创建简单的聊天机器人时,此功能也很方便。 机器人只需要订阅chats表中的更改,并在适当的时候对它们做出React。

Our Lorem bot will reply with a random section of Lorem Ipsum whenever prompted with @lorem. The bot subscribes to the chats table and scans the beginning of the message. If it starts with@lorem, it will reply with a message in the same room.

每当有@lorem提示时,我们的Lorem机器人将随机回复Lorem Ipsum部分。 该漫游器订阅chats表并扫描消息的开头。 如果以@lorem ,它将在同一房间回复并显示一条消息。

  1. // lorem-bot.js
  2. const LoremIpsum = require("lorem-ipsum").LoremIpsum;
  3. const lorem = new LoremIpsum({
  4. sentencesPerParagraph: {
  5. max: 8,
  6. min: 4,
  7. },
  8. wordsPerSentence: {
  9. max: 16,
  10. min: 4,
  11. },
  12. });
  13. // Run Lorem bot
  14. const runBot = function (conn) {
  15. console.log("Lorem bot started");
  16. r.table("chats")
  17. .changes()
  18. .run(conn, (err, cursor) => {
  19. if (err) throw err;
  20. cursor.each((err, row) => {
  21. const msg = row.new_val.msg.trim().split(/\s+/);
  22. // Is the message directed at me?
  23. if (msg[] === "@lorem") {
  24. let num = 10;
  25. if (msg.length >= 1) {
  26. num = parseInt(msg[1]) || num;
  27. }
  28. r.table("chats")
  29. .insert({
  30. user: "lorem",
  31. msg: lorem.generateWords(num),
  32. roomId: row.new_val.roomId,
  33. ts: Date.now(),
  34. })
  35. .run(conn, function (err, res) {
  36. if (err) throw err;
  37. });
  38. }
  39. });
  40. });
  41. };
  42. // Connect to RethinkDB
  43. const r = require("rethinkdb");
  44. const rdbConnect = async function () {
  45. try {
  46. const conn = await r.connect({
  47. host: process.env.RETHINKDB_HOST || "localhost",
  48. port: process.env.RETHINKDB_PORT || 28015,
  49. username: process.env.RETHINKDB_USERNAME || "admin",
  50. password: process.env.RETHINKDB_PASSWORD || "",
  51. db: process.env.RETHINKDB_NAME || "test",
  52. });
  53. // Handle close
  54. conn.on("close", function (e) {
  55. console.log("RDB connection closed: ", e);
  56. setTimeout(rdbConnect, 10 * 1000); // reconnect in 10s
  57. });
  58. // Start the lorem bot
  59. runBot(conn);
  60. } catch (err) {
  61. throw err;
  62. }
  63. };
  64. rdbConnect();

将应用程序部署到Heroku (Deploy the application to Heroku)

To deploy our working application and bot to Heroku we need to create a Procfile. This file basically tells Heroku what processes to run.

要将我们的工作应用程序和机器人部署到Heroku,我们需要创建一个Procfile 。 该文件基本上告诉Heroku要运行哪些进程。

// Procfilerelease: node migrate.jsweb: node index.jslorem-bot: node lorem-bot.js

The release and web processes are recognized by Heroku as the command to run upon release and the main web app respectively. The lorem-bot process is just a worker process that could have any name.

Heroku将releaseweb进程识别为分别在发行版和主Web应用程序上运行的命令。 lorem-bot进程只是一个工作进程,可以使用任何名称。

Deploy the app to Heroku with

使用将应用程序部署到Heroku

$ echo "node_modules/" > .gitignore$ git add .$ git commit -m 'Working rethink-chat app'$ git push heroku master

You will need to manually enable the lorem-bot process in your Heroku app. You can do so on the Resources tab.

您将需要在Heroku应用中手动启用lorem-bot进程。 您可以在“资源”选项卡上执行此操作。

结论 (Conclusion)

In less than 15 minutes we managed to create and deploy a chat application with a simple bot. This shows the power and ease of use of RethinkDB. The ability to subscribe to queries makes it easy to build a reactive app and a natural fit to interact with Socket.io. Further, Heroku makes deployment a breeze, and with the RethinkDB Cloud add-on you will never have to do the tedious work of managing a database server yourself.

在不到15分钟的时间内,我们设法用一个简单的机器人创建并部署了一个聊天应用程序。 这显示了RethinkDB的强大功能和易用性。 订阅查询的功能使构建响应式应用程序变得容易,并且自然可以与Socket.io进行交互。 而且,Heroku使部署变得轻而易举,并且有了RethinkDB Cloud附加组件,您将不必亲自进行管理数据库服务器的繁琐工作。

Originally published at https://www.rethinkdb.cloud on August 17, 2020.

初于 2020年8月17日 发布在 https://www.rethinkdb.cloud 

翻译自: https://medium.com/swlh/build-a-chat-app-with-socket-io-and-rethinkdb-df10c5c27bb1

分享好友

分享这个小栈给你的朋友们,一起进步吧。

RethinkDB介绍
创建时间:2022-04-08 17:02:34
RethinkDB
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • LCR_
    专家
戳我,来吐槽~