前端实战:Excel 导入导出规范(命名 + 校验 + 错误处理 + 统一交互)|API 与异步请求规范篇

【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 与异步请求规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端体系化学习完全体:基础 → 规范 → 架构 → 大厂面试

四套系列、百余篇高质量实战文,从入门到进阶,一站式补齐前端核心能力

每个系列完结后,都会整理成一篇完整导航文并附上直达链接,方便大家按顺序、体系化学习。

全套内容持续更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
The Future is mine7 小时前
Excel如何只允许对部分表格内容进行修改
excel
kyriewen7 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端