# sendMixin & XRequest
# 功能说明
发送操作混入,提供统一的请求状态管理和中断控制,支持以下特性:
- 支持 SSE (Server-Sent Events) 和 Fetch 两种请求方式
- 错误处理机制
- 支持自定义数据转换器
- 集成 XRequest 类提供强大的请求处理能力
# 导入和使用
import { sendMixin } from 'vue-element-ui-x';
export default {
mixins: [sendMixin],
// ...
};
说明
以下示例的导入方式是解决文档站打包时的报错,正常情况下请按正常的方式导入即可
# 使用示例
# sendMixin 基本用法
基本流程
用户操作 → handleSend/handleAbort/handleFinish → 更新 loading 状态 → 调用用户回调
<template>
<div class="container">
<div class="btn-list">
<el-button
:disabled="loading"
type="primary"
@click="customSend"
>
{{ loading ? '加载中...' : '模拟请求' }}
</el-button>
<el-button
:disabled="!loading"
type="primary"
@click="handleFinish"
>
结束请求
</el-button>
<el-button
:disabled="!loading"
type="danger"
@click="handleAbort"
>
终止请求
</el-button>
</div>
<div
v-if="basicResult"
class="result"
>
结果:{{ basicResult }}
</div>
</div>
</template>
<script>
let sendMixin = {};
if (typeof window !== 'undefined') {
sendMixin = require('vue-element-ui-x').sendMixin;
}
export default {
name: 'SendBasicDemo',
mixins: [sendMixin],
data() {
return {
basicResult: '',
};
},
mounted() {
if (typeof window !== 'undefined') {
// 初始化发送配置
this.initSend({
sendHandler: this.startFn,
finishHandler: this.finishBasicRequest,
abortHandler: this.abortBasicRequest,
onAbort: this.onBasicAbort,
});
}
},
methods: {
customSend() {
if (typeof window !== 'undefined') {
this.handleSend();
}
},
handleFinish() {
if (typeof window !== 'undefined') {
this.handleFinish();
}
},
handleAbort() {
if (typeof window !== 'undefined') {
this.handleAbort();
}
},
async startFn() {
// 在这里做一个异步操作,可以是发请求
console.log('开始模拟请求');
this.basicResult = '';
},
finishBasicRequest() {
this.basicResult = '请求成功完成!时间:' + new Date().toLocaleTimeString();
console.log('基础请求处理完成');
},
abortBasicRequest() {
this.basicResult = '请求被中止';
console.log('基础请求被中止', this.loading);
},
onBasicAbort() {
console.log('基础请求中止回调');
},
},
};
</script>
<style>
.container {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn-list {
display: flex;
gap: 12px;
}
.btn-list .el-button {
width: fit-content;
}
.result {
color: #67c23a;
padding: 8px;
background-color: #f0f9ff;
border-radius: 4px;
}
</style>
显示代码 复制代码 复制代码
有了对状态的控制,我们可以很方便的,自定义一些按钮的加载状态
点击任意按钮开始操作,按钮会自动切换到加载状态,几秒后自动结束:
当前状态:voice | 活跃按钮: 空闲
<template>
<div class="custom-btn-container">
<div class="btn-description">
<p>点击任意按钮开始操作,按钮会自动切换到加载状态,几秒后自动结束:</p>
</div>
<div class="btn-list">
<!-- 语音按钮 -->
<el-button
v-if="!voiceLoading"
style="background-color: #9145c8; border-color: #9145c8; color: white;"
circle
size="mini"
@click="startVoice"
>
<i class="el-icon-microphone"></i>
</el-button>
<el-button
v-if="voiceLoading"
style="background-color: #9145c8; border-color: #9145c8; color: white;"
circle
size="mini"
@click="stopVoice"
>
<i class="el-icon-loading"></i>
</el-button>
<!-- 发送按钮 -->
<el-button
v-if="!senderLoading"
style="background-color: transparent; border-color: #c2306a; color: #c2306a;"
round
size="mini"
@click="startSender"
>
<i class="el-icon-s-promotion"></i>
<span style="margin-left: 5px;">发送</span>
</el-button>
<el-button
v-if="senderLoading"
style="background-color: transparent; border-color: #c2306a; color: #c2306a;"
round
size="mini"
@click="stopSender"
>
<i class="el-icon-refresh el-icon-loading"></i>
<span style="margin-left: 5px;">发送中</span>
</el-button>
<!-- 播放按钮 -->
<el-button
v-if="!readLoading"
size="mini"
type="success"
style="background-color: #ff7f7f; border-color: #ff7f7f;"
@click="startRead"
>
<i
class="el-icon-video-play"
style="font-size: 20px; color: #fff;"
></i>
<span style="margin-left: 5px;">播放</span>
</el-button>
<el-button
v-if="readLoading"
size="mini"
type="success"
style="background-color: #ff7f7f; border-color: #ff7f7f;"
@click="stopRead"
>
<i
class="el-icon-video-pause"
style="font-size: 20px; color: #fff;"
></i>
<span style="margin-left: 5px;">播放中</span>
</el-button>
<!-- 录制按钮 -->
<el-button
v-if="!recordLoading"
size="mini"
circle
style="background-color: #fff884; border-color: #fff884; color: #104674;"
@click="startRecord"
>
<i class="el-icon-camera"></i>
</el-button>
<el-button
v-if="recordLoading"
size="mini"
circle
style="background-color: #fff884; border-color: #fff884; color: #104674;"
@click="stopRecord"
>
<i class="el-icon-aim el-icon-loading"></i>
</el-button>
</div>
<div class="status-info">
<p>
当前状态:{{ type }} | 活跃按钮:
<span
v-if="voiceLoading"
class="status-active"
>
语音录制中
</span>
<span
v-if="senderLoading"
class="status-active"
>
文本发送中
</span>
<span
v-if="readLoading"
class="status-active"
>
视频播放中
</span>
<span
v-if="recordLoading"
class="status-active"
>
视频录制中
</span>
<span
v-if="!voiceLoading && !senderLoading && !readLoading && !recordLoading"
class="status-idle"
>
空闲
</span>
</p>
</div>
</div>
</template>
<script>
export default {
name: 'CustomButtonDemo',
data() {
return {
type: 'voice',
// 多个独立的loading状态
voiceLoading: false,
senderLoading: false,
readLoading: false,
recordLoading: false,
// 存储定时器引用
timers: {},
};
},
methods: {
// 语音按钮操作
startVoice() {
this.type = 'voice';
this.voiceLoading = true;
this.$message.success('自定义语音按钮,开始录音!');
// 模拟3秒后自动结束
this.timers.voice = setTimeout(() => {
this.stopVoice();
}, 3000);
},
stopVoice() {
this.voiceLoading = false;
this.$message.info('自定义语音按钮,结束录音!');
if (this.timers.voice) {
clearTimeout(this.timers.voice);
delete this.timers.voice;
}
},
// 发送按钮操作
startSender() {
this.type = 'sender';
this.senderLoading = true;
this.$message.success('自定义发送按钮,开始发送文本!');
// 模拟2秒后自动结束
this.timers.sender = setTimeout(() => {
this.stopSender();
}, 2000);
},
stopSender() {
this.senderLoading = false;
this.$message.info('自定义发送按钮,结束发送!');
if (this.timers.sender) {
clearTimeout(this.timers.sender);
delete this.timers.sender;
}
},
// 播放按钮操作
startRead() {
this.type = 'read';
this.readLoading = true;
this.$message.success('自定义播放,开始播放啦!');
// 模拟4秒后自动结束
this.timers.read = setTimeout(() => {
this.stopRead();
}, 4000);
},
stopRead() {
this.readLoading = false;
this.$message.info('自定义播放按钮,结束播放!');
if (this.timers.read) {
clearTimeout(this.timers.read);
delete this.timers.read;
}
},
// 录制按钮操作
startRecord() {
this.type = 'record';
this.recordLoading = true;
this.$message.success('自定义录制,开始录制啦!');
// 模拟5秒后自动结束
this.timers.record = setTimeout(() => {
this.stopRecord();
}, 5000);
},
stopRecord() {
this.recordLoading = false;
this.$message.info('自定义录制按钮,结束录制!');
if (this.timers.record) {
clearTimeout(this.timers.record);
delete this.timers.record;
}
},
},
beforeDestroy() {
// 清理所有定时器
Object.values(this.timers).forEach(timer => {
if (timer) clearTimeout(timer);
});
},
};
</script>
<style>
.custom-btn-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fafafa;
}
.btn-description {
text-align: center;
}
.btn-description p {
margin: 0;
color: #606266;
font-size: 14px;
}
.btn-list {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.btn-list .el-button {
min-width: fit-content;
transition: all 0.3s ease;
}
.btn-list .el-button:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.status-info {
text-align: center;
padding-top: 10px;
border-top: 1px solid #e4e7ed;
}
.status-info p {
margin: 0;
color: #909399;
font-size: 13px;
}
.status-active {
color: #409eff;
font-weight: bold;
}
.status-idle {
color: #67c23a;
}
/* 图标动画效果 */
.el-icon-loading {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.btn-list {
gap: 15px;
}
.btn-list .el-button {
min-width: 80px;
}
}
</style>
显示代码 复制代码 复制代码
以上都是 前端加载状态 的控制,那一定少不了 请求状态 控制。接下来我们来介绍一下 工具类 XRequest 的简单用法
# XRequest 基础用法(sse)
XRequest 工具类支持的SSE请求方式
接收到的消息:
已断开
暂无消息
<template>
<div class="xrequest-container">
<div class="description">
<p>XRequest 工具类支持的SSE请求方式</p>
</div>
<div class="btn-list">
<el-button
type="primary"
:disabled="loading"
@click="startSSERequest"
>
{{ loading ? '请求中...' : '发起 SSE 请求' }}
</el-button>
<el-button
type="danger"
:disabled="!loading"
@click="abortRequest"
>
取消请求
</el-button>
<el-button
type="info"
@click="clearResult"
>
清空结果
</el-button>
</div>
<div class="result-container">
<div class="result-header">
<h4>接收到的消息:</h4>
<el-tag
v-if="loading"
type="success"
size="mini"
>
连接中
</el-tag>
<el-tag
v-else
type="info"
size="mini"
>
已断开
</el-tag>
</div>
<div class="result-content">
<div
v-if="!str"
class="empty-state"
>
暂无消息
</div>
<pre
v-else
class="message-display"
>
{{ str }}</pre
>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'XRequestDemo',
data() {
return {
str: '',
loading: false,
sse: null,
};
},
mounted() {
// 初始化 XRequest 实例
this.initXRequest();
},
methods: {
initXRequest() {
// 导入 XRequest 类
const { XRequest } = require('vue-element-ui-x');
// 创建 XRequest 实例
this.sse = new XRequest({
baseURL: 'https://testsse.element-ui-x.com',
type: 'sse', // 使用 SSE 模式
onMessage: msg => {
console.log('收到消息:', msg);
// 假设消息格式为 { data: string }
if (msg && msg.data) {
this.str += `
${msg.data}`;
} else {
this.str += `
${JSON.stringify(msg)}`;
}
},
onOpen: () => {
console.log('SSE 连接已打开');
this.$message.success('SSE 连接已建立');
this.loading = true;
},
onError: error => {
console.error('SSE 错误:', error);
this.loading = false;
},
onAbort: () => {
console.log('SSE 连接已中止');
this.loading = false;
},
onFinish: messages => {
console.log('SSE 连接已完成,共接收消息:', messages?.length || 0);
this.$message.success('连接已完成');
this.loading = false;
},
});
},
startSSERequest() {
if (this.loading) {
return;
}
// 清空之前的结果
this.str = '';
try {
// 发起 SSE 请求
this.sse.send('/api/sse');
this.$message.info('正在建立 SSE 连接...');
} catch (error) {
console.error('发起请求失败:', error);
this.$message.error('发起请求失败');
}
},
abortRequest() {
if (this.sse) {
this.sse.abort();
}
},
clearResult() {
this.str = '';
},
},
beforeDestroy() {
// 组件销毁时清理资源
if (this.sse) {
this.sse.abort();
this.sse = null;
}
},
};
</script>
<style>
.xrequest-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fafafa;
}
.description {
text-align: center;
}
.description p {
margin: 0;
color: #606266;
font-size: 14px;
}
.btn-list {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.result-container {
border: 1px solid #e4e7ed;
border-radius: 6px;
background-color: #fff;
overflow: hidden;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.result-header h4 {
margin: 0;
font-size: 14px;
color: #303133;
}
.result-content {
padding: 16px;
min-height: 120px;
max-height: 300px;
overflow-y: auto;
}
.empty-state {
color: #909399;
text-align: center;
font-style: italic;
line-height: 88px;
}
.message-display {
margin: 0;
padding: 12px;
background-color: #f9f9f9;
border: 1px solid #e4e7ed;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
color: #2c3e50;
}
/* 响应式设计 */
@media (max-width: 768px) {
.btn-list {
flex-direction: column;
gap: 8px;
}
.btn-list .el-button {
width: 100%;
}
.result-header {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
}
</style>
显示代码 复制代码 复制代码
# XRequest 基础用法(fetch)
XRequest 工具类的fetch请求方式
接收到的消息:
已断开
暂无消息
<template>
<div class="xrequest-container">
<div class="description">
<p>XRequest 工具类的fetch请求方式</p>
</div>
<div class="btn-list">
<el-button
type="primary"
:disabled="loading"
@click="startSSERequest"
>
{{ loading ? '请求中...' : '发起请求' }}
</el-button>
<el-button
type="danger"
:disabled="!loading"
@click="abortRequest"
>
取消请求
</el-button>
<el-button
type="info"
@click="clearResult"
>
清空结果
</el-button>
</div>
<div class="result-container">
<div class="result-header">
<h4>接收到的消息:</h4>
<el-tag
v-if="loading"
type="success"
size="mini"
>
连接中
</el-tag>
<el-tag
v-else
type="info"
size="mini"
>
已断开
</el-tag>
</div>
<div class="result-content">
<div
v-if="!str"
class="empty-state"
>
暂无消息
</div>
<pre
v-else
class="message-display"
>
{{ str }}</pre
>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'XRequestDemo',
data() {
return {
str: '',
loading: false,
sse: null,
};
},
mounted() {
// 初始化 XRequest 实例
this.initXRequest();
},
methods: {
initXRequest() {
// 导入 XRequest 类
const { XRequest } = require('vue-element-ui-x');
// 创建 XRequest 实例
this.sse = new XRequest({
baseURL: 'https://testsse.element-ui-x.com',
type: 'fetch', // 使用 fetch 模式
onMessage: msg => {
// 假设消息格式为 { data: string }
if (msg && msg.data) {
this.str += `
${msg.data}`;
} else {
this.str += `
${JSON.stringify(msg)}`;
}
},
onError: error => {
console.error('错误:', error);
this.$message.error('请求出现错误');
this.loading = false;
},
onAbort: () => {
console.log('请求已中止');
// this.$message.info('请求已中止');
this.loading = false;
},
onFinish: messages => {
console.log('请求已完成,共接收消息:', messages?.length || 0);
this.$message.success('请求已完成');
this.loading = false;
},
});
},
startSSERequest() {
if (this.loading) {
return;
}
// 清空之前的结果
this.str = '';
try {
// 发起 SSE 请求
this.sse.send('/api/sse');
this.$message.info('正在请求...');
this.loading = true;
} catch (error) {
console.error('发起请求失败:', error);
this.$message.error('发起请求失败');
}
},
abortRequest() {
if (this.sse) {
this.sse.abort();
}
},
clearResult() {
this.str = '';
},
},
beforeDestroy() {
// 组件销毁时清理资源
if (this.sse) {
this.sse.abort();
this.sse = null;
}
},
};
</script>
<style>
.xrequest-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
border: 1px solid #ebeef5;
border-radius: 8px;
background-color: #fafafa;
}
.description {
text-align: center;
}
.description p {
margin: 0;
color: #606266;
font-size: 14px;
}
.btn-list {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.result-container {
border: 1px solid #e4e7ed;
border-radius: 6px;
background-color: #fff;
overflow: hidden;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.result-header h4 {
margin: 0;
font-size: 14px;
color: #303133;
}
.result-content {
padding: 16px;
min-height: 120px;
max-height: 300px;
overflow-y: auto;
}
.empty-state {
color: #909399;
text-align: center;
font-style: italic;
line-height: 88px;
}
.message-display {
margin: 0;
padding: 12px;
background-color: #f9f9f9;
border: 1px solid #e4e7ed;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
color: #2c3e50;
}
/* 响应式设计 */
@media (max-width: 768px) {
.btn-list {
flex-direction: column;
gap: 8px;
}
.btn-list .el-button {
width: 100%;
}
.result-header {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
}
</style>
显示代码 复制代码 复制代码
# sendMixin 和 XRequest的混合使用
使用 sendMixin 对前端进行状态控制,使用 XRequest 对后端进行状态控制
暂无消息
<template>
<div class="btn-list">
<el-button
v-if="!loading"
style="background-color: #c2306a; border-color:#c2306a; color: white;"
round
plain
icon="el-icon-position"
size="large"
@click="customSend"
></el-button>
<el-button
v-if="loading"
style="background-color: #c2306a; border-color:#c2306a; color: white;"
round
plain
icon="el-icon-loading"
size="large"
@click="handleAbort"
></el-button>
</div>
<div class="result-content">
<div
v-if="!str"
class="empty-state"
>
暂无消息
</div>
<pre
v-else
class="message-display"
>
{{ str }}</pre
>
</div>
</template>
<script>
let sendMixin = {};
if (typeof window !== 'undefined') {
sendMixin = require('vue-element-ui-x').sendMixin;
}
export default {
name: 'XRequestDemo',
mixins: [sendMixin],
data() {
return {
str: '',
sse: null,
};
},
mounted() {
// 初始化 XRequest 实例
if (typeof window !== 'undefined') {
this.initXRequest();
this.initSend({
sendHandler: this.startFn,
finishHandler: this.finishBasicRequest,
abortHandler: this.abortBasicRequest,
onAbort: this.onBasicAbort,
});
}
},
methods: {
customSend() {
this.handleSend();
},
initXRequest() {
// 导入 XRequest 类
const { XRequest } = require('vue-element-ui-x');
// 创建 XRequest 实例
this.sse = new XRequest({
baseURL: 'https://testsse.element-ui-x.com',
type: 'fetch', // 使用 fetch 模式
onMessage: msg => {
console.log('收到消息:', msg);
if (msg && msg.data) {
this.str += `
${msg.data}`;
} else {
this.str += `
${JSON.stringify(msg)}`;
}
},
onError: error => {
console.error('错误:', error);
this.$message.error('请求出现错误');
},
onAbort: () => {},
onFinish: messages => {
this.handleFinish();
console.log('请求已完成,共接收消息:', messages?.length || 0);
},
});
},
finishBasicRequest() {
console.log('基础请求处理完成');
},
abortBasicRequest() {
if (this.sse) {
this.sse.abort();
}
console.log('基础请求被中止', this.loading);
},
onBasicAbort() {
console.log('基础请求中止回调');
},
startFn() {
this.str = '';
this.sse.send('/api/sse');
},
},
beforeDestroy() {
// 组件销毁时清理资源
if (this.sse) {
this.sse.abort();
this.sse = null;
}
},
};
</script>
显示代码 复制代码 复制代码
# 混入属性
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| loading | 请求加载状态 | Boolean | false |
| _sendPromise | 发送操作 Promise | Promise | null |
| _sendController | 发送控制器 | Object | null |
# 混入方法
| 方法名 | 说明 | 参数 | 返回值 |
|---|---|---|---|
| initSend | 初始化发送配置 | options: { onAbort, sendHandler, abortHandler, finishHandler } | - |
| handleSend | 执行发送操作 | ...args: 传递给 sendHandler 的参数 | - |
| handleAbort | 中止发送操作 | - | - |
| handleFinish | 完成发送操作 | - | - |
| createXRequest | 创建 XRequest 实例 | options: XRequest 配置选项 | XRequest |
# XRequest 类
# 构造函数选项
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| baseURL | 基础 URL | String | '' |
| type | 请求类型 (sse/fetch) | String | 'sse' |
| transformer | 数据转换器 | Function | - |
| baseOptions | 基础配置选项 | Object | {} |
| onAbort | 中止回调 | Function | - |
| onMessage | 消息回调 | Function | - |
| onError | 错误回调 | Function | - |
| onOpen | 连接打开回调 (SSE 模式) | Function | - |
| onFinish | 完成回调 | Function | - |
# XRequest 方法
| 方法名 | 说明 | 参数 | 返回值 |
|---|---|---|---|
| send | 发送请求 | url: 请求地址, options: 配置 | XRequest |
| abort | 中止请求 | - | - |
# 事件
混入本身不直接触发事件,但支持在 initSend 中配置回调函数:
| 回调函数 | 说明 | 回调参数 |
|---|---|---|
| onAbort | 请求中止时触发 | - |
| sendHandler | 发送处理器 | ...args: 发送参数 |
| abortHandler | 中止处理器 | - |
| finishHandler | 完成处理器 | - |
# 注意事项
- 该混入支持现代浏览器的 Fetch API 和 EventSource API,使用前请确保浏览器兼容性。
- SSE 模式适用于服务器推送场景,Fetch 模式适用于流式读取响应数据。
- 自定义
transformer函数可以对接收的数据进行预处理。 baseOptions配置仅在 SSE 模式下生效,用于 EventSource 构造函数。- Fetch 模式下的
options参数会传递给 fetch 函数,支持所有标准 fetch 选项。