本文内容概述
在架构设想和功用开辟中,代码的可保护性和可扩大性一直是工程师不懈的寻求。本文将以我工作中开辟的 IM 通讯效劳 SDK 作为示例,和人人一同议论下前端基本效劳类营业的代码中对可保护性和可扩大方面的探究。
本文不触及详细的代码和手艺相干细节,假如想相识 IM 长衔接相干的手艺细节,可以浏览我之前的文章:
- WebSocket系列之基本知识入门篇
- WebSocket系列之JavaScript数字数据怎样转换为二进制数据
- WebSocket系列之JavaScript字符串怎样与二进制数据间举行相互转换
- WebSocket系列之二进制数据设想与传输
- WebSocket系列之怎样竖立和保护牢靠的衔接
背景引见
大象 SDK 是美团生态中担任 IM 通讯效劳的基本效劳。作为 IM 通讯效劳的 Web 端载体,我们对差别的营业线供应差别的功用来满足特定的需求,同时须要支撑 PC、Web、挪动端H5、微信小顺序等各个平台。
差别的营业方需乞降差别的平台对 Web SDK 的功用和模块请求都不雷同,因而在悉数 Web SDK 中有很多部份存在须要适配多场景的状况。
处置惩罚这类罕见的场景,我们平常有以下几个思绪:
- 针对差别的场景零丁开辟差别的基本效劳代码。这类操纵灵活性最强,然则本钱也是最高的,假如我们须要面临 M 个营业需乞降 N 个平台,我们就须要有 M * N 套代码。这个关于手艺人员来讲,基本上是一个不可以吸收的状况。
- 将一切的代码悉数聚合到一个营业模块中,经由过程内部的 IF ELSE 推断逻辑来自动挑选须要实行的代码逻辑。这类计划不会涌现雷同代码反复编写的状况,同时也统筹了灵活性,看上去是一个不错的挑选。然则我们细致一想就会发明,一切的代码都堆积到一同,在后期会遇到大批的推断逻辑,在可保护性上来看是一个庞大的灾害。同时,我们一切的代码都放到一同,这会致使我们的包体积越来越大,而其他营业在运用相干功用时,也会引入大批无用代码,糟蹋流量。
那末,我们在既须要统筹可保护性,有须要保证开辟效力的状况下,我们应当怎样去举行相干营业的架构设想呢?
中心准绳
在我的设想理念中,有这么几个准绳须要恪守:
- 针对接口范例编程,而不针对特定代码编程(即设想形式中的战略形式)。我们在举行架构设想时,优先推断各个功用和模块中流转的数据花样和交互的数据接口范例,如许我们可以保证在举行特定代码编写的时刻,只针对详细花样举行数据处置惩罚,而不会设想到数据内容自身。
- 各模块权责清楚,宽进严出。每一个模块都是单一全责,暴露特定数据花样的 API,处置惩罚商定好数据花样的内容。
- 供应计划供用户挑选,而不帮用户做决议计划。我们不去推断用户地点环境、挑选功用,而是供应多个挑选来让用户主动去做这个决议计划。
详细实践
上面的准绳可以比较笼统,我们来看几个详细的场景,人人就可以对这个有一个特定的看法。
衔接模块设想(长衔接部份)
衔接模块包括长衔接和短衔接部份,我们在这里就用长衔接部份来举行举例,短衔接部份我们可以依据相似的准绳举行设想即可。在设想长衔接部份时,我们须要斟酌的是:衔接战略与切换战略。总的来讲就是我们须要在什么时刻运用哪种长衔接。
起首,我们以浏览器端为例,我们可以挑选的长衔接有:WebSocket 和长轮询。这个时刻,我们可以起首以 WebSocket 优先,而长轮询作为备选计划来组成我们的长衔接部份。因而,我们可以会在代码中直接用代码来完成这个计划。相干伪代码以下:
import WebSocket from 'websocket';
import LongPolling from 'longPolling';
class Connection {
private _websocket;
private _longPolling;
constructor() {
this._websocket = new WebSocket();
this._longPollong = new LongPolling();
}
connect() {
this.websocket.connect();
// 只表达相干寄义用于申明
if (websocket.isConnected) {
this.websocket.send(message);
} else {
this.longPolling.connect();
}
}
}
在一般状况下来看,我们发明这个代码没有什么题目。然则,假如我们的需求发生了某些变化呢?比方我们如今须要在某些特定的场景下,只开启长轮询,而不开启 WebSocket 呢(比方在 IE 浏览器内里)?之前的做法是在组织器的时刻,通报一个参数进去,用来掌握我们是否是开启 WebSocket。因而,我们的代码会变成以下的模样。
class Connection {
private _useWebSocket;
private _websocket;
private _longPolling;
constructor({useWebSocket}) {
this._useWebSocket = useWebSocket;
this._websocket = new WebSocket();
this._longPollong = new LongPolling();
}
connect() {
if (this._useWebSocket) {
this.websocket.connect();
// 只表达相干寄义用于申明
if (websocket.isConnected) {
this.websocket.send(message);
} else {
this.longPolling.connect();
}
} else {
this._longPolling.connect();
}
}
}
如今,我们经由过程增添一个推断参数,对connect
函数举行了简朴的革新,满足了在特定场景下的指运用长轮询的需求。
很不幸,我们的题目又来了,我们在针对挪动端 H5 的场景下,我们须要一个只需 WebSocket 衔接,而不须要长轮询。那末,依据我们之前的体式格局,我们可以又须要在增添一个新的参数useLongPolling
。这个代码示例我就不增添了,人人应当可以设想出来。
在线上运转了一段时间后,新的需求又来了,我们须要在微信小顺序内里支撑 IM 的长衔接。那末,依据我们之前的思绪,我们须要在私有属性和connect要领中增添一堆推断逻辑。详细示例以下:
import WebSocket from 'websocket';
import LongPolling from 'longPolling';
import WXWebSocket from 'wxwebsocket';
class Connection {
private _websocket;
private _longPolling;
private _wxwebsocket;
constructor() {
// 假如在微信小顺序容器中
if (isInWX()) {
this._wxwebsocket = new WXWebSocket();
} else {
this._websocket = new WebSocket();
this._longPollong = new LongPolling();
}
}
connect() {
if (isInWx()) {
this._wxwebsocket.connect();
} else {
this.websocket.connect();
// 只表达相干寄义用于申明
if (websocket.isConnected) {
this.websocket.send(message);
} else {
this.longPolling.connect();
}
}
}
}
从这个例子,人人应当可以发明相干的题目了,假如我们再支撑百度小顺序、头条小顺序等更多的平台,我们就会在我们的推断逻辑内里加更多的逻辑,如许会让我们的可保护性有显著的下落。
如今有一些类库可以支撑多平台的接口一致(人人去GitHub上面找一下就可以发明),那末为何我没有用相干的产物呢?这是由于 SDK 作为一个基本效劳,对包大小比较敏感,同时用到的须要兼容 API 并不多,所以我们本身做相干的兼容比较适宜。
那末,我们应当怎样设想这个计划,从而处理这个题目呢。让我们回忆下我们的设想理念。
- 针对接口范例编程,而不针对特定代码编程。
- 各模块权责清楚,宽进严出。
- 供应计划供用户挑选,而不帮用户做决议计划。
经由过程这些设想理念,我们来看下详细的做法。
三个设想理念我们须要组合运用。起首是针对构造范例编程
。我们来看下详细的用法。
起首我们定义一个长衔接的接口以下:
export default interface SocketInterface {
connect(url: string): void;
disconnect(): void;
send(data: any[]): void;
onOpen(func): void;
onMessage(func): void;
onClose(func): void;
onError(func): void;
isConnected(): boolean;
}
有了这个长衔接的接口范例后,我们可以让 WebSocket 和长轮询两个模块都完成这个接口。因而,他们就有了一致的 API。有了一致的 API 以后,我们就可以将衔接战略中的操纵“泛化”,从操纵详细的衔接体式格局转换为操纵被选中的衔接体式格局。
其次,依据我们的各模块全责清楚
的准绳,我们的衔接模块应当只掌握我们的衔接战略,并不须要体贴她运用的是 WebSocket 照样长轮询,照样说微信小顺序的 API。
原理很简朴,然则详细我们应当怎样来实践呢?我们来看下下面这个示例:
class Connection {
private _sockets = [];
private _currentSocket;
constructor({Sockets}) {
for (let Socket of Sockets) {
let socket = new Socket();
socket.onOpen(() => {
for (let socket of this._sockets) {
if (socket.isconnected()) {
this._currentSocket = socket;
} else {
socket.disconnect();
}
}
});
this._sockets.push(socket);
}
}
connect() {
for (let socekt of this._sockets) {
socket.connect();
}
}
}
经由过程上面这个示例人人可以看到,我们泛化了每一个衔接体式格局的差别,转为用一致的接口范例来束缚相干的模块。如许带来的优点是,我们假如须要兼容 WebSocket 和长轮询时,我们可以把这两个的组织函数通报进来;假如我们须要支撑微信小顺序,我们也只须要将微信小顺序的 API 封装一次,我们就可以获得我们须要的模块,如许可以保证我们的衔接模块只担任衔接,而不去体贴它不应体贴的兼容性题目。
那末由用户就会问了,那我们是在哪一层来推断传入的参数究竟是哪些呢?是在这个模块的上一层吗?这个题目很简朴,还记得我们的第三个划定规矩是什么吗?那就是供应计划供用户挑选,而不帮用户做决议计划
。因而,我们在构建长衔接部份的时刻,我们就在 Webpack 内里定义一些常量用于推断我们当前构建时,我们临盆的的包是用于什么场景。详细示例以下:
import Connection from 'connection';
import WebSocket from 'websocket';
import LongPolling from 'longPolling';
import WXWebSocket from 'wxwebsocket';
class WebSDK {
private _connection;
constructor() {
if (CONTAINER_NAME === 'WX') {
this._connection = new Connection({Sockets: [WXWebSocket]});
}
if (CONTAINER_NAME === 'PC') {
this._connection = new Connection({Sockets: [WebSocket, LongPolling]});
}
if (CONTAINER_NAME === 'H5') {
this._connection = new Connection({Sockets: [WebSocket]});
}
}
}
我们经由过程在 Webpack 中定义 CONTAINER_NAME
这个常量,我们可以在打包时构建差别的 Web SDK 包。在保证对外暴露 API 完全一致的状况下,营业方可以在差别的容器内,采纳对应的打包体式格局,引入差别的 Web SDK 的包,同时不须要修改任何代码。
可以有人会问了,这个体式格局看上去实在和之前的体式格局没有什么差别,只是把这个 IF ELSE 的逻辑挪动到了表面。然则,我可以通知人人,这里有两个显著的上风:
- 我们可以笼统零丁的模块去治理和保护这个自力的推断逻辑,它不会和我们的长衔接部份代码举行耦合。
- 我们可以在打包过程当中运用 tree-shaking,如许我们可以让我们的 Web SDK 构建的包中,不会涌现我们不须要的模块的代码。
音讯流处置惩罚
上面的长衔接部份,我们看到了三个准绳的运用。接下来我们来看下我们怎样运用这个准绳举行数据流的处置惩罚。
在 IM 场景中,我们会遇到很多范例的音讯。我们以微信民众号为例,我们会遇到单聊(单人-单人)、群聊(单人-群组)、民众号(单人-民众号)等谈天场景。假如我们须要去盘算音讯的未读数,同时用音讯来更新左边的会话列表,我们就须要三套险些完全一样的逻辑。
那末,我们有无什么更优的要领呢。很显著,我们可以依据上面引见的准绳,定义一个音讯接口。
interface MessageInterface {
public fromId: string;
public toId: string;
public fromName: string;
public messageType: number;
public messageBody;
public uuid: string;
public serverId: string;
public extension: string;
}
经由过程之前的例子,人人应当可以明白,我们如今的一切营业逻辑,比方更新未读数、更新会话列表的预览音讯时,我们就只须要针对悉数音讯接口内里的数据举行处置惩罚。如许的话,我们的处置惩罚流程就会变成一个流水线功课,我们只担任处置惩罚特定逻辑的数据,而不论详细的数据内容是什么模样的。
因而,假如我们新增一类会话范例,比方客服音讯,我们也可以依据上面这个接口去完成客服音讯类,复用本来的逻辑,而不须要从新完成一套完全的代码。
我们的在一开始就须要对数据举行转换,如许才可以保证我们在内部流转时不会犹疑数据花样差别致使代码保护性变差。须要注重的是,依据我们的各模块权责清楚,宽进严出
准绳,我们在像其他模块输出时,我们也须要保证我们只输出这一种花样的数据,而吸收的数据,我们应当尽最大的勤奋去顺应种种场景。
可以有人会问,我们内部本身划定运用谁人体系就可以,掌握了严出了,我们天然就不用处置惩罚宽进了。然则,你写的代码和模块很有可以会和其他人一同保护,这个时刻,你只能从范例上面来束缚他,而不能掌握他。因而,我们在吸收其他非同一开辟模块的数据时,我们可以会遇到一些异常状况。这个时刻假如我们对宽进有做处置惩罚,也可以保证该模块可以一般运转。
有了之前的履历,人人对这个示例应当很好明白,我就不多做引见了。
总结
这一篇文章没有引见什么代码层面的东西,而是和人人一同交流了一下,我在一样平常工作中遇到的一些可以的题目,以及关于设想形式相干的运用场景。
假如我们须要作为一个基本效劳供应方,须要让本身的代码有扩大性和可保护性,我们须要:
- 面临接口范例编程。
- 单一全责、宽进严出。
- 不帮用户做决议计划。
固然,在用户产物层面,可以上面的设想有部份雷同的处所,也有部份差别的处所,有时间的话,我会在后面再和人人举行分享。
人人假如有兴致的话可以在批评区宣布下本身看法,也可以在批评内里留言举行议论,也迎接人人宣布本身的看法。
作者引见与转载声明
黄珏,2015年毕业于华中科技大学,现在任职于美团基本研发平台大象营业部,自力担任大象 Web SDK 的开辟与保护。
本文未经作者许可,制止转载。