企业级文件下载工具函数优化方案
# 企业级文件下载工具函数优化方案
下面是一个经过全面优化的企业级文件下载工具函数,解决了文件名提取、编码处理、错误处理等关键问题:
```typescript
// src/utils/file/fileDownload.ts
import { apiClient } from 'src/services/axios'
import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { formatDate } from 'src/utils/date/dateUtils'
/**
* 企业级文件下载工具
*
* 功能特点:
* 1. 支持 RFC 5987 标准的文件名编码解析
* 2. 完善的错误处理与日志记录
* 3. 支持自定义文件名和默认文件名
* 4. 自动处理 Blob 和 URL 资源释放
* 5. 提供下载进度回调
* 6. 支持请求取消
*/
// 文件下载选项类型
export interface FileDownloadOptions {
/**
* 自定义文件名(可选)
*/
customFilename?: string;
/**
* 默认文件名前缀(可选)
*/
defaultFilenamePrefix?: string;
/**
* 文件扩展名(可选,默认为 'xlsx')
*/
fileExtension?: string;
/**
* 下载进度回调函数(可选)
* @param progress 下载进度 (0-100)
*/
onProgress?: (progress: number) => void;
/**
* 请求取消信号(可选)
*/
cancelToken?: AxiosRequestConfig['cancelToken'];
}
/**
* 从 Content-Disposition 头部提取文件名(支持 RFC 5987 标准)
* @param contentDisposition Content-Disposition 头部值
* @returns 解码后的文件名或 null
*/
function extractFilename(contentDisposition: string | null): string | null {
if (!contentDisposition) return null
// 优先处理 RFC 5987 编码的文件名 (filename*=)
const rfc5987Regex = /filename\*=(?:UTF-8|utf-8)''([^;]+)/i
const rfc5987Match = rfc5987Regex.exec(contentDisposition)
if (rfc5987Match && rfc5987Match[1]) {
try {
// 解码 RFC 5987 格式的文件名
return decodeURIComponent(rfc5987Match[1])
} catch (e) {
console.warn('RFC 5987 文件名解码失败,尝试备用方案', e)
}
}
// 回退到标准 filename 提取
const standardRegex = /filename=["']?([^;"']+)["']?/i
const standardMatch = standardRegex.exec(contentDisposition)
if (standardMatch && standardMatch[1]) {
// 移除可能的引号
return standardMatch[1].replace(/['"]/g, '')
}
return null
}
/**
* 处理文件下载响应
* @param response Axios 响应对象
* @param options 下载选项
*/
function handleFileResponse(response: AxiosResponse<Blob>, options: FileDownloadOptions): void {
const {
customFilename,
defaultFilenamePrefix = 'download',
fileExtension = 'xlsx'
} = options
// 1. 确定文件名
let filename = customFilename || ''
if (!filename) {
const contentDisposition = response.headers['content-disposition']
filename = extractFilename(contentDisposition) || ''
}
if (!filename) {
// 使用带时间戳的默认文件名
const now = new Date()
const beijingTime = new Date(now.getTime())
filename = `${defaultFilenamePrefix}_${formatDate(beijingTime, 'YYYYMMDD_HHmmss')}.${fileExtension}`
} else if (!filename.includes('.')) {
// 确保文件名有扩展名
filename = `${filename}.${fileExtension}`
}
// 2. 获取文件类型
const contentType = response.headers['content-type'] || 'application/octet-stream'
// 3. 创建 Blob 对象
const blob = new Blob([response.data], { type: contentType })
// 4. 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.style.display = 'none'
// 5. 添加并触发下载
document.body.appendChild(link)
link.click()
// 6. 清理资源(使用 RAF 确保可靠执行)
requestAnimationFrame(() => {
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
})
}
/**
* 企业级文件下载函数
* @param url 下载地址
* @param params 请求参数(POST 使用)
* @param options 下载选项
* @param config Axios 配置
*/
export async function downloadFile(
url: string,
params?: unknown,
options: FileDownloadOptions = {},
config: AxiosRequestConfig = {}
): Promise<void> {
try {
// 合并配置
const mergedConfig: AxiosRequestConfig = {
responseType: 'blob', // 必须设置为 blob
...config,
cancelToken: options.cancelToken || config.cancelToken,
}
// 添加进度回调支持
if (options.onProgress) {
mergedConfig.onDownloadProgress = (progressEvent) => {
if (progressEvent.total) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
options.onProgress?.(percent)
}
}
}
// 根据方法决定参数位置
const method = mergedConfig.method?.toUpperCase() || 'POST'
const request = method === 'GET'
? apiClient.get(url, mergedConfig)
: apiClient.post(url, params, mergedConfig)
const response = await request
handleFileResponse(response, options)
} catch (error) {
const axiosError = error as AxiosError
// 分类处理错误
if (axiosError.isAxiosError) {
if (axiosError.response) {
// 服务器响应错误 (4xx, 5xx)
console.error(`❌ 文件下载失败 [${axiosError.response.status}]: ${axiosError.message}`, {
url,
status: axiosError.response.status,
headers: axiosError.response.headers,
})
// 尝试从错误响应中提取信息
if (axiosError.response.data instanceof Blob) {
try {
const errorText = await new Response(axiosError.response.data).text()
console.error('服务器错误详情:', errorText.slice(0, 500))
} catch (e) {
console.error('无法解析错误响应:', e)
}
}
} else if (axiosError.request) {
// 请求已发出但无响应
console.error('❌ 文件下载请求无响应:', axiosError.message, {
url,
code: axiosError.code,
})
} else {
// 请求配置错误
console.error('❌ 文件下载配置错误:', axiosError.message, {
url,
config,
})
}
} else {
// 非 Axios 错误
console.error('❌ 文件下载发生意外错误:', error)
}
throw new Error('文件下载失败,请稍后重试或联系管理员')
}
}
/**
* 创建下载取消令牌
* @returns 取消令牌和取消函数
*/
export function createDownloadCancelToken() {
const source = apiClient.CancelToken.source()
return {
cancelToken: source.token,
cancel: source.cancel,
}
}
```
## 使用示例
### 基本使用
```typescript
import { downloadFile } from 'src/utils/file/fileDownload'
// 简单下载
await downloadFile('/api/reports/export')
// 带参数下载
await downloadFile(
'/api/reports/export',
{ year: 2023, type: 'audit' },
{
defaultFilenamePrefix: '年度审计报告',
fileExtension: 'pdf'
}
)
```
### 带进度显示和取消功能
```typescript
import { downloadFile, createDownloadCancelToken } from 'src/utils/file/fileDownload'
// 创建取消令牌
const { cancelToken, cancel } = createDownloadCancelToken()
// 带进度回调的下载
try {
await downloadFile(
'/api/large-file/download',
null,
{
onProgress: (percent) => {
console.log(`下载进度: ${percent}%`)
// 更新UI进度条
},
cancelToken
}
)
} catch (e) {
if (apiClient.isCancel(e)) {
console.log('下载已取消')
} else {
console.error('下载失败', e)
}
}
// 取消下载
// cancel('用户取消了下载')
```
### 自定义文件名
```typescript
// 强制使用自定义文件名
await downloadFile(
'/api/reports/export',
null,
{ customFilename: '2023年度财务报表.xlsx' }
)
```
## 功能亮点
1. **RFC 5987 标准支持**:
- 正确处理 `filename*=` 编码格式
- 自动解码 UTF-8 编码的文件名
2. **增强的错误处理**:
- 区分网络错误、服务器错误和配置错误
- 尝试解析错误响应的 Blob 内容
- 提供详细的错误日志
3. **下载控制**:
- 支持进度回调
- 提供请求取消功能
- 自动清理创建的 DOM 元素和 URL 对象
4. **文件名处理**:
- 支持自定义文件名
- 智能添加文件扩展名
- 默认文件名带时间戳避免冲突
5. **请求灵活性**:
- 支持 GET 和 POST 方法
- 可传递任意 Axios 配置
- 自动处理参数位置
6. **资源安全**:
- 使用 requestAnimationFrame 确保资源释放
- 隐藏创建的 DOM 元素
- 可靠的 Blob URL 回收机制
这个实现考虑了企业级应用的各种边界情况,提供了完善的错误处理和日志记录,同时保持接口简洁易用,适合在各种复杂场景下使用。