---
nav: utils
toc: content
group: 网络请求
---

# WebSocket
对 WebSocket 的简易封装，支持配合 React 使用，同时实例与组件 hook分离，可以跨组件共享实例。



## 安装
```bash
npm i @baidu/starling-web-socket --save
```


## 使用
### 创建 WebSocket 客户端
```js {3}
import {createWebSocket} from '@baidu/starling-web-socket';

const demoSocket = createWebSocket('ws://127.0.0.1:8080');
```
此时连接尚未建立，你可以随时打开连接，当然也可以在创建时，立马开始连接。
```js
// open 还有一个别名是 connect
const demoSocket = createWebSocket('ws://127.0.0.1:8080').open();
```

### 接收消息
**监听来自 Socket 的消息并加以处理**
```js
// onReceive 还有一个别名是 onMessage
demoSocket.onReceive(message => console.log(message));
```
**或等待一个最新消息**
```js {1}
const latestMessage = async demoSocket.receive();
console.log(latestMessage);
```

### 发送消息
**发送一条消息**
```js
demoSocket.send('你好?');
```

**或发起一个「请求」**<br>
:::info{title=combine}
需要配置关联函数 `combine` 来使用 `request` 接口。为了更灵活，该函数接受请求数据和响应数据并返回一个布尔值用以表示两者是关联的。一般情况关联一个请求需要在请求发出前生成一个 `requestId` 携带请求并由服务端响应时带回表示对某一次请求的回应。
:::
```js {6}
const demoSocket = createWebSocket('ws://127.0.0.1:8080', {
    combine: (req, res) => {
        return req?.requestId === res?.requestId;
    }
}).open();
const response = demoSocket.request({
    requestId: 'abcd',
    message: '你是谁?'
}).then(response => {
    console.log(response?.message);
});

```
这个 `requestId` 通常可以自己生成，但为了使用更方便 `starling-web-socket` 提供了一个 requestId 生成器 `withRequestId`，他可以这样使用：

**直接获取一个 requestId**
```js
const requestId = withRequestId();
```

**send 和 request 方法也支持传入函数，通过函数入参注入 requestId**
```js
demoSocket.request(requestId => ({
    requestId,
    message: '你是谁?'
})).then(response => {
    console.log(response?.message);
});
```

**或在函数范围内注入一个 requestId**<br>
与短链接请求不同，在使用 web socket 时有些时候会希望发送一个请求消息后接收多个回复。这时你可以自行保存和使用一个 `requestId`，或使用 `withRequestId` 在一个函数范围内共享一个 `requestId`：
```js {1}
withRequestId(requestId => {
    const offMessage = demoSocket.send({
        requestId,
        message: '你是谁?',
    }).onMessage(response => {
        if (response?.requestId === requestId) {
            if (response?.message === 'done') {
                // 会话结束
                offMessage();
            }
            // 做点什么
        }
    });
});
```

:::success{title=随时可以发送消息}
在发送消息时完全不必担心是否已经与服务器建立了连接，因为如果处在 `connecting` 时消息会被记录，并当 `open` 之后，依次发送记录的消息。
但如果还未安装或连接已经关闭，则会失败这是意料之中的。但别担心后面会介绍如何配置断开重连。
:::



