如何用Vanilla JS、Twilio和Node.js构建一个群组聊天应用程序

463 阅读14分钟

如何用Vanilla JS、Twilio和Node.js建立一个群聊应用

聊天正在成为商业和社会背景下越来越受欢迎的交流媒介。企业将聊天用于客户和员工的公司内部交流,比如用SlackMicrosoft TeamsChantyHubSpot Live ChatHelp Scout等。大多数社交网络和通信应用程序也默认提供聊天选项,如Instagram、Facebook、Reddit和Twitter。其他应用程序如Discord、Whatsapp和Telegram大多是基于聊天的,群聊是其主要功能之一。

虽然存在众多促进聊天的产品,但您可能需要为您的网站定制一个适合您特定交流需求的解决方案。例如,这些产品中有许多是独立的应用程序,可能无法整合到您自己的网站中。让您的用户离开您的网站去聊天可能不是最好的选择,因为这可能会影响用户体验和转换。反过来说,从头开始建立一个聊天应用程序可能是一项艰巨的任务,有时甚至是压倒性的。然而,通过使用Twilio Conversations等API,您可以简化创建过程。这些通信API处理小组创建、添加参与者、发送消息、通知以及其他重要的聊天功能。使用这些API的后端应用程序只需处理认证和对这些API进行调用。然后,前端应用程序显示来自后台的对话、群组和消息。

在本教程中,你将学习如何使用Twilio Conversations API创建一个群组聊天应用程序。这个应用程序的前端将使用HTML、CSS和Vanilla JavaScript构建。它将允许用户创建群聊,发送邀请,登录,以及发送和接收消息。后台将是一个Node.js应用程序。它将为聊天邀请者提供认证令牌并管理聊天创建。

先决条件

在你开始本教程之前,你需要具备以下条件:

  • 安装Node.js
    *你可以使用Node.js下载页面上*的预制安装程序来获得它,它主要用于后端应用程序和在前端应用程序中安装依赖。
  • 一个Twilio账户。
    你可以通过 这个链接在Twilio网站创建一个。
  • http-server来提供前端应用程序。
    你可以通过运行npm i -g http-server 来安装它。你也可以用npx http-server 来一次性运行它。
  • MongoDB,用于后台应用的会话存储。
    它的安装页面*有一个关于如何运行它的详细指南。

后台应用程序

要使用Twilio API发送聊天信息,你需要一个对话。聊天信息是在一个对话中发送和接收的。发送消息的人被称为参与者。参与者只有被添加到对话中,才能在其中发送消息。对话和参与者都是使用Twilio API创建的。后台应用程序将执行这一功能。

参与者需要一个**访问令牌**来发送消息和获取他们订阅的对话。这个项目的前端部分将使用这个访问令牌。后台应用程序创建该令牌并将其发送到前端。在那里它将被用来加载对话和消息。

项目启动器

你将调用后端应用程序twilio-chat-serverGithub上有一个脚手架式的项目启动程序。要克隆该项目并获得启动程序,请运行:

git clone https://github.com/zaracooper/twilio-chat-server.git
cd twilio-chat-server
git checkout starter

后台应用程序采用这种结构:

.
├── app.js
├── config/
├── controllers/
├── package.json
├── routes/
└── utils/

要运行该应用程序,你将使用node index.js 命令。

依赖性

后台应用程序需要8个依赖项。你可以通过运行:来安装它们:

npm i 

下面是每个依赖项的列表:

  • connect-mongo 连接到MongoDB,你将使用它作为会话存储。
  • cors 处理CORS
  • dotenv 从 文件中加载环境变量,你将在后面的步骤中创建该文件。.env
  • express 是你将在后端使用的Web框架。
  • express-session 提供中间件来处理会话数据。
  • http-errors 帮助创建服务器错误。
  • morgan 处理日志。
  • twilio 创建Twilio客户端,生成令牌,创建对话,并添加参与者。

配置

config 文件夹负责从环境变量加载配置。配置分为三类:CORS、Twilio和MongoDB会话数据库的配置。当环境为development 时,你将使用dotenv.env 文件加载config

首先在终端上创建.env 文件。这个文件已经被添加到.gitignore 文件中,以防止它包含的敏感值被检入版本库:

touch .env

下面是你的.env 应该是这样的:

