当前位置: 首页 > news >正文

【前端文件下载实现:多种表格导出方案的技术解析】

前端文件下载实现:多种表格导出方案的技术解析

背景介绍

在企业级应用中,数据导出是一个常见需求,特别是表格数据的导出。在我们的管理系统中,不仅需要支持用户数据的Excel导出,还需要处理多种格式的表格文件下载,如CSV、PDF和其他专有格式。本文将详细介绍我们是如何实现这些功能的,以及在实现过程中遇到的技术挑战和解决方案。

多种表格导出方案对比

在实现表格导出功能时,我们考虑了以下几种技术方案:

1. 前端生成表格文件

适用场景:数据量小,对格式要求不高,不需要复杂样式

实现方式

  • 使用js-xlsx、SheetJS等库在前端直接生成Excel文件
  • 使用PapaParse等库生成CSV文件
  • 使用jsPDF等库生成PDF文件

优点

  • 减轻服务器负担
  • 无需等待网络请求,响应速度快
  • 可离线使用

缺点

  • 大数据量时浏览器性能可能成为瓶颈
  • 复杂样式和格式支持有限
  • 客户端计算资源消耗大

2. 服务端生成文件,前端下载

适用场景:数据量大,需要复杂样式,需要应用业务逻辑

实现方式

  • XMLHttpRequest/Fetch + Blob(我们的主要方案)
  • 表单提交
  • iframe下载
  • a标签下载

优点

  • 可处理大数据量
  • 支持复杂样式和格式
  • 可应用服务端业务逻辑

缺点

  • 依赖网络请求
  • 服务器负担较重
  • 实现复杂度较高

3. 混合方案

适用场景:需要兼顾性能和功能的场景

实现方式

  • 小数据量时前端生成
  • 大数据量或复杂格式时服务端生成

优点

  • 灵活性高
  • 可根据具体需求选择最优方案

缺点

  • 实现和维护成本高
  • 需要前后端配合

实现细节:服务端生成文件,前端下载

我们主要采用服务端生成文件,前端下载的方案。下面详细介绍几种不同的实现方式。

1. XMLHttpRequest + Blob方式(主要方案)

这是我们在用户模块中采用的主要方案,适用于需要POST参数的场景:

