前端实战: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,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
码路人2 小时前
Vue生命周期与keep-alive实战理解
vue.js
慧一居士2 小时前
TanStack功能介绍和使用场景,对应 vue,react 完整使用示例
前端·vue.js
新晨4372 小时前
Git跨分支文件恢复:如何将其他分支的内容安全拷贝到当前分支
前端·git
一枚菜鸟_2 小时前
02-React+TypeScript基础速览
前端·taro
踩着两条虫2 小时前
VTJ.PRO 在线应用开发平台入门与项目初始化
前端·人工智能·ai编程
码路人2 小时前
VUE-组件命名与注册机制
vue.js
流星雨在线2 小时前
大前端通用性能优化(高频场景专项)
前端·性能优化
方安乐2 小时前
ESLint代码规范(一)
前端·javascript·代码规范
酉鬼女又兒2 小时前
零基础快速入门前端JavaScript Array 常用方法详解与实战(可用于备赛蓝桥杯Web应用开发)
开发语言·前端·javascript·chrome·蓝桥杯
January12072 小时前
Vue3打卡计时器:完整实现与优化方案
前端·javascript·css