# Session DB Config
SESSION_DB_HOST=XXXX
SESSION_DB_USER=XXXX
SESSION_DB_PASS=XXXX
SESSION_DB_PORT=XXXX
SESSION_DB_NAME=XXXX
SESSION_DB_SECRET=XXXX

# Twilio Config
TWILIO_ACCOUNT_SID=XXXX
TWILIO_AUTH_TOKEN=XXXX
TWILIO_API_KEY=XXXX
TWILIO_API_SECRET=XXXX

# CORS Client Config
CORS_CLIENT_DOMAIN=XXXX

你可以从这个MongoDB手册条目中了解如何为你的会话数据库创建一个用户。一旦你创建了一个会话数据库和一个可以写入它的用户,你就可以填写SESSION_DB_USER,SESSION_DB_PASS, 和SESSION_DB_NAME 值。如果你正在运行MongoDB的本地实例,SESSION_DB_HOST 将是localhost ,而SESSION_DB_PORT 通常是27017SESSION_DB_SECRET 被 express-session 用来签署会话 ID cookie,它可以是你设置的任何秘密字符串。

在下一步,你将从Twilio控制台获得凭证。这些凭证应该被分配给带有TWILIO_ 前缀的变量。在本地开发期间,前端客户端将运行在http://localhost:3000。所以,你可以使用这个值作为CORS_CLIENT_DOMAIN 环境变量。

添加以下代码到 config/index.js来加载环境变量:

import dotenv from 'dotenv';

if (process.env.NODE_ENV == 'development') {
    dotenv.config();
}

const corsClient = {
    domain: process.env.CORS_CLIENT_DOMAIN
};

const sessionDB = {
    host: process.env.SESSION_DB_HOST,
    user: process.env.SESSION_DB_USER,
    pass: process.env.SESSION_DB_PASS,
    port: process.env.SESSION_DB_PORT,
    name: process.env.SESSION_DB_NAME,
    secret: process.env.SESSION_DB_SECRET
};

const twilioConfig = {
    accountSid: process.env.TWILIO_ACCOUNT_SID,
    authToken: process.env.TWILIO_AUTH_TOKEN,
    apiKey: process.env.TWILIO_API_KEY,
    apiSecret: process.env.TWILIO_API_SECRET
};

const port = process.env.PORT || '8000';

export { corsClient, port, sessionDB, twilioConfig };

环境变量根据它们的作用被分成了不同的类别。每个配置类别都有自己的对象变量,它们都被导出,以便在应用程序的其他部分使用。

从控制台获取Twilio凭证

要建立这个项目,你需要四个不同的Twilio凭证:一个账户SID,一个Auth Token,一个API密钥,和一个API秘密。在控制台中,在*常规设置页面*,向下滚动到API凭证部分。在这里你可以找到你的账户SIDAuth Token

API Credentials section on the General Settings page

要获得一个API密钥秘密,请到 API密钥页面.你可以在下面的截图中看到它。点击"+"按钮,进入 "新API密钥 "页面

A screenshot where it's shown how to create API key button on API Key list page

在API密钥列表页面创建API Key 按钮。(大图预览

)

Standard在这个页面上,添加一个密钥名称,把KEY TYPE ,然后点击创建API密钥。复制API密钥和秘密。你将在一个.env 文件中添加所有这些凭证,你将在后续步骤中看到。

A screenshot with the properties on a New API Key page

实用程序

后台应用程序需要两个工具函数。一个将创建一个令牌,另一个将包裹异步控制器并为其处理错误。

utils/token.js中,添加以下代码,创建一个名为createToken 的函数,该函数将生成Twilio访问令牌。

import { twilioConfig } from '../config/index.js';
import twilio from 'twilio';

function createToken(username, serviceSid) {
    const AccessToken = twilio.jwt.AccessToken;
    const ChatGrant = AccessToken.ChatGrant;

    const token = new AccessToken(
        twilioConfig.accountSid,
        twilioConfig.apiKey,
        twilioConfig.apiSecret,
        { identity: username }
    );

    const chatGrant = new ChatGrant({
        serviceSid: serviceSid,
    });

    token.addGrant(chatGrant);

    return token.toJwt();
}

在这个函数中,你使用你的账户SIDAPI密钥API秘密生成访问令牌。你可以选择提供一个独特的身份,可以是一个用户名、电子邮件等。创建令牌后,您必须为其添加一个聊天授权。聊天授权可以接受一个对话服务ID和其他可选的值。最后,你要将令牌转换为JWT并返回。

