一、为什么要封装自定义导出方法?
在基于若依框架二次开发的后台系统中,框架本身提供了一个 download() 方法用于导出文件。但随着业务增长,会遇到若干限制:
js
// 若依框架内置 download 方法(有局限)
export function download(url, params, filename) {
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }], // 写死 form 格式
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, // 写死 Content-Type
responseType: 'blob',
})
}
实际业务中经常碰到以下问题:
| 痛点 | 原因 |
|---|---|
| 后端接口要求传 JSON body | 内置方法写死了 form-urlencoded 格式 |
| 接口是 GET 请求 | 内置方法只能 POST |
| 需要携带额外请求头 | 无法自定义 headers |
| 使用了独立鉴权方式 | 无法覆盖默认 token 格式 |
为此,我们封装一个更灵活的 commonDownload 方法来解决这些问题。
二、完整实现代码
在 src/utils/request.js 中添加以下方法:
js
import axios from 'axios'
import { Loading, Message } from 'element-ui'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
import { blobValidate, tansParams } from '@/utils/ruoyi'
import errorCode from '@/utils/errorCode'
let downloadLoadingInstance
/**
* 自定义通用导出方法
* 相比框架内置的 download(),支持 GET/POST、JSON/form、自定义 headers
*
* @param {Object} options
* @param {string} options.url - 请求完整地址
* @param {string} options.requsetMethod - 请求方法:'get' | 'post'
* @param {Object} [options.params] - URL query 参数(GET 场景)
* @param {Object} [options.data] - 请求 body(POST JSON 场景)
* @param {boolean}[options.needTransformRequest] - true 时将 data 转 form 格式,默认 false
* @param {Object} [options.headers] - 自定义请求头,默认带 Bearer Token
* @param {string} options.filename - 保存的文件名,如 '订单列表.xlsx'
*/
export function commonDownload({
url,
requsetMethod,
params,
needTransformRequest,
data,
headers,
filename,
}) {
// 第一步:展示全屏 Loading,防止用户重复点击
downloadLoadingInstance = Loading.service({
text: '正在下载数据,请稍候',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
})
// 第二步:直接用原始 axios 发请求,绕开响应拦截器
return axios({
url,
method: requsetMethod,
params,
data,
// 按需将 data 转成 form-urlencoded 格式
transformRequest: needTransformRequest
? [(data) => tansParams(data)]
: undefined,
// 默认以 JSON + Bearer Token 发送,可通过 headers 参数完全覆盖
headers: headers || {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + getToken(),
},
responseType: 'blob', // 接收二进制流
timeout: 120000, // 超时时间 2 分钟
})
.then(async (res) => {
const blobData = res.data
// 第三步:校验是否为合法文件(防止把 JSON 错误响应当文件下载)
const isValidFile = await blobValidate(blobData)
if (isValidFile) {
// 合法文件:触发浏览器下载
const blob = new Blob([blobData])
saveAs(blob, filename)
} else {
// 非合法文件:解析错误信息并提示用户
const resText = await blobData.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg)
}
downloadLoadingInstance.close()
})
.catch((err) => {
console.error(err)
Message.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close()
})
}
三、全局挂载
在 src/main.js 中注册到 Vue 原型,所有组件无需单独引入:
js
// src/main.js
import { download, commonDownload } from '@/utils/request'
Vue.prototype.download = download // 保留框架原生方法
Vue.prototype.commonDownload = commonDownload // 新增自定义方法
四、使用示例:订单管理列表
下面以一个订单管理页面为例,演示完整用法。
4.1 页面结构
vue
<template>
<div class="order-page">
<!-- 筛选栏 -->
<div class="filter-bar">
<el-input
v-model="queryParams.orderNo"
placeholder="订单号"
clearable
style="width: 200px"
/>
<el-select v-model="queryParams.status" placeholder="订单状态" clearable>
<el-option label="待支付" :value="0" />
<el-option label="已支付" :value="1" />
<el-option label="已取消" :value="2" />
</el-select>
<el-date-picker
v-model="queryParams.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
/>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-download" @click="handleExport">导出</el-button>
</div>
<!-- 列表 -->
<el-table v-loading="loading" :data="orderList">
<el-table-column label="订单号" prop="orderNo" />
<el-table-column label="商品名称" prop="productName" />
<el-table-column label="金额(元)" prop="amount" />
<el-table-column label="下单时间" prop="createTime" />
<el-table-column label="状态" prop="statusLabel" />
</el-table>
<el-pagination
:current-page.sync="queryParams.pageNum"
:page-size.sync="queryParams.pageSize"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="loadList"
@current-change="loadList"
/>
</div>
</template>
4.2 逻辑部分
js
<script>
import { getOrderList } from '@/api/order'
export default {
name: 'OrderList',
data() {
return {
loading: false,
total: 0,
orderList: [],
queryParams: {
pageNum: 1,
pageSize: 10,
orderNo: '',
status: '', // 空字符串表示"全部状态"
dateRange: [],
},
}
},
created() {
this.loadList()
},
methods: {
async loadList() {
this.loading = true
try {
const { orderNo, status, dateRange, pageNum, pageSize } = this.queryParams
const res = await getOrderList({
orderNo: orderNo || undefined,
status: status !== '' ? status : undefined,
startDate: dateRange?.[0] || undefined,
endDate: dateRange?.[1] || undefined,
pageNum,
pageSize,
})
this.orderList = res.rows || []
this.total = res.total || 0
} finally {
this.loading = false
}
},
handleQuery() {
this.queryParams.pageNum = 1
this.loadList()
},
/** 导出当前筛选条件下的全量数据 */
handleExport() {
const { orderNo, status, dateRange } = this.queryParams
this.commonDownload({
// ① 后端导出接口地址(含 base URL)
url: process.env.VUE_APP_BASE_API + '/order/export',
// ② POST 请求,参数放在 body 里
requsetMethod: 'post',
// ③ 请求 body(只传有值的字段,undefined 会被 axios 自动忽略)
data: {
orderNo: orderNo || undefined,
// 状态值 0 是合法值,不能用 || undefined,需要严格判断
status: status !== '' ? status : undefined,
startDate: dateRange?.[0] || undefined,
endDate: dateRange?.[1] || undefined,
},
// ④ 文件名
filename: '订单列表.xlsx',
})
},
},
}
</script>
4.3 空值过滤的注意事项
data 里用了两种写法,原因如下:
js
// ✅ 字符串类型:空字符串用 || undefined 过滤即可
orderNo: orderNo || undefined
// ❌ 数字类型(含 0):不能用 || undefined,0 会被错误过滤掉
status: status || undefined // 当 status = 0(待支付)时,会变成 undefined!
// ✅ 数字类型:必须用严格判断
status: status !== '' ? status : undefined
五、参数速查表
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url |
string |
✅ | 请求完整地址(含 base URL) |
requsetMethod |
string |
✅ | 'get' 或 'post' |
filename |
string |
✅ | 下载文件名,如 '订单列表.xlsx' |
data |
object |
❌ | POST 请求 body(JSON 格式传参) |
params |
object |
❌ | URL 查询参数(GET 请求或附加参数) |
needTransformRequest |
boolean |
❌ | true 时把 data 转 form 格式,默认 false |
headers |
object |
❌ | 完全自定义请求头,默认附带 Bearer Token |
六、其他场景示例
场景一:GET 请求导出
js
this.commonDownload({
url: process.env.VUE_APP_BASE_API + '/report/monthly/export',
requsetMethod: 'get',
params: {
year: this.queryParams.year,
month: this.queryParams.month,
},
filename: '月度报表.xlsx',
})
场景二:兼容老接口(form-urlencoded 格式)
js
this.commonDownload({
url: process.env.VUE_APP_BASE_API + '/legacy/staff/export',
requsetMethod: 'post',
data: { deptId: this.deptId, status: 1 },
needTransformRequest: true, // 转成 key=val&key2=val2 格式
filename: '员工列表.xlsx',
})
场景三:带额外请求头(多租户系统)
js
this.commonDownload({
url: process.env.VUE_APP_BASE_API + '/tenant/invoice/export',
requsetMethod: 'post',
data: { startDate: '2024-01-01', endDate: '2024-12-31' },
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + getToken(),
'X-Tenant-Id': this.currentTenantId, // 额外的租户标识
},
filename: '发票记录.xlsx',
})
七、总结
commonDownload 方法的核心设计要点:
- 绕开响应拦截器 :直接用原始
axios,避免 blob 流被 JSON 解析器破坏 - 防御性校验 :
blobValidate识别"伪装成文件的错误响应",给用户正确提示 - 灵活参数设计:同时支持 GET/POST、JSON/form、自定义 headers,一个方法覆盖绝大多数导出场景
- 全局挂载 :注册到
Vue.prototype,业务组件直接this.commonDownload()调用 - 空值过滤:传参时区分字符串和数字类型,用正确的方式过滤空值