feat(工具箱): 实现“WebSocket 测试”工具
This commit is contained in:
@@ -177,18 +177,18 @@ export const toolList = [
|
||||
{
|
||||
id: 'network-tools',
|
||||
title: '网络',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
items: [
|
||||
{
|
||||
id: 'websocket-test-tool',
|
||||
component: 'Network/WebSocketTestTool',
|
||||
title: 'WebSocket',
|
||||
title: 'WebSocket 测试',
|
||||
iconClass: 'mdi mdi-connection',
|
||||
desc: 'WebSocket 测试工具',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
version: '0',
|
||||
enabled: false,
|
||||
desc: '连接 WebSocket 服务端,发送和接收消息。',
|
||||
createdAt: '2024-12-01',
|
||||
updatedAt: '2024-12-01',
|
||||
version: '1',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@@ -1,9 +1,550 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
Reference in New Issue
Block a user