utils/controller.js文件中包含一个asyncWrapper 函数,用于包装异步控制器函数并捕获它们抛出的任何错误。将以下代码粘贴到该文件中。

function asyncWrapper(controller) {
    return (req, res, next) => Promise.resolve(controller(req, res, next)).catch(next);
}

export { asyncWrapper, createToken };

控制器

后台应用程序有四个控制器:两个用于认证,两个用于处理对话。第一个认证控制器创建一个令牌,第二个控制器删除它。其中一个对话控制器创建新的对话,而另一个则将参与者添加到现有的对话中。

会话控制器

controllers/conversations.js文件中,为StartConversation 控制器添加这些导入和代码。

import { twilioConfig } from '../config/index.js';
import { createToken } from '../utils/token.js';
import twilio from 'twilio';

async function StartConversation(req, res, next) {
    const client = twilio(twilioConfig.accountSid, twilioConfig.authToken);

    const { conversationTitle, username } = req.body;

    try {
        if (conversationTitle && username) {
            const conversation = await client.conversations.conversations
                .create({ friendlyName: conversationTitle });

            req.session.token = createToken(username, conversation.chatServiceSid);
            req.session.username = username;

            const participant = await client.conversations.conversations(conversation.sid)
                .participants.create({ identity: username })

            res.send({ conversation, participant });
        } else {
            next({ message: 'Missing conversation title or username' });
        }
    }
    catch (error) {
        next({ error, message: 'There was a problem creating your conversation' });
    }
}

StartConversation 控制器首先创建一个Twilioclient ,使用你的twilioConfig.accountSidtwilioConfig.authToken ,你从config/index.js 得到。

接下来,它创建一个对话。它需要一个对话的标题,它从请求体中得到这个标题。一个用户必须被添加到一个对话中,然后才能参与其中。如果没有访问令牌,参与者不能发送消息。因此,它使用请求正文中提供的用户名和conversation.chatServiceSid ,生成一个访问令牌。然后由用户名识别的用户被添加到对话中。控制器通过响应新创建的对话和参与者来完成。

接下来,你需要创建AddParticipant 控制器。要做到这一点,在你刚才在上面的controllers/conversations.js 文件中添加以下代码。

async function AddParticipant(req, res, next) {
    const client = twilio(twilioConfig.accountSid, twilioConfig.authToken);

    const { username } = req.body;
    const conversationSid = req.params.id;

    try {
        const conversation = await client.conversations.conversations
            .get(conversationSid).fetch();

        if (username && conversationSid) {
            req.session.token = createToken(username, conversation.chatServiceSid);
            req.session.username = username;

            const participant = await client.conversations.conversations(conversationSid)
                .participants.create({ identity: username })

            res.send({ conversation, participant });
        } else {
            next({ message: 'Missing username or conversation Sid' });
        }
    } catch (error) {
        next({ error, message: 'There was a problem adding a participant' });
    }
}

export { AddParticipant, StartConversation };

AddParticipant 控制器将新的参与者添加到已经存在的对话中。使用作为路由参数提供的conversationSid ,它获取对话。然后,它为用户创建一个令牌,并使用请求正文中的用户名将他们添加到对话中。最后,它将对话和参与者作为一个响应发送出去。

认证控制器

文件中的两个控制器 controllers/auth.js中的两个控制器被称为GetTokenDeleteToken 。通过复制和粘贴这些代码将它们添加到文件中。

function GetToken(req, res, next) {
    if (req.session.token) {
        res.send({ token: req.session.token, username: req.session.username });
    } else {
        next({ status: 404, message: 'Token not set' });
    }
}

function DeleteToken(req, res, _next) {
    delete req.session.token;
    delete req.session.username;

    res.send({ message: 'Session destroyed' });
}

export { DeleteToken, GetToken };

GetToken 控制器从会话中检索令牌和用户名(如果它们存在),并将它们作为响应返回。DeleteToken 删除会话。

路由

routes 文件夹有三个文件。index.js,conversations.js, 和auth.js

在文件中添加这些授权路由到 routes/auth.js文件中,添加这段代码。

import { Router } from 'express';

import { DeleteToken, GetToken } from '../controllers/auth.js';

var router = Router();

router.get('/', GetToken);
router.delete('/', DeleteToken);

