一、什么是websocket
WebSocket 是基于 TCP 的一种新的应用层网络协议。它实现了浏览器与服务器全双工通信,允许服务器主动发送信息给客户端。在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket可以用来弥补http协议在持久通信能力方面和只支持客户端发起上的不足。
特点
1 与http协议相同,都是建立在Tcp协议之上,位于应用层的协议
2 与 HTTP 协议有着良好的兼容性:默认端口也是 80(ws) 和 443(wss,运行在 TLS 之上),并且握手阶段采用 HTTP 协议
3 较少的控制开销:连接创建后,ws 客户端、服务端进行数据交换时,协议控制的数据包头部较小
4 可以发送文本,也可以发送二进制数据;
5 没有同源限制,客户端可以与任意服务器通信;
6 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL;
应用场景
1 即时聊天通讯(v4.09.1的AI聊天 和 c端的客服聊天)
2 弹幕展示
3 在线协同编辑
4 实时地图位置
5 实时通知
主要应用于即时性强的场景,如即时通知、实况显示等
二、websocket的基础介绍
1.实例属性解析
图1 websocket属性展示图
① 四种on方法
onopen:用于指定连接成功后的回调函数;
onmessage:用于指定当从服务器接受到信息时的回调函数;
onclose:用于指定连接关闭后的回调函数,如果连接已经关闭,则此方法不执行任何操作;
onerror:用于指定连接失败后的回调函数;
② readyState(四种状态)
websocket的实例属性readyState(只读),返回当前 WebSocket 的连接状态
共有 4 种状态:
CONNECTING — 正在连接中,对应的值为 0;
OPEN — 已经连接并且可以通讯,对应的值为 1;
CLOSING — 连接正在关闭,对应的值为 2;
CLOSED — 连接已关闭或者没有连接成功,对应的值为 3;
③ 其余属性
binaryType:使用二进制的数据类型连接,两种取值:Blob和ArrayBuffer;
bufferedAmount(只读):未发送至服务器的字节数;
extensions(只读):服务器选择的扩展;
protocol(只读):用于返回服务器端选中的子协议的名字;
url(只读):返回值为当前构造函数创建 WebSocket 实例对象时 URL 的绝对路径;
2.主要方法
①close(code,[reason]):用于关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作;close方法中参数code和reason都是选填的,code是一个数字值表示关闭连接的状态号,解释连接被关闭的原因。如果这个参数没有被指定,默认的取值是1000 (表示正常连接关闭),而reason是一个可读的字符串,表示连接被关闭的原因。这个字符串必须是不长于123字节的UTF-8 文本。
对于close方法中的code取值,可以参考:developer.mozilla.org/en-US/docs/…
②send(data):用于在连接成功后向服务端发送数据,根据所需要传输的数据的大小来增加 bufferedAmount 的值;
send方法目前不是任何数据都能发送,现在只能发送三类数据,包含UTF-8的string类型(即JavaScript的string),ArrayBuffer和Blob。
3.主要事件
close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置;
error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置;
message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置;
open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置;
4.如何建立连接
客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket。
服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性。
客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应。
三、websocket的使用
1.生成websocket实例对象,并初始化
export function useWebSocket<Data = any>(
url: MaybeRefOrGetter<string | URL | undefined>,
options: UseWebSocketOptions = {},
): UseWebSocketReturn<Data> {
const {
onConnected,
onDisconnected,
onError,
onMessage,
immediate = true,
autoClose = true,
protocols = [],
} = options
const data: Ref<Data | null> = ref(null)
const status = ref<WebSocketStatus>('CLOSED')
const wsRef = ref<WebSocket | undefined>()
const urlRef = toRef(url)
const _init = () => {
if (typeof urlRef.value === 'undefined')
return
const ws = new WebSocket(urlRef.value, protocols)
wsRef.value = ws
status.value = 'CONNECTING'
ws.onopen = () => {
status.value = 'OPEN'
onConnected?.(ws!)
}
ws.onclose = (ev) => {
status.value = 'CLOSED'
onDisconnected?.(ws, ev)
}
ws.onerror = (ev) => {
onError?.(ws!, ev)
}
ws.onmessage = (ev: MessageEvent) => {
data.value = ev.data
onMessage?.(ws!, ev)
}
}
连接身份确认
连接中的验证身份过程可以有如下两种处理方案:
一种是实例open成功后,通过send方法将本地cookie或者存于localStrage中的身份信息发送给服务端验明身份;
ws.onopen = () => {
status.value = 'OPEN'
// 在握手阶段添加Authorization头
ws.send('Authorization: Bearer ' + YOUR_TOKEN);
onConnected?.(ws!) // 使用者传输进来的在open阶段调用的函数
}
另一种则是在本地取不到身份信息的情况下,可以考虑让后端出一个专门生成ticket的接口,利用这个接口返回的参数拼接在url尾部,然后去做websocket请求,把身份验证的过程交由后端处理。
try{
const res = await _chatGetWebSocketTicket(); // 身份凭证接口
if (res?.code !== 0) {
UiToast({ type: 'error', content: '请刷新页面' });
return;
}
const ticket = res?.zpData?.ticket ?? '';
// 拼接在url尾部创建socket连接
ws.value = new WebSocketAts(`${CAMPUS_AI_WEBSOCKET_URL.value}?ticket=${ticket}`);
}catch (error) {
console.log(error);
}
PS:websoket没有请求头一说,参数一般都是在链接里,或者通过send发送。
2.心跳检测(自动心跳)
websocket连接后,会出现由于网络波动或者服务端错误等问题导致的连接断开或连接没断但不可用等状况。
解决方案:可以采用心跳检测来验明客户端和服务端双方的通讯是否正常。
自动心跳(Automatic Heartbeat)是一种在网络通信中常用的机制,用于维持连接的活跃状态,检测连接是否仍然有效,并及时发现和处理连接断开或故障的情况。心跳机制通过定期发送“心跳”消息(通常是一个简单的 ping 或者 pong 消息)来确认连接双方的状态。
export function useWebSocket<Data = any>(
url: MaybeRefOrGetter<string | URL | undefined>,
options: UseWebSocketOptions = {},
): UseWebSocketReturn<Data> {
......
let heartbeatPause: Fn | undefined // 心跳停止函数
let heartbeatResume: Fn | undefined // 心跳恢复函数
let pongTimeoutWait: ReturnType<typeof setTimeout> | undefined // 心跳超时定时器
// 重置心跳
const resetHeartbeat = () => {
clearTimeout(pongTimeoutWait)
pongTimeoutWait = undefined
}
// 关闭连接
const close: WebSocket['close'] = (code = 1000, reason) => {
resetHeartbeat() // 重置心跳
heartbeatPause?.() // 心跳停止
wsRef.value.close(code, reason) // 此时尚未建立重连逻辑,所以调取close方法直接断开连接
wsRef.value = undefined
}
// 生成心跳控制函数
if (options.heartbeat) {
......
// useIntervalFn的逻辑是将第一个回调参数放入timer标记的setInterval中
// 返回的pause函数作用是将该timer清除掉
// 返回的resume函数则是为timer重新赋值setInterval令其重新生效
// PS:其实用setTimeout去做更好,因为 setInterval 在一些情况下,会导致前后两次定时器代码的执行间隔产生不可预料的变化,甚至会跳过某次定时器代码的执行
// 具体可参考 https://zhuanlan.zhihu.com/p/87595858
// 在页面失活时,定时器也会受到限制,这一点可以用webworker去解决
const { pause, resume } = useIntervalFn(
() => {
// message一般为'ping'
wsRef.value.send(message)
if (pongTimeoutWait != null)
return
pongTimeoutWait = setTimeout(() => {
// 此时尚未建立重连逻辑,所以心跳超时直接断开连接
close()
}, pongTimeout)
},
interval, // 注意一下pongTimeout必须小于等于interval
{ immediate: false },
)
heartbeatPause = pause
heartbeatResume = resume
}
// 初始化
const _init = () => {
......
ws.onopen = () => {
......
heartbeatResume?.() // 连接建立时就开始启动心跳
}
......
ws.onmessage = (e: MessageEvent) => {
if (options.heartbeat) {
// 接收到信息证明连接通道仍然稳定,此时清除掉pongTimeoutWait定时器,避免走入websocket关闭逻辑
resetHeartbeat()
const {
message = DEFAULT_PING_MESSAGE, // DEFAULT_PING_MESSAGE默认写死为PING
responseMessage = message,
} = resolveNestedOptions(options.heartbeat)
//resolveNestedOptions函数是
if (e.data === responseMessage)
// 如果接收到的数据与约定的心跳返回数据一致,那么直接退出执行
return
}
data.value = e.data
onMessage?.(ws!, e)
}
}
}
由此,完成了心跳检测的代码逻辑
小结:
这段代码中,声明了resetHeartbeat函数,用于清除pongTimeoutWait定时器重置心跳超时逻辑的执行时间;接着声明了close函数,用于重置并关闭心跳,断开客户端与服务端websocket的连接;通过useIntervalFn和输入的匿名箭头函数,生成了心跳控制函数pause和resume,并把它们分别赋值给了heartbeatPause和heartbeatResume,还在其中为pongTimeoutWait赋值了定时器,用于服务端回复超时关闭连接用;最后,在初始化中,在websocket的open事件中,加入了启动心跳的逻辑,在message事件中,加入了重置心跳的逻辑。整体逻辑是通过open事件触发heartbeatResume去初始化心跳,并开始pongTimeoutWait的计时,接收到消息后证明连接仍然生效,此时清除pongTimeoutWait的计时,若pongTimeoutWait计时结束还未收到回复会触发close函数逻辑关闭websocket连接
3.断线重连
仅仅有心跳检测机制是不足够的,在心跳检测到websocket的连接已经断开时,我们可以通过加入断线重连机制,使websocket在断开时能够实现自动重连服务器。
export function useWebSocket<Data = any>(
url: MaybeRefOrGetter<string | URL | undefined>,
options: UseWebSocketOptions = {},
): UseWebSocketReturn<Data> {
......
let explicitlyClosed = false // 标记是否为主动关闭,为true时重连不生效
let retried = 0 // 标记当前重连次数
// 关闭连接
const close: WebSocket['close'] = (code = 1000, reason) => {
if (!isClient || !wsRef.value) // isClient用来判断是否为客户端,通过判断window跟document是否为undefined来进行取值
return
explicitlyClosed = true // 标记为主动关闭
resetHeartbeat() // 重置心跳
heartbeatPause?.() // 心跳停止
wsRef.value.close(code, reason)
wsRef.value = undefined
}
// 初始化
const _init = () => {
// 如果为主动关闭,则直接return结束进程
if (explicitlyClosed || typeof urlRef.value === 'undefined')
return
const ws = new WebSocket(urlRef.value, protocols)
......
ws.onopen = () => {
status.value = 'OPEN'
retried = 0 // open事件触发代表重连(连接)成功,此时重置重连次数
......
}
ws.onclose = (ev) => {
......
if (!explicitlyClosed && options.autoReconnect && ws === wsRef.value) {
const {
retries = -1, // 重连次数,不传则不限制重连次数,
delay = 1000, // 重连逻辑执行的延迟时间
onFailed, // 最终重连失败回调
} = resolveNestedOptions(options.autoReconnect)
// retries?: number | (() => boolean)
// 当传入的retries参数为数字时
if (typeof retries === 'number' && (retries < 0 || retried < retries)) {
retried += 1
setTimeout(_init, delay)
}
// 当传入的retries参数为返回值是布尔值的函数时
else if (typeof retries === 'function' && retries()) {
setTimeout(_init, delay)
}
else {
onFailed?.()
}
}
}
}
if (options.heartbeat) {
......
pongTimeoutWait = setTimeout(() => {
close() // 执行
explicitlyClosed = false // 标记为非主动关闭,后面会走onclose中的重连逻辑
}, pongTimeout)
......
}
}
当new WebSocket时,客户端跟服务端连接报错(没连上)时,如下图所示:
图2 websocket连接报错
此时websocket实例先执行onerror事件,再执行onclose事件,故_init时,若出现报错,会走入定义的onclose回调函数中,并计时计数,重复重连的逻辑,直到重连成功或者重连次数耗尽。
PS:onerror一般是服务器有问题了,一开始就没连接上,不是网络波动或者其它问题,所以没必要重连
由此,完成了断线重连的代码逻辑
小结:
这段代码中,通过explicitlyClosed和retried变量,分别标记当前关闭是否为主动关闭 和 当前重连次数,通过onopen事件将retried重置,让重连参数初始化;通过onclose事件将retried自增,并调用定时器执行_init来完成重连逻辑;在pongTimeoutWait的定时器逻辑中,在调用了close函数后把explicitlyClosed设置为false,可以把close函数中执行的explicitlyClosed = true覆盖掉,因为wsRef.value.close(code, reason)代码为异步逻辑,执行到这里时会将其放入异步队列中,再执行close函数下面的explicitlyClosed = false,当执行到websocket的onclose事件时,explicitlyClosed的值已经为false,此时便可进入onclose的断线重连逻辑中。
4.断连时存储发送数据,重连时重新发送
export function useWebSocket<Data = any>(
url: MaybeRefOrGetter<string | URL | undefined>,
options: UseWebSocketOptions = {},
): UseWebSocketReturn<Data> {
......
let bufferedData: (string | ArrayBuffer | Blob)[] = [] // 存储断连发送数据
// 用于连接恢复时,发送存储数据
const _sendBuffer = () => {
if (bufferedData.length && wsRef.value && status.value === 'OPEN') {
for (const buffer of bufferedData)
wsRef.value.send(buffer)
bufferedData = []
}
}
// 封装send函数,若当前处于未连接状态,先缓存数据到bufferedData中,若处于连接状态,则发送前先遍历bufferedData,查看是否有重连时未发出去的缓存数据,全部发送后再将当前最新数据发送
const send = (data: string | ArrayBuffer | Blob, useBuffer = true) => {
// 判断是否为连接状态,未连接则缓存发送信息至bufferedData
if (!wsRef.value || status.value !== 'OPEN') {
if (useBuffer)
bufferedData.push(data)
return false
}
// 处于连接状态则遍历发送bufferedData中的数据
_sendBuffer()
wsRef.value.send(data)
return true
}
if (options.heartbeat) {
......
const { pause, resume } = useIntervalFn(
() => {
+ send(message, false)
- wsRef.value.send(data)
......
},
interval,
{ immediate: false },
)
......
}
}
5.url为ref并且被改变时
const open = () => {
if (!isClient && !isWorker)
return
close()
explicitlyClosed = false
retried = 0
_init()
}
watch(urlRef, open)
6.刷新页面断开websocket
// 当浏览器窗口关闭或者刷新时,在页面完全卸载前beforeunload事件会触发,可以通过监听其来关闭websocket连接,避免服务端一直保持连接不断开
if (isClient)
useEventListener('beforeunload', () => close())
四、考虑边界情况
1.多开解决方案
聊天场景中,当项目设置页面不互踢时,就会存在多开聊天的情况,即多个页面进入同一个聊天界面(跟同一个人交谈),当用户在其中一个页面发送了信息后,其它页面也应回显消息,同时在对方回复后,所有页面也因同步回显回复。那么,我们该如何处理这个问题呢?
websocket回显方案
可以使用接口发送用户传输的信息,统一在websocket中通过message事件吐回来,这样,在其它的多开页面中大家都能收到用户输入的数据,并且此时数据的处理逻辑只需统一维护到message事件中即可
PS:这种方法可以处理跨浏览器多开问题,这是其它方案所做不到的
<template>
<input v-model=inputValue />
</template>
<script>
const inputValue = ref('')
const ws = new WebSocket('ws://example.com/socket')
try{
_sendMessage(inputValue)
}catch(error){
......
}
ws.onMessage = () => {
// 回显数据
......
}
</script>
Broadcast Channel方案
在页面中BroadcastChannel接口代理了一个命名频道,同一频道的页面可以相互发送和监听消息。当某个页面向频道发送消息时,所有监听该频道的页面都会接收到消息。我们可以利用这一点去做跨页面通讯,当用户在一个页面中发送信息时,通知其它页面这一行为,并将发送数据同步过去,在其它页面的onmessage事件中做回显处理即可
PS:Broadcast Channel Api受同源策略限制,但是也可实现跨窗口通讯,只是不能向websocket一样做到跨浏览器通讯
// 通过new BroadcastChannel创建一个频道实例,使用postMessage方法向频道发布消息,而在同一频道的对象 则通过onmessage和onmessageerror消息可以广播到所有监听了该频道的BroadcastChannel对象。
// 因为都为同一页面,所以只需在对应位置写一份postMessage和onmessage的处理方法即可
<template>
<input v-model=inputValue />
<button></button>
</template>
<script>
import { ref,onBeforeUnmount,onMounted} from 'vue'
const ws = new WebSocket('ws://example.com/socket')
const inputValue = ref('')
const messageList = ref([])
// 创建一个广播名为message的实例
const broadcast = new BroadcastChannel('message')
// 发送信息
const handleSendMessage = () =>{
broadcast.postMessage(inputValue.value)
}
// 当调用ws.send时,在后面同步调用handleSendMessage即可
// 接收同一个频道传递的消息
broadcast.onmessage = (e)=>{
const newMessage = {
...e.data,
// 数据处理
}
messageList.value.push(newMessage)
// 逻辑处理
}
// 处理接收到的错误消息
broadcast.onmessageerror = (e) =>{console.log(e)}
// 手动关闭频道广播 防止内存泄漏
onBeforeUnmount(()=>broadcast.close())
</script>
storage事件+LocalStorage方案
这个方案利用了本地浏览器存储,实现了同域下的数据共享
localStorage 方式,基于 window.addEventListener('storage', function(event) {})事件实现了 localStore 变化时候的数据监听,你可以在监听的回调函数中处理接收到的数据逻辑
PS:在当前页面操作localStorage,当前也不会触发storage事件,其它多开的同源页面则会触发
const ws = new WebSocket('ws://example.com/socket')
function send(){
ws.send()
// 改变localStorage
// 逻辑处理
}
window.addEventListener('storage', (event) => {
// 逻辑处理
});
sharedWorker方案
SharedWorker 是 Web Workers API 的一部分,它允许在浏览器后台运行 JavaScript 代码,与主线程并行处理任务。与 Worker 不同的是,SharedWorker 可以被多个浏览上下文(如窗口、标签页或 iframe)共享。这意味着,一旦一个 SharedWorker 被创建,多个页面可以连接到它,并通过消息传递来共享数据和功能。
// 这里踩了一个坑,用红宝书里的写法new SharedWorker('./shared-worker.js')并没有生效,这是由于Webpack 处理模块路径的方式导致的,使用new SharedWorker(new URL('./shared-worker.js', import.meta.url))去处理即可
// Webpack 默认只处理通过 import、require 或特定的动态 import() 引入的模块
// 页面代码
const ws = new WebSocket('ws://example.com/socket')
const worker = new SharedWorker(new URL('./shared-worker.js', import.meta.url));
// 监听消息事件
worker.port.onmessage = function (event) {
console.log('接收到 event', event);
const newMessage = {
...event.data,
// 数据处理
}
// 逻辑处理
};
function sendMessage(msg) {
// 发送消息
worker.port.postMessage(msg);
const newMessage = {
...msg,
// 数据处理
}
// 逻辑处理
}
// shared-worker.js示例
const connections = [];
self.onconnect = function (event) {
const port = event.ports[0];
connections.push(port);
port.onmessage = function (event) {
// 接收到消息时,向所有连接发送该消息
connections.forEach(function (conn) {
if (conn !== port) {
conn.postMessage(event.data);
}
});
};
port.start();
};
iframe+postMessage方案(遗弃方案)
这个方案其实行不通,因为要单纯使用postMessage,就必须要有open和opener,但是同一页面多开的情况下拿不到这个,iframe就更不行了,因为要加入iframe,其中的src必须与当前需要多开的页面相同,那样会陷入无限嵌套的情况。
2.触发事件调用多个函数的情况
// 比如使用者希望在websocket的send、message、close、open事件触发时处理多个函数的调用
// 以message事件为例
const msgCallbackList = [] // message事件触发时需执行的函数列表
// 向函数列表添加函数
onAddMsgCallback(callback = {}) {
if (typeof callback === 'function' && msgCallbackList.indexOf(callback) === -1) {
msgCallbackList.push(callback);
}
}
// 移除函数
onDeleteMsgCallback(callback) {
msgCallbackList = msgCallbackList.filter(item => item !== callback);
}
// message事件触发时调用
ws.onmessage = res => {
......
msgCallbackList.forEach(callback => callback && callback(res));
};
3.服务端主动断开
在websocket连接中,当服务端主动断开连接时,按照useWebSocket的代码,是要进行断线重连的,但是此时断开连接的原因在服务端,客户端进行重连没有任何意义,所以,此时我们要做兜底处理,一个简单的处理方式是可以和后端协商,增加一个closeCode字段,当服务端的websocket连接关闭时,向所有websocket连接发送含closeCode标记为服务端关闭的特征数据,客户端拿到该数据则停止重连
// 假设与后端协商closeCode 0:正常连接 1:服务端断开
ws.onmessage = (e: MessageEvent) => {
......
if(e.data.closeCode === 1){
close()
}
}
// 客户端主动断开只需直接调用close函数即可
// 关闭连接
const close: WebSocket['close'] = (code = 1000, reason) => {
if (!isClient || !wsRef.value) // isClient用来判断是否为客户端,通过判断window跟document是否为undefined来进行取值
return
explicitlyClosed = true // 标记为主动关闭
resetHeartbeat() // 重置心跳
heartbeatPause?.() // 心跳停止
wsRef.value.close(code, reason) // 此时直接关闭websocket连接,不走重连逻辑
wsRef.value = undefined
}
4.网络关闭导致断开
当客户端网络关闭时,也会导致websocket断开连接,此时进行断线重连也是没有意义的,我们可以利用浏览器的 online和offline事件,在offline触发的时候关闭重连和心跳轮询,然后online的时候进行断线重连,这个方案可以解决客户端网络关闭或网络波动引起的问题
图3 网络中断与恢复
// 可以加个中间变量,标记当前websocket是否生效
window.addEventListener("online", () => {
console.log('网络连接恢复');
if(isValid){
open()
}
}, true);
window.addEventListener("offline", () => {
console.log('网络连接中断');
if(isValid){
close()
}
}, true);
5.判断浏览器是否支持socket
if (window.WebSocket || window.MozWebSocket || typeof WebSocket !== 'function') {
console.log("WebSocket is supported by this browser.");
return
}
6.考虑页面失活状态
使用定时器来做心跳检测会有问题,页面失活一段时间定时器就变惰性了(偶尔执行一下甚至不执行),所以页面失活状态下连接是极高概率会断开的。
一般是在电脑息屏后15分钟左右定时器就会自动停止运行,此时心跳也会停止发送,就会出现服务断开的情况,用worker可以解决该问题。
把websocket置于worker中工作,通过JS主线程与worker通讯,来调取websocket的api即可
7.同一页面多开复用同一websocket实例
具体与6工作原理很像,将websocket实例置于worker中,通过与worker的通讯获取到同一实例
6-7 可参考这篇文章:juejin.cn/post/736977…
最后
分享的第三部分主要是依据vue-useWebSocket来讲解的,感兴趣可以去查看源码github.com/vueuse/vueu…
Tip:对于子业务嵌套WebSocket,可以在使用websocket的页面中调用onBeforeRouteLeave,做路由守卫规范websocket断开时机
// 路由守卫规范websocket断开时机,避免多开
onBeforeRouteLeave((to, from, next) => {
if (ws.value) {
ws.value.close();
}
next();
});