feat(工具箱): 实现“WebSocket 测试”工具
This commit is contained in:
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -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 的 WebSocket(WSS)。</n-p>
|
||||||
|
<n-p>若需要连接不带 SSL 的 WebSocket(WS),建议下载到本地后使用。</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>
|
||||||
|
Reference in New Issue
Block a user