### 断开重连
在配置中打开 reopen 选项，就可以在遇到异常断开时尝试重连。[更多关于重连的配置见 create-option](#create-option)
```js
const demoSocket = createWebSocket('ws://127.0.0.1:8080', {
    // 异常断开时是否尝试重连，默认 true
    reopen: true,
    // 服务器关闭时是否重连，默认 true
    reopenWhenServerClosed: true,
});
```



### 序列化
WebSocket 中的消息类型为 `string | ArrayBufferLike | Blob | ArrayBufferView`，`string` 是宽泛的类型不是准确的类型，因此我们往往期望在发送消息和接收消息时使用更为详细的拥有自定义规范的接口（`interface`）类型。实例内置使用 `JSON.stringify` 和 `JSON.parse` 来进行序列化和反序列化，如果需要自定义序列化和反序列化，请对实例方法 `serialization` 和 `deserialization` 进行重写。利用此接口可以轻松的实现消息数据的封装和规范。



### 插件（拓展）
正如序列化行为一样，`starling-web-socket`总是以重写实例方法的方式进行拓展，这种方式在设计模式中被称为「装饰」，所以插件的封装也就是将一系列的实例装饰行为封装为一个装饰器函数。<br>
一个装饰器接收一个 Socket 实例，然后返回一个拓展后的 Socket 新实例。<br>
例如，为实例添加业务握手功能：

``` js
// 拓展实例接口增加属性
interface HandshakeWebSocketClient extends WebSocketClient {
    isEstablished: boolean;
}

// 定义握手消息接口
interface HandshakeMessage {
    type: 'handshake';
    action: 'syn' | 'ack';
}

// 封装握手动作
function handshake(socket: HandshakeWebSocketClient) {
    return new Promise<void>(resolve => {
        // SYN
        socket.send({
            type: 'handshake',
            action: 'syn',
        }).receive<HandshakeMessage>().then(message => {
            // 接收 SYN 响应为 ACK & SNY
            if (message?.type === 'handshake' && message?.action === 'ack') {
                // 回复 ACK
                socket.send({
                    type: 'handshake',
                    action: 'ack'
                });
                // 完成握手
                resolve();
            }
        });
    });
}

function isHandshakeMessage(v: any): v is HandshakeMessage {
    return v && 'handshake' === v?.type;
}

// 封装握手插件
function withHandshake(client: WebSocketClient): HandshakeWebSocketClient {
    const socket = client as HandshakeWebSocketClient;
    socket.isEstablished = false;
    socket.messageQueue = [];

    const {open, close, send} = socket;

    socket.open = function() {
        open();
        // 在连接完成后自动进行握手，并在握手完成后冲洗消息队列
        handshake(socket).then(() => {
            // 握手完成，标记可传输
            socket.isEstablished = true;
            // 补发缓存消息
            this.messages.flush();
        });
        return this;
    }

    socket.close = function() {
        // 关闭前先挥手释放服务器资源
        // 这里仅演示插件方式不做实现
        close();
    }

    socket.send = function(data: any) {
        // 握手已完成或为握手消息，直接发送
        if (this.isEstablished || isHandshakeMessage(data)) {
            send(data);
            return this;
        }

        // 握手未完成，缓存消息，等待握手完成
        this.messages.push(data);
        return this;
    }
    return socket;
}

// 应用插件（装饰）
const demoSocket = withHandshake(
    createWebSocket('ws://127.0.0.1:8080')
);
```

## React Hooks
工具包提供了 React Hook 的使用方式，用于在 React 组件中方便的使用 Socket。
### useSocket
```js {5}
import {createWebSocket, useSocket} from '@baidu/starling-web-socket';
const desktopWS = createWebSocket('ws://127.0.0.1:8080');

const Comp = () => {
    const [latestMessage, sendMessage] = useSocket(desktopWS);

    const greet = useCallback(() => {
        sendMessage({
            type: 'greet',
            message: 'hello?',
        });
    }, [])

    return (
        <>
            <button onClick={greet}>Greet</button>
            <p>{JSON.stringify(latestMessage)}</p>
        </>
    );
}
```

:::success
在使用 useSocket 时，如果 Socket 未建立连接，则会自动进行连接。可以通过配置`{autoOpen: false}`来禁止自动连接。
:::

组件一般不需要对所有的消息做出反应，因此推荐**总是在使用 useSocket 时使用 match 选项**<br>
而不是在组件内对 latestMessage 进行过滤，使用 match 性能会更好。
``` js
const [latestMessage, sendMessage] = useSocket(desktopWS, {
    match: message => message.type === 'greet'
});
```

### useReadyState
正如上文中提到的，一般情况我们并不需要关心连接状态，并且如果想要在作出某些动作时以某种连接状态作为前置条件建议直接在 Socket 实例上读取，而不是使用 React State 将拥有更好的性能。如果是希望将连接状态显示到界面上或给与用户反馈，那么可以使用 `useReadyState` 来获取 Socket 的状态，并在其状态变化时更新组件。
``` js
const socketReadyState = useReadyState(desktopWS);
```

## API
### create option
| 参数                    | 说明                         | 默认值         | 是否必填 | 类型                  |
|------------------------|------------------------------|---------------|--------|------------------------|
| protocols              | 协议                          | -             | 否    | HTMLElemnet             |
| reopen                 | 是否在异常断开始进行重连         | true          | 否    | boolean                  |
| reopenDelay            | 重连间隔（ms）                 | 1000          | 否    | number                  |
| reopenAttempts         | 最大重连次数                   | 10            | 否    | number                  |
| reopenWhenServerClosed | 服务器关闭时是否重连             | true          | 否    | boolean                 |
| combine                | 收发消息绑定                   | -              | 否    | {}                     |
| onOpen                 | 打开回调函数                   | -              | 否    | (e: Event) => void     |
| onClose                | 关闭回调函数                   | -              | 否    | (e: Event) => void     |
| onMessage              | 接到消息回调函数                | -              | 否    | (e: Event) => void     |
| onError                | 异常回调函数                   | -              | 否    | (e: Event) => void     |

### instance attribute
| 属性名            | 说明                                                     |
|------------------|---------------------------------------------------------|
| readyState       | 连接状态（-1：未开始、0：连接中、1：已连接、2：关闭中、3：已关闭）|
| isOpened         | 是否已经打开                                              |
| isActive         | 是否处于活跃状态（连接中或已连接）                            |
| messages         | 缓存中等待发送的消息列表                                    |

### instance method
| 方法名               | 说明                          | 参数                                       | 返回值                 |
|---------------------|------------------------------|--------------------------------------------|------------------------|
| open                | 打开连接，别名 connect         | -                                           | Socket                |
| close               | 关闭连接，别名 disconnect      | -                                           | -                      |
| send                | 发送消息                      | 消息数据或接受 requestId 并返回消息数据的函数    | () => void             |
| request             | 请求消息                      | 消息数据或接受 requestId 并返回消息数据的函数    | () => void              |
| receive             | 等待一个新消息                 | Promise<T = any>                            | (e: Event) => void     |
| onReceive           | 监听消息，别名 onMessage       | handler: (data: T) => void                  | (e: Event) => void     |
| onReadyStateChange  | ReadyState 状态变更时调用      | handler: (readyState: ReadyState) => void   | (e: Event) => void     |
| serialization       | 序列化，发送消息前调用          | 接受要发送的数据返回一个新的数据                  | (data: object) => string     |
| deserialization     | 反序列化，接收到消息时调用       | 接受收到的消息数据返回一个新的数据                | (data: string) => object   |