export const exportUserFeedback = async (params: any = {}): Promise<void> => {try {const baseURL = `/${import.meta.env.VITE_API_XXX_BASEPATH}`;const url = `${baseURL}/xxxx/xx/exportProblemUserIssueList`;return new Promise<void>((resolve, reject) => {const xhr = new XMLHttpRequest();xhr.open('POST', url, true);xhr.setRequestHeader('Content-Type', 'application/json');xhr.responseType = 'blob'; // 设置响应类型为blobxhr.onload = function() {if (this.status === 200) {// 从响应头中获取文件名const contentDisposition = xhr.getResponseHeader('content-disposition') || '';let filename = `用户反馈_${new Date().getTime()}.xlsx`;// 尝试从content-disposition中提取文件名const filenameMatch = contentDisposition.match(/filename=([^;]+)/);if (filenameMatch && filenameMatch[1]) {try {filename = decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}}// 创建下载链接并触发下载const blob = this.response;const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);resolve();} else {reject(new Error(`导出失败: ${this.status}`));}};xhr.onerror = function() {reject(new Error('网络错误'));};xhr.send(JSON.stringify(params));});} catch (error) {console.error('导出文件失败:', error);throw error;}
};

2. Fetch API方式

对于支持现代浏览器的应用,可以使用更简洁的Fetch API:

export const exportWithFetch = async (params: any = {}): Promise<void> => {try {const baseURL = `/${import.meta.env.VITE_API_XXX_BASEPATH}`;const url = `${baseURL}/report/exportReportData`;const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(params)});if (!response.ok) {throw new Error(`导出失败: ${response.status}`);}// 获取文件名const contentDisposition = response.headers.get('content-disposition') || '';let filename = `报表数据_${new Date().getTime()}.xlsx`;const filenameMatch = contentDisposition.match(/filename=([^;]+)/);if (filenameMatch && filenameMatch[1]) {try {filename = decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}}// 获取blob数据并下载const blob = await response.blob();const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);} catch (error) {console.error('导出文件失败:', error);throw error;}
};

3. 表单提交方式

对于简单的GET请求或需要兼容旧浏览器的场景,可以使用表单提交方式:

export const exportWithForm = (params: any = {}): void => {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/statistics/exportStatisticsData`;// 创建一个隐藏的表单const form = document.createElement('form');form.method = 'POST';form.action = url;form.style.display = 'none';// 添加参数Object.entries(params).forEach(([key, value]) => {if (value !== undefined && value !== null) {const input = document.createElement('input');input.type = 'hidden';input.name = key;input.value = String(value);form.appendChild(input);}});// 提交表单document.body.appendChild(form);form.submit();// 清理setTimeout(() => {document.body.removeChild(form);}, 100);
};

4. iframe方式

对于需要在后台下载且不影响当前页面的场景,可以使用iframe方式:

export const exportWithIframe = (params: any = {}): void => {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/analysis/exportAnalysisData`;// 创建一个隐藏的iframeconst iframe = document.createElement('iframe');iframe.style.display = 'none';document.body.appendChild(iframe);// 创建一个表单const form = document.createElement('form');form.method = 'POST';form.action = url;form.target = iframe.name = `download_iframe_${Date.now()}`;// 添加参数Object.entries(params).forEach(([key, value]) => {if (value !== undefined && value !== null) {const input = document.createElement('input');input.type = 'hidden';input.name = key;input.value = String(value);form.appendChild(input);}});// 提交表单document.body.appendChild(form);form.submit();// 清理setTimeout(() => {document.body.removeChild(form);document.body.removeChild(iframe);}, 5000); // 给足够的时间下载
};

不同类型表格文件的响应头处理

不同类型的表格文件有不同的Content-Type和处理方式,下面我们详细介绍几种常见类型。

1. Excel文件 (XLSX/XLS)

响应头示例

HTTP/1.1 200 OK
content-type: application/vnd.ms-excel;charset=gb2312
content-disposition: attachment;filename=%E7%94%A8%E6%88%B7%E5%8F%8D%E9%A6%88.xlsx

处理方式

  • 使用xhr.responseType = 'blob'接收二进制数据
  • 从Content-Disposition中提取文件名
  • 使用URL.createObjectURL创建下载链接

2. CSV文件

响应头示例

HTTP/1.1 200 OK
content-type: text/csv;charset=utf-8
content-disposition: attachment;filename=data.csv

处理方式

  • CSV文件可以作为文本或二进制处理
  • 如果作为文本处理,需要注意字符编码问题
  • 中文CSV文件可能需要添加BOM头(\uFEFF)以正确显示中文
export const exportCSV = async (params: any = {}): Promise<void> => {try {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/data/exportCSV`;const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(params)});if (!response.ok) {throw new Error(`导出失败: ${response.status}`);}// 获取文件名const contentDisposition = response.headers.get('content-disposition') || '';let filename = `数据_${new Date().getTime()}.csv`;const filenameMatch = contentDisposition.match(/filename=([^;]+)/);if (filenameMatch && filenameMatch[1]) {try {filename = decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}}// 对于CSV,可以选择文本处理或二进制处理// 这里使用二进制处理,与Excel保持一致const blob = await response.blob();const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);} catch (error) {console.error('导出CSV失败:', error);throw error;}
};

3. PDF文件

响应头示例

HTTP/1.1 200 OK
content-type: application/pdf
content-disposition: attachment;filename=report.pdf

处理方式

  • PDF文件处理与Excel类似,都使用blob方式
  • 可以选择直接在浏览器中打开,而不是下载
export const exportPDF = async (params: any = {}): Promise<void> => {try {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/report/exportPDF`;const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(params)});if (!response.ok) {throw new Error(`导出失败: ${response.status}`);}// 获取文件名const contentDisposition = response.headers.get('content-disposition') || '';let filename = `报告_${new Date().getTime()}.pdf`;const filenameMatch = contentDisposition.match(/filename=([^;]+)/);if (filenameMatch && filenameMatch[1]) {try {filename = decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}}const blob = await response.blob();// 选项1:下载文件const downloadUrl = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = downloadUrl;link.download = filename;document.body.appendChild(link);link.click();// 选项2:在新窗口打开PDF(取消注释以启用)// const viewUrl = window.URL.createObjectURL(blob);// window.open(viewUrl, '_blank');// 清理setTimeout(() => {window.URL.revokeObjectURL(downloadUrl);document.body.removeChild(link);}, 100);} catch (error) {console.error('导出PDF失败:', error);throw error;}
};

4. 特殊格式:带有自定义响应头的Excel文件

有些后端框架或服务器配置可能会使用非标准的响应头,例如:

响应头示例

HTTP/1.1 200 OK
content-type: application/octet-stream
x-suggested-filename: 统计报表.xlsx
content-disposition: inline

处理方式

  • 需要检查多个可能的响应头
  • 提供更健壮的文件名提取逻辑
export const exportSpecialExcel = async (params: any = {}): Promise<void> => {try {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/special/exportExcel`;const xhr = new XMLHttpRequest();xhr.open('POST', url, true);xhr.setRequestHeader('Content-Type', 'application/json');xhr.responseType = 'blob';return new Promise<void>((resolve, reject) => {xhr.onload = function() {if (this.status === 200) {// 尝试从多个可能的响应头中获取文件名let filename = `数据_${new Date().getTime()}.xlsx`;// 1. 尝试标准的Content-Dispositionconst contentDisposition = xhr.getResponseHeader('content-disposition') || '';let filenameMatch = contentDisposition.match(/filename=([^;]+)/);// 2. 尝试自定义的X-Suggested-Filenameif (!filenameMatch) {const suggestedFilename = xhr.getResponseHeader('x-suggested-filename');if (suggestedFilename) {filenameMatch = [null, suggestedFilename];}}// 3. 尝试Content-Disposition中的filename*=UTF-8''格式if (!filenameMatch) {const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);if (filenameStarMatch) {filenameMatch = filenameStarMatch;}}if (filenameMatch && filenameMatch[1]) {try {filename = decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}}// 创建下载链接并触发下载const blob = this.response;const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);resolve();} else {reject(new Error(`导出失败: ${this.status}`));}};xhr.onerror = function() {reject(new Error('网络错误'));};xhr.send(JSON.stringify(params));});} catch (error) {console.error('导出文件失败:', error);throw error;}
};

5. 流式下载大文件

对于特别大的表格文件,可以考虑使用流式下载:

export const exportLargeFile = async (params: any = {}): Promise<void> => {try {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/data/exportLargeFile`;// 使用fetch的流式APIconst response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(params)});if (!response.ok) {throw new Error(`导出失败: ${response.status}`);}// 获取文件名const contentDisposition = response.headers.get('content-disposition') || '';let filename = `大文件_${new Date().getTime()}.xlsx`;const filenameMatch = contentDisposition.match(/filename=([^;]+)/);if (filenameMatch && filenameMatch[1]) {try {filename = decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}}// 获取reader和流const reader = response.body?.getReader();if (!reader) {throw new Error('浏览器不支持流式下载');}// 创建一个新的ReadableStreamconst stream = new ReadableStream({start(controller) {function push() {reader.read().then(({ done, value }) => {if (done) {controller.close();return;}controller.enqueue(value);push();}).catch(error => {console.error('流读取错误', error);controller.error(error);});}push();}});// 创建响应对象const newResponse = new Response(stream);// 获取blob并下载const blob = await newResponse.blob();const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);} catch (error) {console.error('导出大文件失败:', error);throw error;}
};

前端生成表格文件的方案

除了服务端生成文件外,有时我们也需要在前端直接生成表格文件。

1. 使用SheetJS生成Excel

import * as XLSX from 'xlsx';export const generateExcel = (data: any[], sheetName = 'Sheet1', fileName = '数据导出.xlsx'): void => {// 创建工作簿const wb = XLSX.utils.book_new();// 创建工作表const ws = XLSX.utils.json_to_sheet(data);// 将工作表添加到工作簿XLSX.utils.book_append_sheet(wb, ws, sheetName);// 生成Excel文件并下载XLSX.writeFile(wb, fileName);
};

2. 使用PapaParse生成CSV

import Papa from 'papaparse';export const generateCSV = (data: any[], fileName = '数据导出.csv'): void => {// 将数据转换为CSV字符串const csv = Papa.unparse(data);// 添加BOM头以支持中文const csvContent = "\uFEFF" + csv;// 创建Blob对象const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });// 创建下载链接并触发下载const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = fileName;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);
};

3. 使用jsPDF生成PDF表格

import jsPDF from 'jspdf';
import 'jspdf-autotable';export const generatePDF = (data: any[], columns: any[], fileName = '数据导出.pdf'): void => {// 创建PDF文档const doc = new jsPDF();// 添加表格doc.autoTable({head: [columns.map(col => col.title)],body: data.map(item => columns.map(col => item[col.dataIndex])),startY: 20,styles: { fontSize: 10, cellPadding: 2 },headStyles: { fillColor: [41, 128, 185], textColor: 255 }});// 添加标题doc.text('数据报表', 14, 15);// 保存PDF文件doc.save(fileName);
};

响应头处理中的挑战与解决方案

1. 中文文件名编码问题

不同的浏览器和服务器对中文文件名的处理方式不同,可能会导致乱码。

常见编码方式

  • URL编码:%E7%94%A8%E6%88%B7%E5%8F%8D%E9%A6%88.xlsx
  • Base64编码:=?UTF-8?B?5oqA5pyv5pWZ6IKy5pyN5Yqh5ZGY?=.xlsx
  • RFC 5987编码:filename*=UTF-8''%E7%94%A8%E6%88%B7%E5%8F%8D%E9%A6%88.xlsx

解决方案

  • 检查多种可能的编码格式
  • 提供默认文件名作为后备方案
  • 使用try-catch包装解码逻辑
function extractFilename(headers: Headers): string {const contentDisposition = headers.get('content-disposition') || '';let filename = `数据_${new Date().getTime()}.xlsx`;// 尝试标准的filename参数let match = contentDisposition.match(/filename=([^;]+)/);if (match && match[1]) {try {return decodeURIComponent(match[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码filename', e);}}// 尝试RFC 5987格式match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);if (match && match[1]) {try {return decodeURIComponent(match[1]);} catch (e) {console.warn('无法解码filename*', e);}}// 尝试Base64编码match = contentDisposition.match(/=\?UTF-8\?B\?([^?]+)\?=/);if (match && match[1]) {try {return atob(match[1]);} catch (e) {console.warn('无法解码Base64文件名', e);}}return filename;
}

2. 不同浏览器的兼容性问题

不同浏览器对下载API和响应头的处理有差异。

解决方案

  • 使用特性检测而不是浏览器检测
  • 提供多种下载方式的回退机制
  • 针对特定浏览器添加特殊处理
function downloadFile(blob: Blob, filename: string): void {// 方法1: 使用a标签下载(现代浏览器)if ('download' in document.createElement('a')) {const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);return;}// 方法2: 使用msSaveBlob(IE10+)if (window.navigator && window.navigator.msSaveBlob) {window.navigator.msSaveBlob(blob, filename);return;}// 方法3: 使用FileReader和data URL(旧浏览器)const reader = new FileReader();reader.onload = function() {const url = reader.result as string;const iframe = document.createElement('iframe');iframe.style.display = 'none';iframe.src = url;document.body.appendChild(iframe);setTimeout(() => {document.body.removeChild(iframe);}, 100);};reader.readAsDataURL(blob);
}

3. 大文件处理

对于特别大的表格文件,直接在内存中处理可能会导致性能问题。

解决方案

  • 使用流式下载
  • 分块处理
  • 添加下载进度提示

3. 大文件处理

export const downloadWithProgress = async (url: string, filename: string): Promise<void> => {// 创建进度条元素const progressContainer = document.createElement('div');progressContainer.style.position = 'fixed';progressContainer.style.top = '10px';progressContainer.style.right = '10px';progressContainer.style.padding = '10px';progressContainer.style.background = '#fff';progressContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';progressContainer.style.borderRadius = '4px';progressContainer.style.zIndex = '9999';const progressText = document.createElement('div');progressText.textContent = '准备下载...';progressContainer.appendChild(progressText);const progressBar = document.createElement('div');progressBar.style.height = '5px';progressBar.style.width = '200px';progressBar.style.background = '#eee';progressBar.style.marginTop = '5px';progressContainer.appendChild(progressBar);const progressInner = document.createElement('div');progressInner.style.height = '100%';progressInner.style.width = '0%';progressInner.style.background = '#4caf50';progressBar.appendChild(progressInner);document.body.appendChild(progressContainer);try {// 获取文件大小const headResponse = await fetch(url, { method: 'HEAD' });const contentLength = Number(headResponse.headers.get('content-length') || '0');// 创建请求const response = await fetch(url);if (!response.ok) {throw new Error(`下载失败: ${response.status}`);}// 获取reader和流const reader = response.body?.getReader();if (!reader) {throw new Error('浏览器不支持流式下载');}// 已接收的字节数let receivedBytes = 0;// 创建一个新的ReadableStreamconst stream = new ReadableStream({start(controller) {function push() {reader.read().then(({ done, value }) => {if (done) {controller.close();return;}// 更新进度receivedBytes += value.length;const progress = contentLength ? Math.round((receivedBytes / contentLength) * 100) : 0;progressInner.style.width = `${progress}%`;progressText.textContent = `下载中... ${progress}%`;controller.enqueue(value);push();}).catch(error => {console.error('流读取错误', error);controller.error(error);});}push();}});// 创建响应对象const newResponse = new Response(stream);// 获取blob并下载const blob = await newResponse.blob();const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();// 更新进度提示progressText.textContent = '下载完成';progressInner.style.width = '100%';// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);document.body.removeChild(progressContainer);}, 2000);} catch (error) {console.error('下载失败:', error);progressText.textContent = `下载失败: ${error.message}`;progressInner.style.background = '#f44336';// 清理setTimeout(() => {document.body.removeChild(progressContainer);}, 3000);throw error;}
};

2. 分页下载

对于特别大的数据集,可以考虑分页下载:

export const downloadByChunks = async (params: any = {}, totalPages: number): Promise<void> => {// 创建进度提示const progressContainer = document.createElement('div');progressContainer.style.position = 'fixed';progressContainer.style.top = '10px';progressContainer.style.right = '10px';progressContainer.style.padding = '10px';progressContainer.style.background = '#fff';progressContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';progressContainer.style.borderRadius = '4px';progressContainer.style.zIndex = '9999';const progressText = document.createElement('div');progressText.textContent = '准备下载...';progressContainer.appendChild(progressText);document.body.appendChild(progressContainer);try {// 创建一个工作簿const wb = XLSX.utils.book_new();// 逐页下载数据for (let page = 1; page <= totalPages; page++) {progressText.textContent = `下载中... ${Math.round((page / totalPages) * 100)}%`;// 获取当前页数据const pageParams = { ...params, page, pageSize: 1000 };const data = await fetchPageData(pageParams);// 将数据添加到工作表if (page === 1) {// 创建新工作表const ws = XLSX.utils.json_to_sheet(data);XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');} else {// 追加到现有工作表const ws = wb.Sheets['Sheet1'];XLSX.utils.sheet_add_json(ws, data, { skipHeader: true, origin: -1 });}}// 生成Excel文件并下载XLSX.writeFile(wb, `数据导出_${new Date().getTime()}.xlsx`);// 更新进度提示progressText.textContent = '下载完成';// 清理setTimeout(() => {document.body.removeChild(progressContainer);}, 2000);} catch (error) {console.error('下载失败:', error);progressText.textContent = `下载失败: ${error.message}`;// 清理setTimeout(() => {document.body.removeChild(progressContainer);}, 3000);throw error;}
};// 获取分页数据的辅助函数
async function fetchPageData(params: any): Promise<any[]> {const baseURL = `/${import.meta.env.VITE_API_SCCNP_BASEPATH}`;const url = `${baseURL}/data/getPageData`;const response = await fetch(url, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(params)});if (!response.ok) {throw new Error(`获取数据失败: ${response.status}`);}const result = await response.json();return result.data || [];
}

4. 响应头获取限制

由于安全原因,浏览器限制了JavaScript可以访问的响应头。只有某些"安全"的头部(如Content-Type)默认可访问,而其他头部(如Content-Disposition)可能需要服务器通过Access-Control-Expose-Headers显式允许。

解决方案

  • 确保服务器配置了正确的CORS头部
  • 使用后端代理转发请求
  • 在无法获取响应头的情况下提供替代方案
// 服务器端配置示例(Node.js + Express)
app.use((req, res, next) => {res.header('Access-Control-Allow-Origin', '*');res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');res.header('Access-Control-Expose-Headers', 'Content-Disposition, Content-Length');next();
});// 前端处理示例
export const safeGetFilename = (xhr: XMLHttpRequest, defaultName: string): string => {try {const contentDisposition = xhr.getResponseHeader('content-disposition');if (!contentDisposition) {console.warn('无法获取Content-Disposition头部,可能需要配置Access-Control-Expose-Headers');return defaultName;}const filenameMatch = contentDisposition.match(/filename=([^;]+)/);if (filenameMatch && filenameMatch[1]) {return decodeURIComponent(filenameMatch[1].replace(/\"/g, ''));}} catch (e) {console.warn('获取文件名失败', e);}return defaultName;
};

如果无法修改服务器配置,可以考虑以下替代方案:

// 前端处理示例:使用默认文件名
export const downloadWithDefaultFilename = async (url, defaultFilename) => {try {const response = await fetch(url);if (!response.ok) {throw new Error(`下载失败: ${response.status}`);}// 尝试获取Content-Disposition,如果无法获取则使用默认文件名let filename = defaultFilename;try {const contentDisposition = response.headers.get('content-disposition');if (contentDisposition) {const match = contentDisposition.match(/filename=([^;]+)/);if (match && match[1]) {filename = decodeURIComponent(match[1].replace(/\"/g, ''));}}} catch (e) {console.warn('无法获取文件名,使用默认文件名', e);}const blob = await response.blob();const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);} catch (error) {console.error('下载失败:', error);throw error;}
};

4. 处理不同的Content-Type

不同的Content-Type可能需要不同的处理方式,特别是对于非标准的Content-Type。

解决方案

  • 根据Content-Type选择不同的处理方式
  • 对于未知的Content-Type,使用通用的二进制处理方式
export const downloadByContentType = async (url) => {try {const response = await fetch(url);if (!response.ok) {throw new Error(`下载失败: ${response.status}`);}// 获取Content-Typeconst contentType = response.headers.get('content-type') || '';// 获取文件名let filename = getFilenameFromResponse(response);// 根据Content-Type选择处理方式if (contentType.includes('text/')) {// 文本文件处理const text = await response.text();const blob = new Blob([text], { type: contentType });downloadBlob(blob, filename);} else if (contentType.includes('application/json')) {// JSON文件处理const json = await response.json();const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });downloadBlob(blob, filename);} else {// 二进制文件处理const blob = await response.blob();downloadBlob(blob, filename);}} catch (error) {console.error('下载失败:', error);throw error;}
};// 辅助函数:从响应中获取文件名
function getFilenameFromResponse(response) {const contentDisposition = response.headers.get('content-disposition') || '';let filename = `文件_${new Date().getTime()}`;// 尝试从Content-Disposition中提取文件名const match = contentDisposition.match(/filename=([^;]+)/);if (match && match[1]) {try {filename = decodeURIComponent(match[1].replace(/\"/g, ''));} catch (e) {console.warn('无法解码文件名', e);}} else {// 尝试从URL中提取文件名const url = response.url;const urlParts = url.split('/');const urlFilename = urlParts[urlParts.length - 1].split('?')[0];if (urlFilename) {filename = urlFilename;}}return filename;
}// 辅助函数:下载Blob
function downloadBlob(blob, filename) {const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = filename;document.body.appendChild(link);link.click();setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);
}

特殊场景处理

1. 处理带有水印的Excel文件

某些业务场景需要在导出的Excel文件中添加水印,这通常需要服务端支持。但在某些情况下,我们也可以在前端处理:

import * as XLSX from 'xlsx';export const addWatermarkToExcel = async (blob: Blob, watermarkText: string): Promise<Blob> => {// 将blob转换为ArrayBufferconst arrayBuffer = await blob.arrayBuffer();// 读取Excel文件const workbook = XLSX.read(arrayBuffer, { type: 'array' });// 遍历所有工作表for (const sheetName of workbook.SheetNames) {const worksheet = workbook.Sheets[sheetName];// 添加水印(这需要使用更复杂的Excel操作库,如exceljs)// 这里只是一个简化示例,实际实现可能需要使用其他库if (!worksheet['!comments']) {worksheet['!comments'] = [];}// 在A1单元格添加注释作为简单的"水印"worksheet['!comments'].push({r: 0, c: 0,a: { t: watermarkText }});}// 将修改后的工作簿写回ArrayBufferconst newArrayBuffer = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' });// 创建新的Blobreturn new Blob([newArrayBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
};

2. 处理加密的Excel文件

某些敏感数据可能需要加密保护:

import * as XLSX from 'xlsx';export const createEncryptedExcel = (data: any[], password: string, fileName: string): void => {// 创建工作簿const wb = XLSX.utils.book_new();// 创建工作表const ws = XLSX.utils.json_to_sheet(data);// 将工作表添加到工作簿XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');// 生成加密的Excel文件const wbout = XLSX.write(wb, { type: 'array', bookType: 'xlsx', password });// 创建Blob并下载const blob = new Blob([wbout], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });const url = window.URL.createObjectURL(blob);const link = document.createElement('a');link.href = url;link.download = fileName;document.body.appendChild(link);link.click();// 清理setTimeout(() => {window.URL.revokeObjectURL(url);document.body.removeChild(link);}, 100);
};

3. 处理多种格式的导出选项

有时我们需要提供多种格式的导出选项,让用户自行选择:

import * as XLSX from 'xlsx';export const exportDataWithOptions = (data: any[], fileName: string): void => {// 创建下拉菜单const menu = document.createElement('div');menu.style.position = 'fixed';menu.style.top = '50%';menu.style.left = '50%';menu.style.transform = 'translate(-50%, -50%)';menu.style.background = '#fff';menu.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';menu.style.borderRadius = '4px';menu.style.padding = '20px';menu.style.zIndex = '9999';const title = document.createElement('h3');title.textContent = '选择导出格式';title.style.margin = '0 0 15px 0';menu.appendChild(title);// 创建选项const formats = [{ label: 'Excel (.xlsx)', value: 'xlsx' },{ label: 'Excel 97-2003 (.xls)', value: 'xls' },{ label: 'CSV (.csv)', value: 'csv' },{ label: 'HTML (.html)', value: 'html' },{ label: 'JSON (.json)', value: 'json' }];formats.forEach(format => {const button = document.createElement('button');button.textContent = format.label;button.style.display = 'block';button.style.width = '100%';button.style.padding = '8px';button.style.margin = '5px 0';button.style.border = '1px solid #ddd';button.style.borderRadius = '4px';button.style.background = '#f5f5f5';button.style.cursor = 'pointer';button.addEventListener('click', () => {// 创建工作簿const wb = XLSX.utils.book_new();// 创建工作表const ws = XLSX.utils.json_to_sheet(data);// 将工作表添加到工作簿XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');// 根据选择的格式导出XLSX.writeFile(wb, `${fileName}.${format.value}`);// 关闭菜单document.body.removeChild(menu);});menu.appendChild(button);});// 添加取消按钮const cancelButton = document.createElement('button');cancelButton.textContent = '取消';cancelButton.style.display = 'block';cancelButton.style.width = '100%';cancelButton.style.padding = '8px';cancelButton.style.margin = '15px 0 5px 0';cancelButton.style.border = '1px solid #ddd';cancelButton.style.borderRadius = '4px';cancelButton.style.background = '#fff';cancelButton.style.cursor = 'pointer';cancelButton.addEventListener('click', () => {document.body.removeChild(menu);});menu.appendChild(cancelButton);// 显示菜单document.body.appendChild(menu);
};

最佳实践总结

基于我们的实践经验,处理表格文件下载时,建议遵循以下最佳实践:

1. 响应头处理

  • 总是检查Content-Disposition头部:这是获取正确文件名的关键
  • 提供默认文件名:作为Content-Disposition不存在或解析失败时的后备方案
  • 正确处理编码:使用decodeURIComponent解码URL编码的文件名
  • 添加错误处理:捕获并处理解码过程中可能出现的异常
  • 考虑浏览器兼容性:处理不同浏览器对响应头的解析差异

2. 下载方式选择

  • 小文件或简单格式:可以考虑前端生成
  • 大文件或复杂格式:优先使用服务端生成
  • 需要应用业务逻辑的场景:使用服务端生成
  • 离线场景:使用前端生成并本地保存

3. 用户体验优化

  • 提供下载进度提示:特别是对于大文件
  • 添加成功/失败反馈:通过消息提示告知用户下载状态
  • 提供多种格式选择:让用户根据需要选择合适的格式
  • 添加文件预览选项:在某些场景下允许用户在下载前预览

4. 安全考虑

  • 验证文件内容:确保下载的是预期的文件类型
  • 限制下载大小:防止恶意大文件攻击
  • 添加权限控制:确保只有授权用户可以下载敏感数据
  • 考虑加密保护:对敏感数据进行加密

总结

通过本文的详细介绍,我们可以看到前端处理表格文件下载有多种方案,每种方案都有其适用场景和优缺点。在实际项目中,我们需要根据具体需求选择合适的方案,并注意处理各种边缘情况和异常情况。

HTTP响应头在文件下载过程中扮演着至关重要的角色。正确理解和处理Content-Type、Content-Disposition、CORS相关头部、缓存控制头部和安全相关头部,是实现可靠文件下载功能的关键。

无论是使用XMLHttpRequest、Fetch API还是前端库生成文件,正确处理HTTP响应头、文件名编码和浏览器兼容性都是实现可靠文件下载功能的关键。同时,良好的用户体验和适当的安全措施也是不可忽视的重要因素。

希望这篇文章能为大家提供一些实用的参考和思路,帮助大家在项目中实现更加完善的表格文件下载功能。

相关文章:

【前端文件下载实现:多种表格导出方案的技术解析】

前端文件下载实现&#xff1a;多种表格导出方案的技术解析 背景介绍 在企业级应用中&#xff0c;数据导出是一个常见需求&#xff0c;特别是表格数据的导出。在我们的管理系统中&#xff0c;不仅需要支持用户数据的Excel导出&#xff0c;还需要处理多种格式的表格文件下载&am…...

933. 最近的请求次数

最近的请求次数 题目描述尝试做法推荐做法 题目描述 写一个 RecentCounter 类来计算特定时间范围内最近的请求。 请你实现 RecentCounter 类&#xff1a; RecentCounter() 初始化计数器&#xff0c;请求数为 0 。 int ping(int t) 在时间 t 添加一个新请求&#xff0c;其中 …...

[C++Qt] 槽函数收不到信号问题(信号的注册)

&#x1f4e2;博客主页&#xff1a;https://loewen.blog.csdn.net&#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;本文由 丶布布原创&#xff0c;首发于 CSDN&#xff0c;转载注明出处&#x1f649;&#x1f4e2;现…...

吴恩达机器学习笔记复盘(三)Jupyter NoteBook

Jupyter NoteBook Jupyter是一个开源的交互式计算环境&#xff1a; 特点 交互式编程&#xff1a;支持以单元格为单位编写和运行代码&#xff0c;用户可以实时看到代码的执行结果&#xff0c;便于逐步调试和理解代码逻辑。多语言支持&#xff1a;不仅支持Python&#xff0c;还…...

面向对象Demo02

方法的调用的回顾 package oop; ​ public class Demo02 {//静态public static void main(String[] args) {//student student new student();student.speak();//非静态 //student student new student();} } package oop; ​ public class student {public static void sp…...

Quitzilla 学习版(安卓):戒掉坏习惯的高效助手

Quitzilla 是一款源自国外的日程管理工具&#xff0c;专为帮助用户戒除不良习惯而设计。它通过强大的习惯追踪功能和丰富的激励机制&#xff0c;让用户能够清晰地看到自己的进步&#xff0c;并逐步克服成瘾行为。这款软件的前身是一款戒烟应用&#xff0c;但经过改进后&#xf…...

AWB-illuminant_weight_vector

1.概念 光源权重向量 AWB 处理通过求取R/G-B/G 空间中灰色统计数据位置的平均值来估计光源, 此估计基于灰度世界假设. 因此, 输入统计数据中真实的灰色统计数据很重要. 通用环境下,灰色统计数据的平均值会遭到灰色区中的非灰色统计数据污染. 因此,估计结果的精确性依赖与选择灰…...

微信小程序面试内容整理-数据绑定

在微信小程序中,数据绑定是将 JavaScript 中的数据与 WXML 中的视图进行关联和同步的过程。它使得视图可以根据数据的变化自动更新,极大地简化了前端开发的复杂度。 数据绑定有两种主要类型: 1. 单向数据绑定:数据从 Java器更新到视图。...

蓝桥每日打卡--背包问题

#蓝桥#JAVA#背包问题 题目描述 有一个背包&#xff0c;它的容量为 W 4。有 3 个物品&#xff0c;它们的重量分别为 [2, 1, 3]&#xff0c;价值分别为 [4, 2, 3]。问在不超过背包容量的情况下&#xff0c;能装入背包的物品的最大价值是多少&#xff1f; 解题思路 动态规划的…...

基于图像比对的跨平台UI一致性校验工具开发全流程指南——Android/iOS/Web三端自动化测试实战

一、需求背景与方案概述 1.1 为什么需要跨平台UI校验&#xff1f; 在移动互联网时代&#xff0c;同一产品需覆盖Android、iOS和Web三端。由于不同平台的开发框架&#xff08;如Android的Material Design与iOS的Cupertino风格&#xff09;及渲染引擎差异&#xff0c;UI界面易出…...

3D点云目标检测——KITTI数据集读取与处理

一、 数据基本情况 KITTI数据集是由德国卡尔斯鲁厄理工学院和丰田美国技术研究院联合创建的一个大规模自动驾驶场景下的计算机视觉算法评测数据集。以下是关于它的详细介绍&#xff1a; 数据集背景&#xff1a;为评估自动驾驶中计算机视觉算法的性能而设计。自动驾驶汽车需在…...

【鸿蒙开发】Hi3861学习笔记- 外部中断

00. 目录 文章目录 00. 目录01. 概述02. EXTI相关API03. 硬件设计04. 软件设计05. 实验现象06. 附录 01. 概述 我们在做按键控制实验时&#xff0c;虽然能实现 IO 口输入功能&#xff0c;但代码是一直在检测 IO 输入口的变化&#xff0c;因此效率不高&#xff0c;特别是在一些…...

技术与情感交织的一生 (一)

目录 一条朋友圈 静默 至暗时刻 选择 成人高考 歇一下 一条朋友圈 大年初一是我合作伙伴的生日&#xff0c;我称呼他为老高&#xff0c;他发的朋友圈写到&#xff1a;“50岁了&#xff0c;留下的皆是珍贵回忆。” &#xff0c;看到留言的瞬间&#xff0c;只有一个感觉&a…...

30天学习Java第六天——Object类

Object类 java.lang.Object时所有类的超类。Java中所有类都实现了这个类中的方法。 toString方法 将Java对象转换成字符串的表示形式。 public String toString() {return getClass().getName() "" Integer.toHexString(hashCode()); }默认实现是&#xff1a;完…...

基于WebRTC与P2P技术,嵌入式视频通话EasyRTC实现智能硬件音视频交互,适配Linux、ARM、RTOS、LiteOS

EasyRTC不仅仅是一个连接工具&#xff0c;更是一个经过深度优化的通信桥梁。它在嵌入式设备上进行了特殊优化&#xff0c;通过轻量级SDK设计、内存和存储优化以及硬件加速支持&#xff0c;解决了传统WebRTC在嵌入式设备上的适配难题&#xff0c;显著节省了嵌入式设备的资源。 1…...

向量库集成指南

文章目录 向量库集成指南Chroma集成Pinecone集成MiLvus集成向量库集成指南 向量库是一种索引和存储向量嵌入以实现高效管理和快速检索的数据库。与单独的向量索引不同,像Pinecone这样的向量数据库提供了额外的功能,例如,索引管理、数据管理、元数据存储和过滤,以及水平扩展…...

深度研究deep-research优秀开源项目

原文链接:https://i68.ltd/notes/posts/20250305-deep-research2/ 港大开源AI科研神器AI-Researcher 项目仓库:GitHub - HKUDS/AI-Researcher: "AI-Researcher: Fully-Automated Scientific Discovery with LLM Agents" & "Open-Sourced Alternative to G…...

芯谷D8563TS:低功耗CMOS实时时钟/日历电路的优选方案

在电子设备中&#xff0c;实时时钟&#xff08;RTC&#xff09;电路对于提供准确的时间和日历信息至关重要。芯谷D8563TS作为一款低功耗的CMOS实时时钟/日历电路&#xff0c;以其丰富的功能、高精度和灵活的可编程性&#xff0c;成为众多嵌入式系统和电池供电设备中的理想选择。…...

FPGA中级项目1——IP核(ROM 与 RAM)

FPGA中级项目1——IP核&#xff08;ROM 与 RAM&#xff09; IP核简介 在 FPGA&#xff08;现场可编程门阵列&#xff09;设计中&#xff0c;IP 核&#xff08;Intellectual Property Core&#xff0c;知识产权核&#xff09;是预先设计好的、可重用的电路模块&#xff0c;用于实…...

Redis的持久化-AOF

1.AOF AOF&#xff08;Append Only File&#xff09;持久化&#xff1a;以独立日志的方式记录每次写命令&#xff0c;重启时在重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性&#xff0c;目前已经是Redis持久化的主流方式。理解掌握好A…...

jmeter-sample

jmeter-sample http request:接口测试常用请求参数ParametersBody DataFiles Upload jdbc request配置JDBC Connection Configuration创建JDBC Requst请求 http request:接口测试常用 请求参数 Parameters 常见于get请求&#xff0c;与拼在接口后面是一样的效果&#xff1a;如…...

2025-03-15 学习记录--C/C++-PTA 练习3-4 统计字符

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 练习3-4 统计字符 本题要求编写程序&#xff0c;输入10个字符&#xff0c;统计其中英文字母、空格或回车、…...

编程自学指南:java程序设计开发,网络编程基础,TCP编程,UDP编程,HTTP客户端开发

编程自学指南&#xff1a;java程序设计开发&#xff0c;网络编程基础 学习目标&#xff1a; 理解网络协议&#xff08;TCP/IP、UDP&#xff09;的核心概念 掌握Socket编程实现客户端与服务端通信 能够通过多线程处理并发网络请求 开发简单的网络应用&#xff08;如聊天程序…...

C++ primer plus 类和对象

目录​​​​​​​ 前言 一 接口的设计 二 方法的设计和使用 三 构造函数 四 析构函数 五 析构函数和构造函数小结 总结 前言 前面已经描述了很多有关于类和对象的知识了&#xff0c;所以我们直接开始上手操作 一 接口的设计 首先我们要知道什么是接口 接口是一个…...

k8s 修改节点驱逐阈值

编辑 /var/lib/kubelet/config.yaml 文件 kind: KubeletConfiguration evictionHard:nodefs.available: "5%" # 降低磁盘压力触发阈值imagefs.available: "10%" # 调整容器镜像存储触发阈值nodefs.inodesFree: "3%...

HiPixel开源AI驱动的图像超分辨率的原生macOS 应用程序,使用 SwiftUI 构建并利用 Upscayl 强大的 AI 模型

一、软件介绍 文末提供程序和源码下载 HiPixel是一个开源程序基于SwiftUI构建的macOS原生应用程序&#xff0c;用于AI驱动的图像超分辨率&#xff0c;并利用Upscayl的强大AI模型。 二、软件特征 具有 SwiftUI 界面的原生 macOS 应用程序使用 AI 模型进行高质量图像放大通过 G…...

使用 .NET Core 实现 RabbitMQ 消息队列的详细教程

RabbitMQ 是一个流行的消息队列中间件&#xff0c;它允许应用程序通过异步消息的方式进行通信。RabbitMQ 支持 AMQP 协议&#xff0c;可以通过多种方式与应用程序交互。在本教程中&#xff0c;我们将深入探讨如何在 .NET Core 环境中使用 RabbitMQ 来实现消息队列。我们将学习如…...

深度学习——同一台电脑使用ssh配置多个github账号

如果一台电脑只有一个github账号&#xff0c;那么进行默认的ssh配置&#xff0c;通过git拉取和提交代码即可&#xff0c;但在实际的工作中&#xff0c;有时候需要在一台电脑登录多个github账号&#xff0c;将不同的项目代码提交到不同的github账号&#xff0c;这个时候如果仅仅…...

windows常用cmd命令

Windows 命令提示符&#xff08;CMD&#xff09;提供了许多实用的命令&#xff0c;用于管理文件、目录、网络、系统配置等。以下是一些常用的 CMD 命令及其用途&#xff1a; 文件和目录操作 dir: 列出当前目录下的文件和子目录。 dircd: 切换当前目录。 cd C:\Users cd .. # 返…...

C语言中的流程控制语句

一.流程控制语句的分类&#xff1a; 1.顺序结构 概念&#xff1a;从上往下依次执行&#xff0c;也是程序默认的执行顺序 2.分支结构 概念&#xff1a;程序在执行的过程中出现了岔路&#xff08;我们只能选择一条支线进行执行&#xff09; &#xff08;1&#xff09;.if语句…...

C语言【数据结构】:理解什么是数据结构和算法(启航)

引言 启航篇&#xff0c;理解什么是数据结构和算法 在 C 语言编程领域&#xff0c;数据结构和算法是两个核心且紧密相关的概念 一、数据结构 定义 数据结构是指相互之间存在一种或多种特定关系的数据元素的集合&#xff08;比如数组&#xff09;&#xff0c;它是组织和存储数…...

WebLogic XMLDecoder反序列化漏洞(CVE-2017-10271)深度解析与实战复现

0x00 漏洞概述 CVE-2017-10271 是Oracle WebLogic Server WLS Security组件中的远程代码执行漏洞。攻击者通过构造恶意XML请求&#xff0c;利用XMLDecoder反序列化机制绕过安全验证&#xff0c;最终实现服务器权限接管。 影响版本 WebLogic 10.3.6.0WebLogic 12.1.3.0WebLog…...

【动态规划篇】746.使用最小花费爬楼梯

746.使用最小花费爬楼梯 题目链接&#xff1a; 746.使用最小花费爬楼梯 题目叙述&#xff1a; 给你一个整数数组 cost &#xff0c;其中 cost[i] 是从楼梯第i个台阶向上爬需要支付的费用。一旦你支付此费用&#xff0c;即可选择向上爬一个或者两个台阶。 你可以选择从下标为 …...

类和对象:

1. const运算符重载&#xff1a; 1. const成员函数&#xff1a; 我们来看我们的下面的代码&#xff1a; 我们来看这个&#xff0c;我们的对象使用const进行修饰&#xff0c;然后我们对象d1调用我们的成员函数&#xff0c;然后我们取d1的地址然后传过去&#xff0c;这时候我们的…...

研究整除的性质——最大公约数(GCD)和最小公倍数(LCM)

最大公约数&#xff08;GCD&#xff09;和最小公倍数&#xff08;Least Common Multiple&#xff0c;LCM&#xff09;研究整除的性质&#xff0c;非常古老&#xff0c;在2000多年前就得到了很好的研究。由于简单易懂&#xff0c;有较广泛的应用&#xff0c;它们是竞赛中频繁出现…...

jenkins 配置邮件问题整理

版本&#xff1a;Jenkins 2.492.1 插件&#xff1a; A.jenkins自带的&#xff0c; B.安装功能强大的插件 配置流程&#xff1a; 1. jenkins->系统配置->Jenkins Location 此处的”系统管理员邮件地址“&#xff0c;是配置之后发件人的email。 2.配置系统自带的邮件A…...

FastAPI复杂查询终极指南:告别if-else的现代化过滤架构

title: FastAPI复杂查询终极指南:告别if-else的现代化过滤架构 date: 2025/3/14 updated: 2025/3/14 author: cmdragon excerpt: 本文系统讲解FastAPI中复杂查询条件的构建方法,涵盖参数验证、动态过滤、安全防护等18个核心技术点。通过引入策略模式、声明式编程等技术,彻…...

MySQL行列转化

初始化表结构&#xff1a; CREATE TABLE student_scores (student_id int NOT NULL,student_name varchar(50) DEFAULT NULL,math_score int DEFAULT NULL,english_score int DEFAULT NULL,science_score int DEFAULT NULL,PRIMARY KEY (student_id) ) ENGINEInnoDB DEFAULT C…...

施磊老师c++(六)

继承与多态-多重继承 文章目录 继承与多态-多重继承1.虚基类和虚继承本节内容 2.菱形继承---怎么解决?本节内容**面试问题: 怎么理解多重继承的?**---重点 3.c提供的四种类型转换本节内容 1.虚基类和虚继承 本节内容 多重继承? 代码复用, 一个派生类 有多个基类 抽象类—有…...

c++:AVL树

1.概念 由于二叉搜索树不能确保为近似完全二叉树的结构&#xff0c;节点相同的情况下&#xff0c;高度可能会很高&#xff0c;高度有可能会很低&#xff0c;所以搜索次数不能稳定维持在logn级别。我们在二叉搜索树的基础上进行平衡调整就可以控制搜索次数稳定在logn级别。 而AV…...

HTML编辑MP4保存名称

上图是HTML的界面&#xff0c;需要点击EDIT_MP4的选项&#xff0c;然后就出现文本框输入MP4名称。输入对应的MP4文件名称后&#xff0c;则点击Add_MP4按钮就可以把MP4的名称修改到json文件里面&#xff0c;json文件是network_detail.json文件。 HTML和CGI程序的交互 上图是htm…...

以太坊AI代理与PoS升级点燃3月市场热情,2025年能否再创新高?

币热网深度报道&#xff1a;以太坊AI代理与PoS升级引爆3月热潮&#xff0c;2025年能否再攀历史新高&#xff1f; 原文来源&#xff1a;币热网 - 区块链信息资讯平台 以太坊升级&#xff0c;市场热情高涨 近期&#xff0c;以太坊市场犹如被一股神秘力量点燃&#xff0c;掀起了…...

IDEA2024又一坑:连接Docker服务连不上,提示:Cannot run program “docker“: CreateProcess error=2

为新电脑安装了IDEA2024版&#xff0c;因为局域网中安装有Docker,所以这台电脑上没有安装&#xff0c;当运行时发现死活连不上Docker报&#xff1a;Cannot run program “docker“: CreateProcess error2 分析&#xff1a; Docker服务有问题 其它电脑都能连&#xff0c;排除 网…...

css基本功

为什么 ::first-letter 是伪元素&#xff1f; ::first-letter 的作用是选择并样式化元素的第一个字母&#xff0c;它创建了一个虚拟的元素来包裹这个字母&#xff0c;因此属于伪元素。 grid布局 案例一 <!DOCTYPE html> <html lang"zh-CN"><head&…...

ALSA vs OSS:Linux 音频架构的演变与核心区别

在 Linux 音频系统的发展过程中&#xff0c;OSS&#xff08;Open Sound System&#xff09; 和 ALSA&#xff08;Advanced Linux Sound Architecture&#xff09; 曾分别在不同阶段承担着音频管理的角色。OSS 是 Linux 早期的音频架构&#xff0c;而 ALSA 作为其继任者&#xf…...

双指针算法介绍+算法练习(2025)

一、介绍双指针算法 双指针&#xff08;或称为双索引&#xff09;算法是一种高效的算法技巧&#xff0c;常用于处理数组或链表等线性数据结构。它通过使用两个指针来遍历数据&#xff0c;从而减少时间复杂度&#xff0c;避免使用嵌套循环。双指针算法在解决诸如查找、排序、去重…...

第八节:红黑树(初阶)

【本节要点】 红黑树概念红黑树性质红黑树结点定义红黑树结构红黑树插入操作的分析 一、红黑树的概念与性质 1.1 红黑树的概念 红黑树 &#xff0c;是一种 二叉搜索树 &#xff0c;但 在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是 Red和 Black 。 通过对 任何…...

【C++标准库类型】深入理解C++中的using声明:从基础到实践

目录 一、using声明基础 1.1 基本语法形式 1.2 典型应用场景 1.3 作用域规则 二、关键注意事项 2.1 命名冲突处理 2.2 头文件使用规范 2.3 与typedef的对比 三、面向对象中的应用 3.1. 解除派生类名称隐藏&#xff08;核心应用&#xff09; 3.2. 构造函数继承&#…...

蓝桥杯2024年第十五届省赛真题-回文数组

题目描述 小蓝在无聊时随机生成了一个长度为 n 的整数数组&#xff0c;数组中的第 i 个数为ai&#xff0c;他觉得随机生成的数组不太美观&#xff0c;想把它变成回文数组&#xff0c;也是就对于任意i ∈ [1, n] 满足 ai an−i1 。小蓝一次操作可以指定相邻的两个数&#xff0c…...

多数元素——面试经典150题(力扣)

题目 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1&#xff1a; 输入&#xff1a;nums [3,2,3] 输出&#xff1a;3 …...