建立一个前端¶
正如我们在 Feathers 基础知识 中看到的,Feathers在浏览器中运行良好,并附带 客户端使用, 可以轻松连接到Feathers服务器.
在本章中,我们将使用现代纯JavaScript创建一个具有注册和登录功能的实时聊天前端.与 Feathers 基础知识 一样,它只适用于最新版本的 Chrome, Firefox, Safari 和 Edge,因为我们不会使用像 Babel 这样的转换器. 最终版本可以在 这里 找到.
注解
我们不会使用前端框架,因此我们可以专注于Feathers的全部内容. Feathers是框架无关的,可以与React,VueJS或Angular等任何前端框架一起使用.有关更多信息,请参阅 与前端框架集成.
设置索引页面¶
我们已经在 public 文件夹中提供静态文件,并在 public/index.html 中有一个占位符页面.让我们将其更新为以下内容:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
<title>Vanilla JavaScript Feathers Chat</title>
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/base.css">
<link rel="stylesheet" href="//cdn.rawgit.com/feathersjs/feathers-chat/v0.2.0/public/chat.css">
</head>
<body>
<div id="app" class="flex flex-column"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment.js"></script>
<script src="//unpkg.com/@feathersjs/client@^3.0.0/dist/feathers.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="app.js"></script>
</body>
</html>
这将加载我们的聊天CSS样式,添加容器 div #app 并加载几个库:
让我们创建 public/app.js,其中所有以下代码都将存在(每个代码示例都添加到该文件的末尾).
连接到API¶
我们首先从最重要的事情开始,即与Feathers API的连接.我们已经在 Feathers 基础知识 中学习了如何在客户端上使用Feathers.在这里,我们做了几乎相同的事情:建立一个Socket连接并初始化一个新的Feathers应用程序.我们还为以后设置了身份验证客户端:
// Establish a Socket.io connection
const socket = io();
// Initialize our Feathers client application through Socket.io
// with hooks and authentication.
const client = feathers();
client.configure(feathers.socketio(socket));
// Use localStorage to store our login token
client.configure(feathers.authentication({
storage: window.localStorage
}));
这允许我们通过websockets与聊天API通信,以进行实时更新.
基础 和 用户/消息 列表HTML¶
接下来,我们必须定义一些静态和动态HTML,当我们想要显示登录页面(也可以作为注册页面)和实际聊天界面时,我们可以将其插入到页面中:
// Login screen
const loginHTML = `<main class="login container">
<div class="row">
<div class="col-12 col-6-tablet push-3-tablet text-center heading">
<h1 class="font-100">Log in or signup</h1>
</div>
</div>
<div class="row">
<div class="col-12 col-6-tablet push-3-tablet col-4-desktop push-4-desktop">
<form class="form">
<fieldset>
<input class="block" type="email" name="email" placeholder="email">
</fieldset>
<fieldset>
<input class="block" type="password" name="password" placeholder="password">
</fieldset>
<button type="button" id="login" class="button button-primary block signup">
Log in
</button>
<button type="button" id="signup" class="button button-primary block signup">
Sign up and log in
</button>
</form>
</div>
</div>
</main>`;
// Chat base HTML (without user list and messages)
const chatHTML = `<main class="flex flex-column">
<header class="title-bar flex flex-row flex-center">
<div class="title-wrapper block center-element">
<img class="logo" src="http://feathersjs.com/img/feathers-logo-wide.png"
alt="Feathers Logo">
<span class="title">Chat</span>
</div>
</header>
<div class="flex flex-row flex-1 clear">
<aside class="sidebar col col-3 flex flex-column flex-space-between">
<header class="flex flex-row flex-center">
<h4 class="font-300 text-center">
<span class="font-600 online-count">0</span> users
</h4>
</header>
<ul class="flex flex-column flex-1 list-unstyled user-list"></ul>
<footer class="flex flex-row flex-center">
<a href="#" id="logout" class="button button-primary">
Sign Out
</a>
</footer>
</aside>
<div class="flex flex-column col col-9">
<main class="chat flex flex-column flex-1 clear"></main>
<form class="flex flex-row flex-space-between" id="send-message">
<input type="text" name="text" class="flex flex-1">
<button class="button-primary" type="submit">Send</button>
</form>
</div>
</div>
</main>`;
// Add a new user to the list
const addUser = user => {
const userList = document.querySelector('.user-list');
if(userList) {
// Add the user to the list
userList.insertAdjacentHTML('beforeend', `<li>
<a class="block relative" href="#">
<img src="${user.avatar}" alt="" class="avatar">
<span class="absolute username">${user.email}</span>
</a>
</li>`);
// Update the number of users
const userCount = document.querySelectorAll('.user-list li').length;
document.querySelector('.online-count').innerHTML = userCount;
}
};
// Renders a new message and finds the user that belongs to the message
const addMessage = message => {
// Find the user belonging to this message or use the anonymous user if not found
const { user = {} } = message;
const chat = document.querySelector('.chat');
// Escape HTML
const text = message.text
.replace(/&/g, '&')
.replace(/</g, '<').replace(/>/g, '>');
if(chat) {
chat.insertAdjacentHTML( 'beforeend', `<div class="message flex flex-row">
<img src="${user.avatar}" alt="${user.email}" class="avatar">
<div class="message-wrapper">
<p class="message-header">
<span class="username font-600">${user.email}</span>
<span class="sent-date font-300">${moment(message.createdAt).format('MMM Do, hh:mm:ss')}</span>
</p>
<p class="message-content font-300">${text}</p>
</div>
</div>`);
chat.scrollTop = chat.scrollHeight - chat.clientHeight;
}
};
这将添加以下变量和函数:
loginHTML包含登录/注册页面的一些静态HTMLchatHTML包含主要的聊天页面内容(一旦用户登录)addUser(user)是一个将新用户添加到左侧用户列表的函数addMessage(message)是一个向列表中添加新消息的函数.它还将确保在添加消息时始终滚动到消息列表的底部
显示登录/注册或聊天页面¶
接下来,我们将添加两个功能来显示登录和聊天页面,其中我们还将添加25个最新聊天消息和注册用户的列表.
// Show the login page
const showLogin = (error = {}) => {
if(document.querySelectorAll('.login').length) {
document.querySelector('.heading').insertAdjacentHTML('beforeend', `<p>There was an error: ${error.message}</p>`);
} else {
document.getElementById('app').innerHTML = loginHTML;
}
};
// Shows the chat page
const showChat = async () => {
document.getElementById('app').innerHTML = chatHTML;
// Find the latest 25 messages. They will come with the newest first
// which is why we have to reverse before adding them
const messages = await client.service('messages').find({
query: {
$sort: { createdAt: -1 },
$limit: 25
}
});
// We want to show the newest message last
messages.data.reverse().forEach(addMessage);
// Find all users
const users = await client.service('users').find();
users.data.forEach(addUser);
};
showLogin(error)将显示loginHTML的内容,或者,如果登录页面已经显示,则添加错误消息.当您尝试使用无效凭据登录或使用已存在的用户注册时,会发生这种情况.showChat()做了几件事。首先,我们将静态chatHTML添加到页面。然后我们使用Feathers查询语法从消息Feathers服务获取最新的25条消息(这与我们的聊天API的/messages端点相同)。由于列表将首先返回最新消息,因此我们需要反转数据。然后我们通过调用我们的addMessage函数来添加每条消息,这样它看起来就像聊天应用程序一样 - 旧的消息在向上滚动时变老。之后,我们通过调用addUser获取所有注册用户的列表,以在侧栏中显示它们。
登录和注册¶
好的.现在我们可以显示登录页面(包括出错时的错误消息),如果我们登录,则调用上面定义的 showChat.我们已经构建了UI,现在我们必须添加功能以实际允许人们注册,登录和注销.
// Retrieve email/password object from the login/signup page
const getCredentials = () => {
const user = {
email: document.querySelector('[name="email"]').value,
password: document.querySelector('[name="password"]').value
};
return user;
};
// Log in either using the given email/password or the token from storage
const login = async credentials => {
try {
if(!credentials) {
// Try to authenticate using the JWT from localStorage
await client.authenticate();
} else {
// If we get login information, add the strategy we want to use for login
const payload = Object.assign({ strategy: 'local' }, credentials);
await client.authenticate(payload);
}
// If successful, show the chat page
showChat();
} catch(error) {
// If we got an error, show the login page
showLogin(error);
}
};
document.addEventListener('click', async ev => {
switch(ev.target.id) {
case 'signup': {
// For signup, create a new user and then log them in
const credentials = getCredentials();
// First create the user
await client.service('users').create(credentials);
// If successful log them in
await login(credentials);
break;
}
case 'login': {
const user = getCredentials();
await login(user);
break;
}
case 'logout': {
await client.logout();
document.getElementById('app').innerHTML = loginHTML;
break;
}
}
});
getCredentials()从登录/注册页面获取用户名(电子邮件)和密码字段的值,以便直接与Feathers身份验证一起使用.login(credentials)将使用本地身份验证策略(例如用户名和密码)对getCredentials针对Feathers API返回的凭据进行身份验证,或者,如果没有给出凭据,请尝试使用存储在localStorage中的JWT。 这将首先尝试从localStorage获取JWT,一旦您成功登录,它将自动放入,这样我们就不必每次访问聊天时都登录。 只有当它不起作用时,它才会显示登录页面。最后,如果登录成功,它将显示聊天页面。我们还为三个按钮添加了单击事件侦听器.
#login将获得凭据,然后使用这些凭据登录.单击#signup将注册并同时登录.它将首先在我们的API上创建一个新用户,然后使用相同的用户信息登录. 最后,#logout将忘记JWT,然后再次显示登录页面.
实时和发送消息¶
在最后一步中,我们将添加发送新消息的功能,并使用户和消息列表实时更新.
document.addEventListener('submit', async ev => {
if(ev.target.id === 'send-message') {
// This is the message text input field
const input = document.querySelector('[name="text"]');
ev.preventDefault();
// Create a new message and then clear the input field
await client.service('messages').create({
text: input.value
});
input.value = '';
}
});
// Listen to created events and add the new message in real-time
client.service('messages').on('created', addMessage);
// We will also see when new users get created in real-time
client.service('users').on('created', addUser);
login();
submit按钮事件监听器从输入字段获取消息文本,在消息服务上创建新消息,然后清除该字段.接下来,我们添加了两个
created事件监听器.一个用于messages调用addMessage函数将新消息添加到列表中,一个用于users,它通过addUser将用户添加到列表中.这就是Feathers实时和我们需要做的一切,以便让所有内容自动更新.为了启动我们的应用程序,我们调用
login(),如上所述,它将立即显示聊天应用程序(如果我们之前已登录且令牌未过期)或登录页面.