export default router;

GET 路由在/ 路径上返回一个令牌,而DELETE 路由删除一个令牌。

接下来,复制并粘贴以下代码到 routes/conversations.js文件。

import { Router } from 'express';
import { AddParticipant, StartConversation } from '../controllers/conversations.js';
import { asyncWrapper } from '../utils/controller.js';

var router = Router();

router.post('/', asyncWrapper(StartConversation));
router.post('/:id/participants', asyncWrapper(AddParticipant));

export default router;

在这个文件中,创建了对话的路由器。一个用于创建对话的POST 路径/ 和另一个用于添加参与者的POST 路径/:id/participants 被添加到路由器中。

最后,将下面的代码添加到你的新 routes/index.js文件。

import { Router } from 'express';

import authRouter from './auth.js';
import conversationRouter from './conversations.js';

var router = Router();

router.use('/auth/token', authRouter);
router.use('/api/conversations', conversationRouter);

export default router;

通过在这里添加conversationauth 路由器,你将它们分别在/api/conversations/auth/token 上提供给主路由器。路由器然后被导出。

后台应用程序

现在是时候把后端部分放在一起了。在文本编辑器中打开该 index.js文件,并粘贴以下代码。

import cors from 'cors';
import createError from 'http-errors';
import express, { json, urlencoded } from 'express';
import logger from 'morgan';
import session from 'express-session';
import store from 'connect-mongo';

import { corsClient, port, sessionDB } from './config/index.js';

import router from './routes/index.js';

var app = express();

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

app.use(cors({
    origin: corsClient.domain,
    credentials: true,
    methods: ['GET', 'POST', 'DELETE'],
    maxAge: 3600 * 1000,
    allowedHeaders: ['Content-Type', 'Range'],
    exposedHeaders: ['Accept-Ranges', 'Content-Encoding', 'Content-Length', 'Content-Range']
}));
app.options('*', cors());

app.use(session({
    store: store.create({
        mongoUrl: `mongodb://${sessionDB.user}:${sessionDB.pass}@${sessionDB.host}:${sessionDB.port}/${sessionDB.name}`,
        mongoOptions: { useUnifiedTopology: true },
        collectionName: 'sessions'
    }),
    secret: sessionDB.secret,
    cookie: {
        maxAge: 3600 * 1000,
        sameSite: 'strict'
    },
    name: 'twilio.sid',
    resave: false,
    saveUninitialized: true
}));

app.use('/', router);

app.use(function (_req, _res, next) {
    next(createError(404, 'Route does not exist.'));
});

app.use(function (err, _req, res, _next) {
    res.status(err.status || 500).send(err);
});

app.listen(port);

这个文件从创建Express应用程序开始。然后它设置了JSON和URL编码的有效载荷解析,并添加了日志中间件。接下来,它设置了 CORS 和会话处理。如前所述,MongoDB被用来作为会话存储。

所有这些都设置好后,在配置错误处理之前,它又添加了在前面步骤中创建的路由器。最后,它使应用程序在.env 文件中指定的端口监听和接受连接。如果你没有设置端口,该应用程序将监听端口8000

一旦你完成了后端应用程序的创建,确保MongoDB正在运行,并通过在终端运行这个命令来启动它。

NODE_ENV=development npm start

你传递NODE_ENV=development 变量,这样配置就会从本地.env 文件中加载。

前端

这个项目的前端部分有几个功能。它允许用户创建对话,查看他们所参加的对话列表,邀请其他人参加他们创建的对话,并在对话中发送消息。这些作用是由四个页面实现的:

  • 会话页面
  • 一个聊天页面
  • 一个错误页面
  • 一个登录页面

你会把前端应用程序称为twilio-chat-appGithub上有一个脚手架式的启动程序。要克隆该项目并获得启动程序,请运行:

git clone https://github.com/zaracooper/twilio-vanilla-js-chat-app.git
cd twilio-vanilla-js-chat-app
git checkout starter

该应用程序采用这种结构:

.
├── index.html
├── pages
│   ├── chat.html
│   ├── conversation.html
│   ├── error.html
│   └── login.html
├── scripts
│   ├── chat.js
│   ├── conversation.js
│   └── login.js
└── styles
    ├── chat.css
    ├── main.css
    └── simple-page.css

样式和HTML标记已经被添加到启动程序中的每个页面。本节将只涉及你必须添加的脚本。

