feat(工具箱): 实现“WebSocket 测试”工具

This commit is contained in:
2024-12-01 19:58:48 +08:00
parent c4537b1103
commit 56ba451325
2 changed files with 549 additions and 8 deletions

View File

@@ -177,18 +177,18 @@ export const toolList = [
{ {
id: 'network-tools', id: 'network-tools',
title: '网络', title: '网络',
enabled: false, enabled: true,
items: [ items: [
{ {
id: 'websocket-test-tool', id: 'websocket-test-tool',
component: 'Network/WebSocketTestTool', component: 'Network/WebSocketTestTool',
title: 'WebSocket', title: 'WebSocket 测试',
iconClass: 'mdi mdi-connection', iconClass: 'mdi mdi-connection',
desc: 'WebSocket 测试工具', desc: '连接 WebSocket 服务端,发送和接收消息。',
createdAt: '', createdAt: '2024-12-01',
updatedAt: '', updatedAt: '2024-12-01',
version: '0', version: '1',
enabled: false, enabled: true,
}, },
], ],
}, },

View File

@@ -1,9 +1,550 @@
<template> <template>
<div class="tool-detail-page"></div> <div class="tool-detail-page">
<!-- 注意 -->
<n-card size="small" title="注意">
<n-p>由于浏览器限制通过 HTTPS 访问网站时只能连接带 SSL WebSocketWSS</n-p>
<n-p>若需要连接不带 SSL WebSocketWS建议下载到本地后使用</n-p>
</n-card>
<!-- 输入 -->
<n-card size="small" title="输入">
<n-form
class="form-no-feedback"
label-align="left"
label-placement="top"
label-width="auto"
>
<n-form-item label="连接地址:">
<n-input-group class="address-input">
<n-select
v-model:value="data.address.prefix"
:options="[
{ label: 'ws://', value: 'ws://' },
{ label: 'wss://', value: 'wss://' },
]"
/>
<n-input
v-model:value="data.address.suffix"
type="text"
></n-input>
</n-input-group>
</n-form-item>
<n-form-item label="发送内容(自动移除换行符):">
<n-input
v-model:value="data.inputs"
placeholder="在此处输入要发送的内容"
type="textarea"
:rows="2"
></n-input>
</n-form-item>
</n-form>
</n-card>
<!-- 操作 -->
<n-card size="small" title="操作">
<n-flex>
<n-button
type="success"
:disabled="data.ws !== null"
@click="wsConnect"
>连接</n-button>
<n-button
type="warning"
:disabled="data.ws === null"
@click="wsClose"
>断开</n-button>
<n-button
type="primary"
:disabled="data.ws === null"
@click="wsSend"
>发送</n-button>
<n-button
type="error"
@click="clearInputs"
>清空输入</n-button>
<n-button
type="error"
@click="clearMessages"
>清空消息</n-button>
</n-flex>
</n-card>
<!-- 日志 -->
<n-card size="small" title="日志">
<div
ref="logsContentRef"
class="logs-content"
:style="{ height: (data.logsHeight + 'px') }"
>
<div class="message-list">
<div
v-for="item in data.messages"
:key="item.id"
class="message-item"
>
<!-- 时间 -->
<n-tag
:type="item.type === 'send' ? 'primary' : 'success'"
size="small"
>{{ getCommonDateTime(item.time) }}</n-tag>
<!-- 消息内容 -->
<div
v-if="data.parseType === 'html'"
v-html="item.message"
class="message-content"
></div>
<div
v-else
class="message-content"
>{{ item.message }}</div>
</div>
</div>
</div>
</n-card>
<!-- 设置 -->
<n-card size="small" title="设置">
<n-form
class="form-no-feedback"
label-align="left"
label-placement="top"
label-width="auto"
>
<!-- 日志高度 -->
<n-form-item label="日志高度">
<n-input-number
v-model:value="data.logsHeight"
:min="80"
:max="800"
:precision="0"
:step="1"
></n-input-number>
</n-form-item>
<!-- 日志最大行数 -->
<n-form-item label="日志最大行数">
<n-input-number
v-model:value="data.logsMax"
:min="1"
:max="8192"
:precision="0"
:step="1"
></n-input-number>
</n-form-item>
<!-- 解析类型 -->
<n-form-item label="解析类型">
<n-radio-group v-model:value="data.parseType">
<n-radio-button
v-for="item in data.parseTypes"
:key="item.name"
:label="item.label"
:value="item.name"
/>
</n-radio-group>
</n-form-item>
<!-- 自动滚动 -->
<n-form-item label="自动滚动">
<n-radio-group v-model:value="data.autoScroll">
<n-radio-button label="开启" :value="true" />
<n-radio-button label="关闭" :value="false" />
</n-radio-group>
</n-form-item>
</n-form>
</n-card>
</div>
</template> </template>
<script setup> <script setup>
import {
NButton, NCard,
NFlex, NForm, NFormItem,
NInput, NInputGroup, NInputNumber,
NP, NRadioButton, NRadioGroup, NSelect, NTag,
} from 'naive-ui';
import {
reactive, ref, nextTick, onMounted, onBeforeUnmount,
} from 'vue';
import {
$dialog, $notification,
} from '@/assets/js/naive-ui';
import getCommonDateTime from '@frost-utils/javascript/common/getCommonDateTime';
/** 数据 */
const data = reactive({
/** 连接地址 */
address: {
prefix: 'ws://',
suffix: '',
},
/** 自动滚动结果 */
autoScroll: true,
/** 发送内容 */
inputs: '',
/** 日志高度 */
logsHeight: 320,
/** 日志最大行数 */
logsMax: 100,
/** 接收内容 */
messages: [],
/** 消息 ID */
messageID: 0,
/** 消息解析类型 */
parseType: 'string',
/** 消息解析类型列表 */
parseTypes: [
{ name: 'html', label: 'HTML' },
{ name: 'json', label: 'JSON' },
{ name: 'string', label: '字符串' },
],
/**
* @desc WebSocket 对象
* @type {WebSocket}
*/
ws: null,
});
/**
* @desc 日志内容 ref
* @type {VueRef<HTMLElement>}
*/
const logsContentRef = ref(null);
/** 清空输入 */
function clearInputs() {
$dialog.create({
content: '确定要清空输入的内容吗?',
negativeText: '取消',
positiveText: '确定',
title: '确认',
type: 'default',
onPositiveClick: () => {
data.inputs = '';
},
});
}
/** 清空消息 */
function clearMessages() {
$dialog.create({
content: '确定要清空消息内容吗?',
negativeText: '取消',
positiveText: '确定',
title: '确认',
type: 'default',
onPositiveClick: () => {
data.messages = [];
data.messageID = 0;
},
});
}
/**
* @description 处理 WebSocket 关闭
* @param {CloseEvent} event
*/
function handleClose(event) {
let ws = event.target;
notify({
message: 'WebSocket 已关闭',
type: 'warning',
});
if (ws) {
ws.removeEventListener('close', handleClose);
ws.removeEventListener('error', handleError);
ws.removeEventListener('message', handleMessage);
ws.removeEventListener('open', handleOpen);
}
}
/** 处理 WebSocket 错误 */
function handleError() {
notify({
message: 'WebSocket 发生错误',
type: 'error',
});
}
/**
* @description 处理 WebSocket 消息
* @param {MessageEvent} ev
*/
function handleMessage(ev) {
let msg = ev.data;
let el = logsContentRef.value;
if (!msg) {
return;
}
let parsed = wsParse(false, msg);
let result = (parsed || msg)
console.log('%c%s', 'color: #2196F3;', '[接收]', (parsed || result));
// 记录消息
pushMessage('receive', msg);
// 自动滚动
nextTick(() => {
if (el && data.autoScroll) {
el.scrollTo(0, el.scrollHeight)
}
});
}
/** 处理 WebSocket 打开 */
function handleOpen() {
notify({
message: 'WebSocket 已连接',
type: 'info',
});
}
/** 初始化 */
function init() {
// 检测兼容性
if (typeof WebSocket === 'undefined') {
notify({
duration: 0,
message: '您的浏览器不支持 WebSocket。',
title: '错误',
type: 'error',
});
}
}
/**
* @description 提示信息
* @param {object} options
* @param {number} options.duration
* @param {string} options.message
* @param {string} options.title
* @param {string} options.type
*/
function notify(options) {
let {
duration = 3000,
message = '',
title = '提示',
type = 'info',
} = options;
return $notification.create({
content: message,
duration: duration,
title: title,
type: type,
});
}
/**
* @description 添加消息
* @param {string} type 类型receive、send
* @param {string} msg 消息内容
*/
function pushMessage(type, msg = '') {
let types = ['receive', 'send'];
if (types.indexOf(type) === -1) {
return;
}
let current = data.messages.length;
let max = data.logsMax;
// 最大行数
if (current >= max) {
data.messages.splice(0, (current - max + 1));
}
data.messageID += 1;
data.messages.push({
id: data.messageID,
message: msg,
time: Date.now(),
type: type,
});
}
/** 关闭连接 */
function wsClose() {
if (data.ws) {
data.ws.close();
data.ws = null;
}
}
/** 打开连接 */
function wsConnect() {
let info = data.address;
let address = (info.prefix + info.suffix);
if (!info.suffix) {
notify({
message: '请填写连接地址',
title: '连接失败',
type: 'warning',
});
return;
}
try {
let ws = new WebSocket(address);
// 监听事件
ws.addEventListener('close', handleClose);
ws.addEventListener('error', handleError);
ws.addEventListener('message', handleMessage);
ws.addEventListener('open', handleOpen);
// 保存对象
data.ws = ws;
} catch (error) {
notify({
message: String(error),
title: '连接失败',
type: 'error',
});
}
}
/**
* @description 解析消息
* @param {boolean} isSend 是否为发送,否则为接收
* @param {string} content 消息文本内容
* @returns {null|object|string} 成功则返回解析后的消息,否则返回 null
*/
function wsParse(isSend = false, content = '') {
let parseType = data.parseType;
if (parseType === 'json') {
// JSON
try {
return JSON.parse(content);
} catch (error) {
let s1 = isSend ? '解析发送的 JSON 消息失败' : '解析接收的 JSON 消息失败';
let s2 = String(error);
console.warn(s1);
console.warn(s2);
notify({
message: s2,
title: s1,
type: 'warning',
});
return null;
}
} else {
// 默认不处理
return content;
}
}
/** 发送消息 */
function wsSend() {
let instance = data.ws;
let message = data.inputs.replace(/(\n|\r)/g, '');
let parsed = wsParse(true, message);
if (instance && parsed) {
console.log('%c%s', 'color: #4CAF50;', '[发送]', parsed);
instance.send(message);
pushMessage('send', message);
}
}
onMounted(() => {
init();
});
onBeforeUnmount(() => {
wsClose();
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.address-input {
.n-select {
width: 100px;
}
.n-input {
flex-grow: 1;
width: 0;
}
}
.logs-content {
padding: 8px 16px;
border: 1px solid #F0F0F0;
border-radius: 4px;
background-color: #FFF;
line-height: 1.6;
overflow-y: auto;
user-select: text;
.message-item {
display: flex;
margin: 0.5rem 0;
word-break: break-all;
> * {
margin: auto 0;
}
}
.n-tag {
margin-right: 0.5em;
}
.message-content {
position: relative;
}
}
</style> </style>