在现代的 Web 应用程序中,实时性已经逐渐成为一个基本要素。而其中最简单高效的解决方案之一,就是利用 Socket.IO。
不过在实际开发过程中,很多人会遇到一个麻烦的问题,即:Socket.IO 的 emit
和 on
方法,第一个参数通常是事件名称,而第二个参数则是待发送的数据。然而这种方法显然很不灵活,因为你无法直接控制是否发送第二个参数。而有时候,我们需要在事件名称的基础上再添加一些其他辅助信息,才能满足业务需求。
这篇文章就要告诉你,如何利用 JavaScript 的“偏函数”概念,解决这个问题,让你的 Socket.IO 应用变得更加灵活和健壮!
偏函数概述
所谓“偏函数”,顾名思义就是“部分应用一个函数”。也就是说,给定一个函数和部分参数,可以产生另外一个函数。
我们来看个例子。假设你有一个加法函数:
function add(x, y) { return x + y; }
现在你需要一个函数,它总是加上 5。你可以这样做:
function addFive(y) { return add(5, y); }
可以看出,addFive
变成了 add
的“偏函数”。
如果换一个场景,假设你需要做一个高阶函数,把 add
和 6
封装到一个函数里:
function otherFunc(func, y) { return func(6, y); }
这时候你就可以直接调用 otherFunc(add, 4)
,结果是 10。
了解了偏函数的概念,我们再来看看如何运用到 Socket.IO 上。
针对 Socket.IO 的问题
首先我们看一下 Socket.IO 常用的 emit
和 on
方法,以及它们暴露的接口:
socket.emit('message', data); socket.on('message', (data) => { ... });
这里的 'message'
参数即为事件名称,data
则为需要发送的数据。然而,这样的设计并不太好,因为你无法将事件名称与其他参数“拼合”起来,只能在名称前面加一些固定的字符,比如 client-message
或 server-message
。
如果你需要一些更复杂的业务逻辑,例如在前端 Angular 应用中使用 WebSocket,实现一个 “通过用户名发送信息” 的功能。会出现这样一种调用:
socket.emit(`user-message-${username}`, message);
很遗憾,这么做显然是有问题的。考虑到 Socket.IO 允许一个事件可以有多个监听器,另一个使用 socket.on
监听该事件的地方也可能是这样调用的:
socket.on(`user-message-${username}`, (message) => { ... });
两边之间的信息不匹配,很有可能会造成难以预料的错误。
为什么会出现这个问题?是因为 Socket.IO 的固有设计限制吗?其实不是的。如果我们能够将“事件名称”与“其他参数”分离,把它作为一个对象传进去,就很容易解决这个问题了。
事实上,JavaScript 已经提供了相应的方法,bind
。我们可以这样写:
socket.emit.bind(socket, {eventName: `user-message-${username}`, message: message})();
在服务端的实现也类似:
socket.on('user-message', (data) => { const username = data.username; const message = data.message; // ... do something ... });
但是这样写并不优雅,需要大量重复代码,而且不容易维护。这边提供了一种更好的解决方法,即利用 JavaScript 的“偏函数”概念。
解决方法:深度应用偏函数
有了偏函数,我们可以有自动为 emit
和 on
添加事件名称功能的函数,代码如下:
-- -------------------- ---- ------- ----- --------- - -------- ---------- -- - ------ ----- -- - ---------------------- ----- -- -- ----- ------- - -------- ---------- ----- -- - ------ -- -- - -------------------- ------ -- --
如上所示,我们通过 curryEmit
和 curryOn
获得了一个“部分应用”的函数。即,在调用 curryEmit(socket, 'user-message')
时,会返回一个新的函数,这个新函数在调用时再传入 msg
参数。当我们需要发送 user-message
类型事件时,就可以直接调用此函数,像这样:
const sendMessage = curryEmit(socket, 'user-message'); sendMessage(msg);
这在后续的代码编写中,非常方便。
同样,我们也可以使用 curryOn
实现将特定类型的事件附加到 socket.on
上的需求:
const onMessageReceived = curryOn(socket, 'user-message', (msg) => { // do something with the msg }); onMessageReceived();
这样的好处是在之后的代码维护过程中,改变事件名称只需要改动一处,即 curryEmit
和 curryOn
调用的地方即可,其他代码不受影响。
当然,我们也可以使用一些高级技巧,将允许多个事件名称以及多个参数的情况下,使用“偏函数”的方式实现。如果你有了解拓展函数,可以参考如下代码实现:
-- -------------------- ---- ------- -------- ------------------- --------- - -------- -------- - --- ----- - --------- ------ -------- ----------- - -- ------------ -- ------ - ------ ---------- - ---- - ------ -------- --------------- - --- ------- - ---------------------- ------ -------------- - - - - --- ---- --------- -- --------- - --- - - --------------------------- -------------------- ----- - - ------------------- - --------------- ---------- -------- -- - -- -- --------- ---- -------- --- ------- -- ----------------- ---------- -------- ---- -- - -- -- --------- ---- --------- -------- --- --- - ---
实际案例—Angular + Socket.IO
以上理论处理了“事件名称”和“消息体”的问题之后,我们再来看一个实际的案例,也就是 Angular
客户端通过 Socket.IO 与服务器交互,发送和接受消息。
服务端代码非常基础,就不展开介绍了,读者可以直接到 GitHub 地址 查看。
下面是前端代码实现,首先我们需要引入 Socket.IO:
import * as io from 'socket.io-client';
接着连接 Socket.IO 服务器:
-- -------------------- ---- ------- ----------- ---- - -- ------- --- --- ------ ----------- - --------------------- -- --------- -- -------- -------- ------------------------------ ----- -- - --------------------- -------- --------- --- -
在需要发送消息给服务端的地方调用:
sendMessage(msg: string): void { const send = curryEmit(this.socket, 'user-message'); send(this.username, msg)(); }
代码中,curryEmit
函数接受一个 Socket.IO 的 socket
对象和一个事件名称,返回一个新的函数以便之后随时发送此事件。使用 curryEmit
函数我们生成了 send
函数,随时调用:
const send = curryEmit(this.socket, 'user-message'); send(this.username, msg)();
这时候就可以发送带参数的事件名称 user-message:{username}
给后端。
完整代码实现可以在 GitHub 上找到。
总结
在本文中,我们讲述了 Socket.IO 中常见的问题,以及这个问题带来的不便。随后我们介绍了偏函数的概念,并通过一些简单的示例说明了它的基本用法。最后,我们通过一个实际的案例,向读者展示了怎么样基于 Socket.IO 以及 Angular 实现一个更加灵活、健壮的“实时消息传递”应用。
“偏函数”之于 JavaScript,就像汽车之于路面,一样重要。它不仅提供了一种高效的设计思路,还能让我们的代码变得更加灵活、可读性和可维护性大大增强。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/653dac487d4982a6eb768573