依赖性

该应用程序有两个依赖项。 axios@twilio/conversations.你将使用axios 来向后端应用程序发出请求,使用@twilio/conversations 来发送和获取信息以及脚本中的对话。你可以在终端上通过运行来安装它们:

npm i

索引页

这个页面作为应用程序的登陆页面。你可以在这里找到这个页面的标记(index.html)。它使用两个CSS样式表。 styles/main.css所有的页面都使用,而 styles/simple-page.css小的、不太复杂的页面使用。

你可以在前面的段落中找到这些样式表的链接内容。下面是这个页面的截图,它看起来像什么。

Twilio Vanilla JS Chat App Landing Page

错误页

当一个错误发生时,这个页面会显示出来。 pages/error.html 的内容可以在这里找到。如果发生错误,用户可以点击按钮进入主页。在那里,他们可以再次尝试他们所尝试的内容。

Twilio Vanilla JS Chat App Error Page

对话页面

在这个页面上,用户向一个表格提供要创建的对话的标题和他们的用户名。

pages/conversation.html 的内容可以在这里找到。在scripts/conversation.js 文件中添加以下代码:

window.twilioChat = window.twilioChat || {};

function createConversation() {
    let convoForm = document.getElementById('convoForm');
    let formData = new FormData(convoForm);

    let body = Object.fromEntries(formData.entries()) || {};

    let submitBtn = document.getElementById('submitConvo');
    submitBtn.innerText = "Creating..."
    submitBtn.disabled = true;
    submitBtn.style.cursor = 'wait';

    axios.request({
        url: '/api/conversations',
        baseURL: 'http://localhost:8000',
        method: 'post',
        withCredentials: true,
        data: body
    })
        .then(() => {
            window.twilioChat.username = body.username;
            location.href = '/pages/chat.html';
        })
        .catch(() => {
            location.href = '/pages/error.html';
        });
}

当用户点击提交按钮时,createConversation 函数被调用。在该函数中,表单的内容被收集起来,并被用于向后台的POST 请求的正文中,http://localhost:8000/api/conversations/

你将使用axios 来发出请求。如果请求成功,将创建一个对话,并将用户添加到其中。然后用户将被重定向到聊天页面,他们可以在对话中发送消息。

下面是一个对话页面的截图:

Twilio Vanilla JS Chat App Conversation Page

聊天页面

在这个页面上,用户将查看他们所参与的对话的列表,并向他们发送信息。你可以在这里找到 pages/chat.html 的标记,在这里找到 styles/chat.css 的样式。

scripts/chat.js 文件一开始就定义了一个命名空间twilioDemo

window.twilioChat = window.twilioChat || {};

添加下面的initClient 函数。它负责初始化Twilio客户端和加载对话:

async function initClient() {
    try {
        const response = await axios.request({
            url: '/auth/token',
            baseURL: 'http://localhost:8000',
            method: 'GETget',
            withCredentials: true
        });

        window.twilioChat.username = response.data.username;
        window.twilioChat.client = await Twilio.Conversations.Client.create(response.data.token);

        let conversations = await window.twilioChat.client.getSubscribedConversations();

        let conversationCont, conversationName;

        const sideNav = document.getElementById('side-nav');
        sideNav.removeChild(document.getElementById('loading-msg'));

        for (let conv of conversations.items) {
            conversationCont = document.createElement('button');
            conversationCont.classList.add('conversation');
            conversationCont.id = conv.sid;
            conversationCont.value = conv.sid;
            conversationCont.onclick = async () => {
                await setConversation(conv.sid, conv.channelState.friendlyName);
            };

            conversationName = document.createElement('h3');
            conversationName.innerText = `💬 ${conv.channelState.friendlyName}`;

            conversationCont.appendChild(conversationName);
            sideNav.appendChild(conversationCont);
        }
    }
    catch {
        location.href = '/pages/error.html';
    }
};

当页面加载时,initClient 从后台获取用户的访问令牌,然后用它来初始化客户端。一旦客户端被初始化,它就被用来获取用户订阅的所有对话。之后,对话被加载到side-nav 。如果发生任何错误,用户将被发送到错误页面。

setConversion 函数加载一个单一的对话。在文件中复制并粘贴下面的代码来添加它:

