与 Sapper (Svelte) 和 Socket.IO 交流
语境
设置
搭建服务器
构建客户
结论
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
语境
在开发上一个项目——一个间隔重复学习应用(点击此处体验演示)——的过程中,我偶然发现了Svelte,这是一个组件框架,它会在构建时运行,并将你的代码编译成优化的原生 JavaScript。我稍微体验了一下它的教程,感觉非常有趣。我还很快发现了Sapper,一个用于构建 Svelte 应用的应用程序框架。因此,我决定下一个项目要做一个小项目,以便更好地学习 Svelte 和 Sapper。
除了 Svelte 之外,我还想测试一下Socket.IO,这是一个支持客户端和服务器之间实时、双向、基于事件的通信的库。因此,我认为将两者结合起来的好方法是构建一个简单的聊天应用程序。我也觉得这样的项目不仅能让我更好地学习这些技术,还能作为一个示例来教授其他人(例如在研讨会、聚会、课程等场合)。因此,我将在下面简要介绍一下我的应用构建过程。如果您比较心急,可以直接访问应用程序的代码仓库或体验一下演示。
设置
启动和运行过程相当轻松,正如 Sapper 文档中所述那样简单。通过以下命令序列,我创建了一个新的 Sapper 项目,可以开始使用了:
npx degit "sveltejs/sapper-template#rollup" chat-app
cd chat-app
npm install
下一步是安装 Socket.IO 的基本组件。这很容易npm install --save socket.io。然而,我遇到一个问题:我无法让我的index.svelte程序正确导入io服务器默认提供的独立客户端构建中的对象/socket.io/socket.io.js。这可能是由于所有资源加载方式存在竞争条件造成的。因此,我选择导入另一个包node_modules,但这导致了找不到模块的错误bufferutil。
最后,我意识到如果我想采用这种方法,还需要显式安装客户端包。npm install --save socket.io-client虽然入门文档中没有提到这一点,但在扩展文档中有说明(哎呀!)。另一种方法是尝试从 CDN 加载,但我没有尝试。安装客户端包后,我就可以将其导入到我的项目中index.svelte并开始使用该io对象了!
搭建服务器
Socket.IO 的文档很棒,但它们主要针对 Express。我比较熟悉 Express,也很喜欢它,但我之前从未尝试过 Polka,而它默认自带 Sapper,所以我觉得这是个绝佳的尝试机会。幸运的是,Polka 的开发者(或者说作者本人?)也写了一个示例(虽然是个聊天应用!),专门展示了如何使用 Socket.IO;因此,我可以大量参考这个示例。
点击此处查看。这基本上就是你入门所需的一切。因此,我不会在这里赘述,而是会在下文解释客户端的不同部分时直接引用相关的服务器代码。
构建客户
客户端仅包含一个 index.svelte 页面和一个Heading.svelte组件。我决定将标题提取出来,作为一个独立的组件,以便将来可能在其他页面甚至其他项目中使用它。使用 Svelte 实现类似的功能非常容易;例如,我的Heading.svelte代码如下所示:
<style>
[...CSS...]
</style>
<script>
export let text;
</script>
<div id="heading">{text}</div>
然后,在[此处应插入参考文献]中index.svelte,它的用法如下:
<script>
import Heading from '../components/Heading.svelte';
[...]
</script>
<body>
<div class="main">
<Heading text={'Chat App'} />
[...]
</body>
还应该注意的是,您可以通过简单地为其赋值而不是将其留空来text设置默认值。Heading.svelte
聊天功能主要由各种 Socket.IOsocket.on和socket.emit(或socket.broadcast.emit)调用处理,这些调用会响应用户操作而触发。例如,要实现消息发送,我们首先需要向应用程序的 send 方法添加一个事件监听器button,这在 Svelte 中可以非常轻松地实现:
<form action="">
<input id="m" autocomplete="off" {placeholder} bind:value={message} />
<button on:click|preventDefault={handleSubmit}>Send</button>
</form>
还要注意的是,我们将消息字段的值绑定input到了变量上message。这将创建一个双向绑定,并确保输入到字段中的任何内容都会自动绑定到我们在script代码块中定义的消息变量(通过类似通常的方式let message = '')。
当用户点击发送按钮button时,我们代码块handleSubmit中定义的函数script将被调用。该函数首先将消息添加到一个messages数组中,该数组作为存储给定会话所有消息的数据结构,然后触发一个'message'事件:
messages = messages.concat(message);
socket.emit('message', message);
然后,在服务器端,我们有一个监听器,它会监听此类事件,并在收到此类事件时做出相应的反应。在我们的例子中,这意味着将消息广播给所有用户(除了发送消息的用户):
socket.on('message', function(message) {
socket.broadcast.emit('message', message);
})
最后,回到我们的script代码块中index.svelte,我们还有另一个socket.on监听器等待接收此类'message'事件,以便将它们添加到messages数组中并在聊天窗口中显示它们:
socket.on('message', function(message) {
messages = messages.concat(message);
});
此时,你可能在想:“好的,这不错,但是我们如何向用户显示这些消息呢?”。Svelte 让这一切变得非常简单。借助 Svelte 的#each块功能,我们可以简单地创建一个列表元素ul,然后使用 ` lifor each messagein`元素填充它messages:
<div id="chatWindow">
<ul id="messages">
{#each messages as message}
<li>{message}</li>
{/each}
</ul>
</div>
太棒了!现在每次有新消息message添加进来messages,聊天窗口都会立即更新并显示!不过,如果新消息能淡入而不是突然出现就更好了。是不是该用 CSS 或 JavaScript 来实现一下了?不,Svelte 来帮忙:
<li transition:fade>{message}</li>
没错,只需这些就能为每条消息添加漂亮的淡入过渡效果。而且,到目前为止,你只需要这些就能拥有一个可用的聊天客户端了!我本来也可以就此止步,但我还想添加一些小功能,其中之一就是显示当前聊天用户总数的消息。
这最终有点棘手,因为我不确定应该在哪里存储和更新这个值。Svelte 本身实现了一个应用范围的 store;然而,事实证明这不是正确的解决方案,因为我需要一个在每次有新用户连接时都能更新的值。因此,最终的解决方案是在服务器端存储该值,然后在每次有新用户连接时'connection'更新该值并将其发送回客户端,客户端随后相应地更新其本地副本。首先是服务器端代码:
[...]
let numUsers = 0;
io(server).on('connection', function(socket) {
++numUsers;
let message = 'Server: A new user has joined the chat';
socket.emit('user joined', { message, numUsers });
socket.broadcast.emit('user joined', { message, numUsers });
[...]
请注意,我们必须同时调用 `response socket.emit()`(向事件发送者发送消息)和socket.broadcast.emit`response()`(向除事件发送者之外的所有用户发送消息)。这是因为我们希望加入的用户和所有其他用户都能收到此事件,并且本地存储的用户数量要正确。
然后,回到客户端的script代码块中,我们设置了本地变量来存储已连接用户的数量,以及事件监听器'user joined':
let numUsersConnected = 0;
[...]
socket.on("user joined", function({message, numUsers}) {
messages = messages.concat(message);
numUsersConnected = numUsers;
updateScroll();
});
[...]
最后,html我们在代码块中按如下方式插入数据:
<p>There {numUsersConnected == 1 ? 'is' : 'are'} {numUsersConnected} {numUsersConnected == 1 ? 'user' : 'users'} currently chatting!</p>
您可能已经注意到,在上面的监听器中,我们还调用了一个名为 `autorolls` 的函数updateScroll。这个函数的作用是在每次收到新消息时自动滚动聊天窗口:
function updateScroll() {
const chatWindow = document.getElementById('chatWindow');
chatWindow.scrollTop = chatWindow.scrollHeight;
}
不过我遇到的问题是,每次调用这个函数时,聊天窗口都会自动滚动,但不会滚动到最底部div。相反,它只会滚动到最后一条消息的上方一点,所以用户总是需要再滚动一点才能看到最新消息。这个奇怪的bug到底是什么原因造成的呢?
我最终意识到这是由于一个小小的竞态条件造成的。本质上,`updateScroll` 函数在新消息实际添加到 DOM之前scrollTop就被调用了,导致滚动条始终位于最新消息的上方!为了解决这个问题,我简单地将赋值scrollTop操作放入一个setTimeout回调函数中,这样它就会由 Web API 处理,并强制等待job queue所有其他代码执行完毕:
function updateScroll() {
const chatWindow = document.getElementById('chatWindow');
setTimeout(() => {
chatWindow.scrollTop = chatWindow.scrollHeight;
}, 0);
}
于是,卷轴开始完美运行了!
在使用 Svelte 时,你可能会遇到的最后一个陷阱是,如果你尝试向window对象添加事件监听器,执行时会报错。这也是由于竞态条件导致的;不过,Svelte 对此有文档说明,并提供了一种通过特殊元素ReferenceError: window is not defined与对象交互的简单方法。在这个应用中,该元素用于在窗口卸载时调用一个函数:windowsvelte:window
<svelte:window on:unload={emitUserDisconnect}
这样一来,你不仅可以访问该window对象,还可以轻松设置事件监听器!
结论
这是一个非常快速且有趣的入门项目,可以帮助你熟悉 Svelte/Sapper 和 Socket.IO。我认为它非常适合在下午的知识分享会(例如线下聚会)中与其他人一起完成。虽然其中确实存在一些需要注意的地方,但一旦你真正理解了问题,通常会发现这些问题其实已经有文档记录,并且存在简洁的解决方案。我建议你尝试一下这些技术;它们都能让开发变得相当轻松,而且更重要的是——非常有趣!
文章来源:https://dev.to/tmns/chatting-with-sapper-svelte-and-socket-io-4c6a