在现代的 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
添加事件名称功能的函数,代码如下:
// javascriptcn.com 代码示例 const curryEmit = (socket, eventName) => { return (msg) => { socket.emit(eventName, msg); }; }; const curryOn = (socket, eventName, func) => { return () => { socket.on(eventName, func); }; };
如上所示,我们通过 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
调用的地方即可,其他代码不受影响。
当然,我们也可以使用一些高级技巧,将允许多个事件名称以及多个参数的情况下,使用“偏函数”的方式实现。如果你有了解拓展函数,可以参考如下代码实现:
// javascriptcn.com 代码示例 function handleEvent(socket, mappings) { function curry(f) { var arity = f.length; return function f1(...args) { if (args.length >= arity) { return f(...args) } else { return function f2(...moreArgs) { var newArgs = args.concat(moreArgs); return f1(...newArgs) } } } } for (var eventName in mappings) { var f = curry(mappings[eventName]); socket.on(eventName, f()); } } handleEvent(socket, { 'user-message': (username, message) => { // do something with username and message }, 'user-broadcast': (username, message, uid) => { // do something with username, message, and uid } });
实际案例—Angular + Socket.IO
以上理论处理了“事件名称”和“消息体”的问题之后,我们再来看一个实际的案例,也就是 Angular
客户端通过 Socket.IO 与服务器交互,发送和接受消息。
服务端代码非常基础,就不展开介绍了,读者可以直接到 GitHub 地址 查看。
下面是前端代码实现,首先我们需要引入 Socket.IO:
import * as io from 'socket.io-client';
接着连接 Socket.IO 服务器:
// javascriptcn.com 代码示例 ngOnInit(): void { // connect the web socket this.socket = io.connect(this.url); // subscribe to incoming messages this.socket.on('user-message', (msg) => { console.log(`Incoming message: ${msg}`); }); }
在需要发送消息给服务端的地方调用:
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