async function setConversation(sid, name) {
    try {
        window.twilioChat.selectedConvSid = sid;

        document.getElementById('chat-title').innerText = '+ ' + name;

        document.getElementById('loading-chat').style.display = 'flex';
        document.getElementById('messages').style.display = 'none';

        let submitButton = document.getElementById('submitMessage')
        submitButton.disabled = true;

        let inviteButton = document.getElementById('invite-button')
        inviteButton.disabled = true;

        window.twilioChat.selectedConversation = await window.twilioChat.client.getConversationBySid(window.twilioChat.selectedConvSid);

        const messages = await window.twilioChat.selectedConversation.getMessages();

        addMessagesToChatArea(messages.items, true);

        window.twilioChat.selectedConversation.on('messageAdded', msg => addMessagesToChatArea([msg], false));

        submitButton.disabled = false;
        inviteButton.disabled = false;
    } catch {
        showError('loading the conversation you selected');
    }
};

当用户点击一个特定的对话时,setConversation 被调用。这个函数接收对话的SID名称,并使用SID来获取对话和它的消息。然后,这些消息被添加到聊天区。最后,添加一个监听器,以观察添加到对话中的新消息。当收到这些新消息时,它们被追加到聊天区。如果发生任何错误,会显示错误信息。

这是聊天页面的截图:

A screenshot of the chat page

接下来,您将添加addMessagedToChatArea ,该函数加载对话消息:

function addMessagesToChatArea(messages, clearMessages) {
    let cont, msgCont, msgAuthor, timestamp;

    const chatArea = document.getElementById('messages');

    if (clearMessages) {
        document.getElementById('loading-chat').style.display = 'none';
        chatArea.style.display = 'flex';
        chatArea.replaceChildren();
    }

    for (const msg of messages) {
        cont = document.createElement('div');
        if (msg.state.author == window.twilioChat.username) {
            cont.classList.add('right-message');
        } else {
            cont.classList.add('left-message');
        }

        msgCont = document.createElement('div');
        msgCont.classList.add('message');

        msgAuthor = document.createElement('p');
        msgAuthor.classList.add('username');
        msgAuthor.innerText = msg.state.author;

        timestamp = document.createElement('p');
        timestamp.classList.add('timestamp');
        timestamp.innerText = msg.state.timestamp;

        msgCont.appendChild(msgAuthor);
        msgCont.innerText += msg.state.body;

        cont.appendChild(msgCont);
        cont.appendChild(timestamp);

        chatArea.appendChild(cont);
    }

    chatArea.scrollTop = chatArea.scrollHeight;
}

当从侧边导航中选择当前对话时,该函数addMessagesToChatArea 将当前对话的消息添加到聊天区。当有新消息添加到当前对话中时,也会调用它。在获取消息时,通常会显示一个加载消息。在对话信息被添加之前,这个加载信息被删除。来自当前用户的消息被排列在右边,而来自小组参与者的所有其他消息被排列在左边。

这就是加载消息的样子:

A screenshot with the 'loading messages' displayed in the middle

添加sendMessage 功能来发送消息。

function sendMessage() {
    let submitBtn = document.getElementById('submitMessage');
    submitBtn.disabled = true;

    let messageForm = document.getElementById('message-input');
    let messageData = new FormData(messageForm);

    const msg = messageData.get('chat-message');

    window.twilioChat.selectedConversation.sendMessage(msg)
        .then(() => {
            document.getElementById('chat-message').value = '';
            submitBtn.disabled = false;
        })
        .catch(() => {
            showError('sending your message');
            submitBtn.disabled = false;
        });
};

当用户发送消息时,sendMessage 函数被调用。它从文本区获取消息文本并禁用提交按钮。然后使用当前选择的对话,使用其sendMessage 方法发送消息。如果成功,文本区被清除,提交按钮被重新启用。如果不成功,就会显示一个错误信息。

showError 方法在被调用时显示一个错误信息;hideError 则隐藏它:

function showError(msg) {
    document.getElementById('error-message').style.display = 'flex';
    document.getElementById('error-text').innerText = `There was a problem ${msg ? msg : 'fulfilling your request'}.`;
}

function hideError() {
    document.getElementById('error-message').style.display = 'none';
}

这就是这个错误信息的模样:

A screenshot with the error message banner on top

logout 函数注销了当前用户。它通过向后台发出请求,清除他们的会话来做到这一点。然后用户会被重定向到对话页面,如果他们愿意,可以创建一个新的对话。

