【Excel 导入导出 + Axios 请求封装】中后台前端实战:从规范约定到落地代码,彻底搞定导入导出与异步请求,避开文件名混乱、校验缺失、接口异常等高频坑!

📑 文章目录
- 一、写在前面
- [二、Excel 导入导出:为什么要定规范?](#二、Excel 导入导出:为什么要定规范?)
- 三、核心规范一览
- 四、文件命名规范
- [4.1 导入模板命名](#4.1 导入模板命名)
- [4.2 导出文件命名](#4.2 导出文件命名)
- 五、格式校验规范(导入)
- [5.1 前端基础校验(必做)](#5.1 前端基础校验(必做))
- [5.2 后端校验与错误返回(建议)](#5.2 后端校验与错误返回(建议))
- 六、错误处理规范
- [6.1 统一错误码约定](#6.1 统一错误码约定)
- [6.2 导入错误展示示例(Vue3 + Element Plus)](#6.2 导入错误展示示例(Vue3 + Element Plus))
- 七、导出规范与交互
- [7.1 导出前校验](#7.1 导出前校验)
- [7.2 统一 Loading 与下载触发](#7.2 统一 Loading 与下载触发)
- [八、API 与异步请求规范(进阶)](#八、API 与异步请求规范(进阶))
- [8.1 封装 axios 实例](#8.1 封装 axios 实例)
- [8.2 文件类接口的特殊处理](#8.2 文件类接口的特殊处理)
- [8.3 并发与取消请求](#8.3 并发与取消请求)
- [九、完整示例:用户批量导入(Vue3 + Element Plus + xlsx)](#九、完整示例:用户批量导入(Vue3 + Element Plus + xlsx))
- 十、规范总结速查表
- 十一、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
- [📝 API 与异步请求规范](#📝 API 与异步请求规范)
- [📚 系列总览](#📚 系列总览)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、写在前面
本篇围绕「Excel 导入导出」和「API/异步请求规范」两个主题,以「能用、好懂、少踩坑」为目标,讲清楚:日常该怎么写、为什么这么写、容易在哪里出错。
适合:
- 会用 JS,但对 Excel 导入导出和请求封装不太熟的同学
- 想系统补一补实战规范的 1~3 年开发者
- 有几年经验,希望统一自己习惯的老前端
不涉及「超级底层」原理,重点在规范、约定和可复用的写法。
[⬆ 返回目录](#⬆ 返回目录)
二、Excel 导入导出:为什么要定规范?
很多人会写导入导出,但往往只是「能跑」:
- 文件名随意:
data.xlsx、导出.xlsx、未命名.xlsx - 不做格式校验,直接上传,后端报错后用户一脸懵
- 错误信息不统一:有时弹窗,有时表格里标红,有时控制台才有
一旦出现一次「导入了错误模板」「导出了数据缺失」,用户就会对整个功能失去信任。所以需要一套可复用的规范和约定。
[⬆ 返回目录](#⬆ 返回目录)
三、核心规范一览
| 规范项 | 导入 | 导出 |
|---|---|---|
| 文件命名 | 模板固定命名,用户下载后可直接上传 | 导出文件带时间戳或业务标识 |
| 格式校验 | 校验扩展名、表头、必填列 | 导出前校验数据结构、空数据提示 |
| 错误处理 | 行级错误返回,支持下载错误明细 | 导出失败时提示原因和重试方式 |
| 交互统一 | 统一 Loading、进度、成功/失败提示 | 统一进度条、下载提示 |
[⬆ 返回目录](#⬆ 返回目录)
四、文件命名规范
4.1 导入模板命名
模板文件名要一眼能看出「是什么」「最新版」,建议:
{业务模块}_{用途}_模板_{版本}.xlsx
示例:
用户管理_批量导入_模板_v1.xlsx订单_批量导出_模板_20240320.xlsx
前端下载时,应使用和后端约定好的文件名,而不是随机名。
js
// ❌ 不推荐:用户下载后文件名不可预期
const res = await axios.get('/api/download/template')
// 下载下来的可能是 download 或乱码
// ✅ 推荐:从 Content-Disposition 或后端约定中获取文件名
const res = await axios.get('/api/download/template', { responseType: 'blob' })
const fileName = res.headers['content-disposition']
? decodeURIComponent(res.headers['content-disposition'].split('filename=')[1].replace(/"/g, ''))
: '用户管理_批量导入_模板_v1.xlsx'
const url = window.URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', fileName)
link.click()
window.URL.revokeObjectURL(url)
这样用户保存的始终是「业务可识别的模板名」,上传时也不会搞混。
[⬆ 返回目录](#⬆ 返回目录)
4.2 导出文件命名
导出建议包含:业务、时间、可选:批次标识。
{业务模块}_{描述}_{时间}.xlsx
示例:用户列表_20240320_143052.xlsx
js
// ✅ 导出时生成带时间戳的文件名
function getExportFileName(moduleName = '导出数据') {
const now = new Date()
const date = now.toISOString().slice(0, 10).replace(/-/g, '')
const time = now.toTimeString().slice(0, 8).replace(/:/g, '')
return `${moduleName}_${date}_${time}.xlsx`
}
[⬆ 返回目录](#⬆ 返回目录)
五、格式校验规范(导入)
5.1 前端基础校验(必做)
在上传前先做一层「兜底」,减少无效请求:
| 校验项 | 说明 | 实现要点 |
|---|---|---|
| 扩展名 | 仅允许 .xlsx / .xls | 用后缀判断,避免 .xlsx.txt 等伪装 |
| 文件大小 | 如限制 5MB | 超过直接提示,不再请求 |
| 表头 | 校验必填列是否齐全 | 用 xlsx 库解析首行比对 |
js
// 允许的扩展名
const ALLOWED_EXT = ['.xlsx', '.xls']
const MAX_SIZE = 5 * 1024 * 1024 // 5MB
const REQUIRED_HEADERS = ['姓名', '手机号', '部门'] // 与模板保持一致
function validateImportFile(file) {
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
if (!ALLOWED_EXT.includes(ext)) {
return { valid: false, message: `仅支持 ${ALLOWED_EXT.join('、')} 格式` }
}
if (file.size > MAX_SIZE) {
return { valid: false, message: `文件大小不能超过 ${MAX_SIZE / 1024 / 1024}MB` }
}
return { valid: true }
}
// 若需要表头校验,需在解析后执行
async function validateSheetHeaders(file) {
const XLSX = await import('xlsx')
const buffer = await file.arrayBuffer()
const workbook = XLSX.read(buffer, { type: 'array' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const headers = XLSX.utils.sheet_to_json(sheet, { header: 1 })[0] || []
const missing = REQUIRED_HEADERS.filter(h => !headers.includes(h))
if (missing.length) {
return { valid: false, message: `缺少必填列:${missing.join('、')}` }
}
return { valid: true }
}
[⬆ 返回目录](#⬆ 返回目录)
5.2 后端校验与错误返回(建议)
业务规则、重复校验等更适合放在后端。推荐返回结构:
json
{
"code": 0,
"message": "导入完成,部分行存在错误",
"data": {
"successCount": 95,
"failCount": 5,
"failRows": [
{ "row": 3, "reason": "手机号格式错误" },
{ "row": 7, "reason": "部门不存在" }
],
"errorFileUrl": "/api/import/error-file/xxx.xlsx" // 可选:错误明细文件
}
}
前端拿到后,可以:
- 显示「成功 X 条,失败 Y 条」
- 列出失败行号和原因
- 提供错误明细文件下载
[⬆ 返回目录](#⬆ 返回目录)
六、错误处理规范
6.1 统一错误码约定
建议前后端约定一套业务码,而不是只靠 HTTP 状态码:
| 码 | 含义 | 前端处理 |
|---|---|---|
| 0 | 成功 | 正常展示 |
| 40001 | 参数/格式错误 | 提示用户检查文件或参数 |
| 40002 | 表头/模板不匹配 | 提示下载最新模板 |
| 50001 | 服务异常 | 通用错误提示 + 可重试 |
js
const ERROR_MAP = {
40001: '文件格式或内容不符合要求,请检查后重试',
40002: '模板已更新,请下载最新模板后重新填写',
50001: '服务异常,请稍后重试'
}
function getErrorMessage(code, defaultMsg = '操作失败,请稍后重试') {
return ERROR_MAP[code] || defaultMsg
}
[⬆ 返回目录](#⬆ 返回目录)
6.2 导入错误展示示例(Vue3 + Element Plus)
html
<template>
<el-upload
:before-upload="beforeUpload"
:http-request="customUpload"
:show-file-list="false"
accept=".xlsx,.xls"
>
<el-button type="primary">选择文件导入</el-button>
</el-upload>
<!-- 导入结果展示 -->
<el-dialog v-model="resultVisible" title="导入结果" width="500px">
<div class="result-summary">
<p>成功:{{ result.successCount }} 条</p>
<p v-if="result.failCount">失败:{{ result.failCount }} 条</p>
</div>
<el-table v-if="result.failRows?.length" :data="result.failRows" max-height="200">
<el-table-column prop="row" label="行号" width="80" />
<el-table-column prop="reason" label="失败原因" />
</el-table>
<template #footer>
<el-button v-if="result.errorFileUrl" type="primary" @click="downloadErrorFile">
下载错误明细
</el-button>
<el-button @click="resultVisible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { validateImportFile } from './validate'
import { importUserApi } from './api'
const resultVisible = ref(false)
const result = reactive({ successCount: 0, failCount: 0, failRows: [], errorFileUrl: '' })
async function customUpload({ file }) {
const { valid, message } = validateImportFile(file)
if (!valid) {
ElMessage.error(message)
return
}
const formData = new FormData()
formData.append('file', file)
try {
const res = await importUserApi(formData)
result.successCount = res.data.successCount
result.failCount = res.data.failCount ?? 0
result.failRows = res.data.failRows ?? []
result.errorFileUrl = res.data.errorFileUrl ?? ''
resultVisible.value = true
if (res.data.failCount === 0) {
ElMessage.success('导入成功')
} else {
ElMessage.warning(`导入完成,${res.data.failCount} 条失败,请查看详情`)
}
} catch (e) {
ElMessage.error(e.message || '导入失败,请稍后重试')
}
}
function beforeUpload() {
return false // 阻止默认上传,使用 customUpload
}
</script>
[⬆ 返回目录](#⬆ 返回目录)
七、导出规范与交互
7.1 导出前校验
导出前应检查:
-
当前查询条件是否有数据
-
数据量是否超过限制(如 1 万条)
js
async function handleExport() {
const { total } = await fetchListPreview({ ...queryParams, pageSize: 1 })
if (!total) {
ElMessage.warning('暂无数据可导出')
return
}
if (total > 10000) {
ElMessage.warning('导出数据超过 1 万条,请缩小筛选范围')
return
}
await doExport()
}
[⬆ 返回目录](#⬆ 返回目录)
7.2 统一 Loading 与下载触发
js
async function doExport() {
const loading = ElLoading.service({ text: '正在导出,请稍候...' })
try {
const res = await exportApi(queryParams)
const fileName = getExportFileName('用户列表')
const url = window.URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (e) {
ElMessage.error(e.message || '导出失败,请稍后重试')
} finally {
loading.close()
}
}
[⬆ 返回目录](#⬆ 返回目录)
八、API 与异步请求规范(进阶)
8.1 封装 axios 实例
建议单独建一个 request.js,统一 baseURL、超时、请求/响应拦截:
js
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE || '/api',
timeout: 30000
})
// 请求拦截:统一加 token
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
},
err => Promise.reject(err)
)
// 响应拦截:统一错误处理
request.interceptors.response.use(
res => {
const { code, message, data } = res.data
if (code === 0) return data
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message))
},
err => {
const msg = err.response?.data?.message || err.message || '网络异常'
ElMessage.error(msg)
return Promise.reject(err)
}
)
export default request
这样业务代码里直接 request.get/post,不再到处写 Authorization 和错误提示。
[⬆ 返回目录](#⬆ 返回目录)
8.2 文件类接口的特殊处理
文件下载需要 responseType: 'blob',并且要根据后端返回判断是 JSON 错误还是二进制文件:
js
// 导出接口封装
export async function exportUserList(params) {
const res = await request({
url: '/user/export',
method: 'post',
data: params,
responseType: 'blob'
})
// 若后端错误时也返回 blob(内容是 JSON 字符串),需要判断
const contentType = res.headers['content-type'] || ''
if (contentType.includes('application/json')) {
const text = await res.data.text()
const json = JSON.parse(text)
throw new Error(json.message || '导出失败')
}
return res.data
}
[⬆ 返回目录](#⬆ 返回目录)
8.3 并发与取消请求
大批量导出、导入时,避免重复点击:
js
import axios from 'axios'
let exportController = null
async function handleExport() {
if (exportController) {
exportController.abort()
}
exportController = new AbortController()
try {
await request({
url: '/user/export',
method: 'post',
data: queryParams,
responseType: 'blob',
signal: exportController.signal
})
// ... 处理下载
} catch (e) {
if (axios.isCancel(e)) {
ElMessage.info('已取消导出')
} else {
ElMessage.error(e.message || '导出失败')
}
} finally {
exportController = null
}
}
[⬆ 返回目录](#⬆ 返回目录)
九、完整示例:用户批量导入(Vue3 + Element Plus + xlsx)
下面是一个可复用的导入组件示例,串联前面提到的规范。
html
<!-- UserImport.vue -->
<template>
<div class="user-import">
<el-upload
ref="uploadRef"
:auto-upload="false"
:limit="1"
:on-change="onFileChange"
:on-exceed="onExceed"
accept=".xlsx,.xls"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
<template #tip>
<div class="el-upload__tip">
仅支持 xlsx/xls,单个文件不超过 5MB,请使用
<el-link type="primary" @click="downloadTemplate">下载最新模板</el-link>
</div>
</template>
</el-upload>
<el-button
v-if="currentFile"
type="primary"
:loading="uploading"
style="margin-top: 12px"
@click="submitImport"
>
开始导入
</el-button>
<el-dialog v-model="resultVisible" title="导入结果" width="560px">
<el-result
v-if="result.failCount === 0"
icon="success"
title="导入成功"
:sub-title="`共导入 ${result.successCount} 条数据`"
/>
<div v-else>
<p>成功 {{ result.successCount }} 条,失败 {{ result.failCount }} 条</p>
<el-table :data="result.failRows" max-height="240" border>
<el-table-column prop="row" label="行号" width="80" />
<el-table-column prop="reason" label="失败原因" />
</el-table>
<el-button
v-if="result.errorFileUrl"
type="primary"
size="small"
style="margin-top: 12px"
@click="downloadErrorFile"
>
下载错误明细
</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import * as XLSX from 'xlsx'
import request from '@/utils/request'
const REQUIRED_HEADERS = ['姓名', '手机号', '部门']
const MAX_SIZE = 5 * 1024 * 1024
const ALLOWED_EXT = ['.xlsx', '.xls']
const uploadRef = ref()
const currentFile = ref(null)
const uploading = ref(false)
const resultVisible = ref(false)
const result = reactive({ successCount: 0, failCount: 0, failRows: [], errorFileUrl: '' })
const emit = defineEmits(['success'])
function validateFile(file) {
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
if (!ALLOWED_EXT.includes(ext)) {
return { valid: false, msg: `仅支持 ${ALLOWED_EXT.join('、')} 格式` }
}
if (file.size > MAX_SIZE) {
return { valid: false, msg: '文件大小不能超过 5MB' }
}
return { valid: true }
}
async function validateHeaders(file) {
const buffer = await file.raw.arrayBuffer()
const wb = XLSX.read(buffer, { type: 'array' })
const ws = wb.Sheets[wb.SheetNames[0]]
const headers = XLSX.utils.sheet_to_json(ws, { header: 1 })[0] || []
const missing = REQUIRED_HEADERS.filter(h => !headers.includes(h))
if (missing.length) {
return { valid: false, msg: `缺少必填列:${missing.join('、')}` }
}
return { valid: true }
}
function onFileChange(file) {
const { valid, msg } = validateFile(file)
if (!valid) {
ElMessage.error(msg)
uploadRef.value?.clearFiles()
currentFile.value = null
return
}
currentFile.value = file
}
function onExceed() {
ElMessage.warning('每次只能导入一个文件')
}
async function submitImport() {
if (!currentFile.value) return
const { valid, msg } = validateFile(currentFile.value)
if (!valid) {
ElMessage.error(msg)
return
}
const headerValid = await validateHeaders(currentFile.value)
if (!headerValid.valid) {
ElMessage.error(headerValid.msg)
return
}
uploading.value = true
const formData = new FormData()
formData.append('file', currentFile.value.raw)
try {
const data = await request({
url: '/user/import',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
result.successCount = data.successCount ?? 0
result.failCount = data.failCount ?? 0
result.failRows = data.failRows ?? []
result.errorFileUrl = data.errorFileUrl ?? ''
resultVisible.value = true
uploadRef.value?.clearFiles()
currentFile.value = null
if (data.failCount === 0) emit('success')
} catch (e) {
ElMessage.error(e.message || '导入失败')
} finally {
uploading.value = false
}
}
async function downloadTemplate() {
try {
const res = await request({
url: '/user/template',
responseType: 'blob'
})
const url = window.URL.createObjectURL(new Blob([res]))
const link = document.createElement('a')
link.href = url
link.download = '用户管理_批量导入_模板_v1.xlsx'
link.click()
window.URL.revokeObjectURL(url)
ElMessage.success('模板下载成功')
} catch (e) {
ElMessage.error(e.message || '下载失败')
}
}
function downloadErrorFile() {
if (!result.errorFileUrl) return
window.open(result.errorFileUrl, '_blank')
}
</script>
该示例覆盖:文件校验、表头校验、统一错误展示、结果弹窗和模板下载,可直接作为项目中的导入组件使用。
[⬆ 返回目录](#⬆ 返回目录)
十、规范总结速查表
| 场景 | 要点 |
|---|---|
| 导入模板 | 固定命名,带版本;下载时使用约定文件名 |
| 导出文件 | 带时间戳,格式如:模块_日期_时间.xlsx |
| 导入校验 | 前端:扩展名、大小、表头;后端:业务规则、重复等 |
| 错误处理 | 约定业务码,统一错误文案,导入支持行级错误和明细下载 |
| 请求封装 | 统一 axios 实例,拦截器处理 token、错误提示 |
| 文件接口 | 使用 responseType: 'blob',判断 JSON 错误再抛出 |
| 导出交互 | 先校验有无数据、是否超量,再导出,并统一 Loading |
[⬆ 返回目录](#⬆ 返回目录)
十一、小结
本文从「日常该怎么选、为什么这么选、容易踩哪些坑」出发,整理了 Excel 导入导出和 API 请求的实战规范,并给出了可直接复用的 Vue 示例。
建议把「文件命名」「格式校验」「错误处理」「统一交互」写进团队或项目规范里,新功能直接复用,减少反复踩坑。如果你在实践中有不同约定或更好的做法,欢迎在评论区交流。
🔍 系列模块导航
📝 API 与异步请求规范
一、《Axios 统一封装实战:拦截器配置 + baseURL 优化 + 接口规范,避坑重复代码|API 与异步请求规范篇》
二、《Axios 接口请求规范实战:请求参数 / 响应处理 / 异常兜底,避坑中后台 API 调用混乱|API 与异步请求规范篇》
三、《Axios + Vue 错误处理规范:中后台项目实战,统一捕获系统 / 业务 / 接口异常|API 与异步请求规范篇》
四、《前端实战:Excel 导入导出规范(命名 + 校验 + 错误处理 + 统一交互)|API 与异步请求规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试
四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力
- 前端基础实战系列 : 《前端基础实战:JS/TS与Vue体系化扫盲(47 篇完整目录 + 避坑)》
- 前端规范实战系列 : 《JS/TS/Vue 前端规范实战:从写对到写优,搞定中后台规范落地,打造可维护代码(40 篇全目录)》
- 前端架构实战系列:聚焦工程化、性能优化、可维护架构、中后台体系设计(持续更新中)
- 前端大厂面试系列:覆盖高频考点、手写题、项目深挖、简历与面试技巧(规划中)
每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。
全套内容持续更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~