# IM客户端设计常见问题

不知不觉做了六年的程序员，主做前端也已经三年时间，在这六年的时间里做了不少项目。虽然对基础层面、算法、JS运行原理去深究一些底层的运行机制还是一知半解。但这三年的项目中遇到的问题及做的优化点也有很多，深入JS底层的问题、深入逻辑底层的算法也遇到不少，但总是缺乏系统的梳理。在这里，率先对两年前我所做一个IM项目所遇到的问题作出整，方便知识后序查阅。

项目背景：工程师端是为实现线上客服在线通过IM的形式，发送/接受：文本、图片、标签、语音 及 在线视频直播。 等方式解决各个通路（公众号、APP）接入用户的问题。项目以 Electron 作为壳子，使用 vue 实现窗口界面。整个界面风格非常像Mac版QQ

![image](http://note.youdao.com/yws/res/21043/WEBRESOURCE70409b1016e26593a435ce5428542c77)

## IM消息发送顺序问题

不知道大家有没有观察过桌面版QQ发送框的输入逻辑，我们在输入框中同时输入的内容类型可以是：文字、表情、图片、链接... 其中，文字、表情和链接可以被单独发送出去，图片需要做拆分，比如我输入下列内容

```
今天天气真好[图片] 太阳火热 [图片] [图片]
```

点击发送按钮，该内容会被拆分为:

```
[
    { type: 'text', content: '今天天气真好' },
    { type: 'image', url: [file] }, 
    { type: 'text', content: '太阳火热' },
    { type: 'image', url: [file] }, 
    { type: 'image', url: [file] }, 
]
```

在发送的时候，我们需要调用多次发布接口，发布文字消息、图片、文字、图片、图片。由于是多消息发送，保证消息的发布顺序直观重要。关于这种情况，我们参考了一个 经典的 Promise 红绿灯问题：

解决方案：<https://www.w3ctech.com/topic/916>

## 多消息通路整合

我们当时遇到的业务问题在于复杂的后端服务设计，整个应用由于用户接入的通路不同，导致需要对接多套IM 消息机制，接入 APP端用户IM 使用 长轮询链接，接入 微信端用户IM使用Websocket。两套机制在客户端的业务业务场景基本一致，但是所调用的系统和服务各不相同。所以我们在action 层做了分离，action的实例使用适配器模式进行初始化，action 的适配方法通过参数判断该用户是由 APP通路接入 还是 微信通路。调用不同的 action 实现

```
class ActionBase {
  getUserInfo() {}
}

class ActionAdapter extends ActionBase {
  constructor () {
    super()

    this.apiWX = new WXApi()
    this.apiApp = new AppApi()
  }

  getUserInfo(userId, phone, from) {
    if (from === 'wx')
      return this.apiWX.getUserInfo(userId, phone)

    this.apiApp.getUserInfo(userId)
  }
}

class WXApi extends ActionBase {
  getUserInfo(userId, phone) {
    console.log('Get user info from wx')
  }
}

class AppApi extends ActionBase {
  getUserInfo(userId) {
    console.log('Get user info from APP')
  }
}

const adapter = new ActionAdapter()
adapter.getUserInfo('1', '18668888888', 'wx')
```

MVC 和 MVVM 设计模式的精髓就在于View 层与 Model 层的解耦，通过View组件调用适配器，我们可以实现在最小化View层修改的前提下，适配新的业务体系。

## 输入框的复制与覆盖

## Electron 的跨进程通讯

Electron 有两个进程 Main 及 Renderer，两个进程通过IPC进行实时通讯。在业务触发一个较底层的功能时，我们通过 渲染进行调用 ipcMain，与主进程进行通讯，在 MainProcess主进程 中对对应的系统级 Electron 的API进行调用。

同时，在一些底层事件中，比如 关闭程序、触发快捷键、点击菜单栏等，主进程会率先收到消息，通过 ipcRenderer 调用渲染进程，通知页面对事件作出响应。

## 保证消息必达

* 监听错误并提示感叹号
* 刷新聊天记录
* Websocket链接监听
* 客户端心跳保活
* 服务器轮询监测网络抖动

## Electron 的多窗口管理

维护窗口

## 复杂跨多系统长链路调用，前端如何独善其身

随着项目业务的逐渐增多，我们调用后台的系统服务也变得越来越多，一度达到十几套系统的复杂程度。单一一个请求的前端业务动作，比如登陆，就要调用三套系统的五个接口实现。有些业务接口的饿调用顺序还是强依赖的，比如你必须先拿到 SessionId 才能请求登陆用户基本信息。后端系统相当复杂，并且从系统稳定性的角度看，几乎不能做大的调整。那么我们只有从前端的角度入手解决问题，开始的时候我们使用Promise 的回调链去处理这种长链路请求情况。比如登陆操作：

```javascript
login(username, password) {
    const data = {}
    apiWX.auth({ username, password }).then(res => {
        return apiWX.getUserInfo({ res.userId })
    }).then(res => {
        return apiApp.auth({ username, password })
    }).then(res => {
        return apiApp.getUserInfo({ userId: res.userId })
    }).then(res => {
        return apiWX.getWorkState({ userId: res.userId })
    }).then(res => {
        return apiWX.startSocketListener({ userId: res.userId })
    }).then(res => {
        return apiApp.startSocketListener({ userId: res.userId })
    })
}
```

有一些很明显的一个问题：

1. 一些系统之间可能前后并没有依赖关系，如果单纯是顺序调用的话，加载时间会非常长

```
对不同的调用链路进行拆分，不把没有依赖关系的promise放到一条链路当中
```

1. 单一接口调用出错会导致余下接口都调用不成功，那怕是不那么重要的服务。针对上述问题我们做了两点优化

```javascript
/**
 * finally方法用于指定不管Promise对象最后状态如何，都会执行的操作。
 * 它与done方法的最大区别，它接受一个普通的回调函数作为参数，该函数不管怎样都必须执行。
 * @param callback
 * @returns {Promise.<Promise.<*>>}
 */
Promise.prototype.finally = function (callback) {
    const P = this.constructor
    return this.then(
        value => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => {
            throw reason
        })
    )
}
```

1. Promise对象的回调链，不管以then方法或catch方法结尾，要是最后一个方法抛出错误，都有可能无法捕捉到（因为Promise内部的错误不会冒泡到全局）

```javascript
Promise.prototype.done = function (onFulfilled, onRejected) {
    this.then(onFulfilled, onRejected)
        .catch((reason) => {
            setTimeout(() => {
                throw reason
            }, 0)
        })
}
```

## 对抗复杂链路调用的堡垒：中台构建

我们可以看到，大型企业内部管理系统的复杂程度令人咋舌，而我们期待服务端去做出一些改变，基于其系统的历史情况，尤其很多系统属于跨部门协同，是不现实的。而每一个系统基于其自身的定位，只是在几个业务线之间，而我们的应用有需要跨越多个系统进行调用，取数据、实现业务能力。所以最好的方案是我们在中间搭建一层中台，一方面针对后端，通过内网对单一业务动作集成不同的系统，这样对于一些跨系统的请求更快。前端只需要跟中台通讯，保证客户端调用的单一性，适度的与复杂调用链路解偶，降低客户端内部的复杂度。另外，中台实现了一种新的业务能力，这种能力可以被其他的前端应用调用，实现业务的快速拓展。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://georgezou.gitbook.io/thought/ji-shu-wen-zhang/im-ke-hu-duan-she-ji-chang-jian-wen-ti.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