function logout(logoutButton) {
    logoutButton.disabled = true;
    logoutButton.style.cursor = 'wait';

    axios.request({
        url: '/auth/token',
        baseURL: 'http://localhost:8000',
        method: 'DELETEdelete',
        withCredentials: true
    })
        .then(() => {
            location.href = '/pages/conversation.html';
        })
        .catch(() => {
            location.href = '/pages/error.html';
        });
}

添加inviteFriend 功能来发送对话邀请。

async function inviteFriend() {
    try {
        const link = `http://localhost:3000/pages/login.html?sid=${window.twilioChat.selectedConvSid}`;

        await navigator.clipboard.writeText(link);

        alert(`The link below has been copied to your clipboard.\n\n${link}\n\nYou can invite a friend to chat by sending it to them.`);
    } catch {
        showError('preparing your chat invite');
    }
}

为了邀请其他人参与对话,当前用户可以向另一个人发送一个链接。这个链接是到登录页面,并包含当前对话的SID作为查询参数。当他们点击邀请按钮时,该链接被添加到他们的剪贴板上。然后会显示一个警告,给出邀请指示。

这里是一个邀请提示的截图:

A screenshot of the invite alert

登录页面

在这个页面上,当用户被邀请参加一个对话时,他们就会登录。你可以在这个链接中找到pages/login.html 的标记

scripts/login.jslogin 函数负责登录对话邀请者。复制下面的代码并将其添加到上述文件中。

function login() {
    const convParams = new URLSearchParams(window.location.search);
    const conv = Object.fromEntries(convParams.entries());

    if (conv.sid) {
        let submitBtn = document.getElementById('login-button');
        submitBtn.innerText = 'Logging in...';
        submitBtn.disabled = true;
        submitBtn.style.cursor = 'wait';

        let loginForm = document.getElementById('loginForm');
        let formData = new FormData(loginForm);
        let body = Object.fromEntries(formData.entries());
        
        axios.request({
            url: `/api/conversations/${conv.sid}/participants`,
            baseURL: 'http://localhost:8000',
            method: 'POSTpost',
            withCredentials: true,
            data: body
        })
            .then(() => {
                location.href = '/pages/chat.html';
            })
            .catch(() => {
                location.href = '/pages/error.html';
            });
    } else {
        location.href = '/pages/conversation.html';
    }
}

login 函数从URL中获取对话sid 查询参数,从表单中获取用户名。api/conversations/{sid}/participants/ 然后它向后端应用程序的POST 请求。后端应用程序将用户添加到对话中,并生成一个用于消息传递的访问令牌。如果成功,在后端为用户启动一个会话。

然后用户被重定向到聊天页面,但如果请求返回一个错误,他们会被重定向到错误页面。如果URL中没有对话sid 查询参数,用户会被重定向到对话页面

下面是登录页面的截图:

A screenshot of the login page

运行应用程序

在你启动前端应用程序之前,确保后端应用程序正在运行。如前所述,你可以在终端上使用这个命令启动后端应用程序。

NODE_ENV=development npm start

要为前端应用程序提供服务,在另一个终端窗口中运行这个命令。

http-server -p 3000

这样就可以在http://localhost:3000,为应用程序提供服务。一旦运行,就去http://localhost:3000/pages/conversation.html;为你的对话设置一个名称,并添加你的用户名,然后创建它。当你到了聊天页面,点击conversation ,然后点击邀请按钮。

在一个单独的隐身窗口中,粘贴邀请链接,并输入一个不同的用户名。一旦你在隐身窗口的聊天页面上,你就可以开始和自己聊天了。你可以在同一个对话中,在第一个窗口的用户和隐身窗口的第二个用户之间来回发送消息。

总结

在本教程中,你学会了如何使用Twilio Conversations和Vanilla JS创建一个聊天应用。你创建了一个Node.js应用,它可以生成用户访问令牌,为他们维护会话,创建对话,并将用户作为参与者加入其中。你还使用HTML、CSS和Vanilla JS创建了一个前端应用程序。这个应用程序应该允许用户创建对话,发送消息,并邀请其他人来聊天。它应该从后台应用获得访问令牌,并使用它们来执行这些功能。我希望这个教程能让你更好地了解Twilio Conversations是如何工作的,以及如何使用它进行聊天信息传递。