← 返回目录

1. 图片上传预览与边界场景处理

这页讲怎么让用户选完图片后立刻在页面上看到预览,不用等上传完。整个流程分两阶段:先本地预览(纯浏览器操作,不发网络请求),再真正上传到服务器(FormData + $.ajax)。

本地预览核心三步:
拿到用户选的 File——input change 事件里 e.target.files[0] 就是用户选的文件
用 FileReader 读成 base64——new FileReader() 创建阅读器,readAsDataURL(file) 把文件转成 data:image/...;base64,xxx 字符串
塞进 img 的 src——img.src = base64,浏览器立刻渲染显示,全程不联网

真正上传核心两步:
FormData 装文件——new FormData(),append("file", file) 把文件塞进表单
$.ajax 发 POST——processData:false + contentType:false 两个开关必须加,否则上传失败

坑:文件类型校验(accept 属性 + 手动判断 type)、大小限制(file.size 对比)、重复选图要清空旧预览、取消上传用 AbortController

1.1 前置必备知识

① FormData 文件上传载体

普通键值对无法传递二进制图片文件,FormData 是浏览器专门用于文件上传的对象。

// 追加文件语法

// 上传硬性规则:
// 1. POST 请求,禁止手动设置 Content-Type
// 2. 浏览器自动生成 multipart/form-data 分隔头部
// 3. 手动覆盖会导致后端收不到图片
formData.append('字段名', File对象);

② jQuery $.ajax 上传固定配置

发送 FormData 必须增加两项配置,缺失会上传失败:

$.ajax({
  url: "接口地址",
  type: "POST",
  data: formData,

  // ★ 关闭 jQuery 自动序列化参数
  processData: false,

  // ★ 关闭默认表单请求头,交给浏览器自动处理
  contentType: false
});

③ jQuery 引入规范

所有代码依赖 jQuery,页面头部先引入 CDN,业务代码写在 $(function() {}) DOM 就绪函数中:

< script src = "https:  // cdn.jsdelivr.net/npm/jquery@3.

  <
  script >
  $(function () {
    // 所有业务代码写在这里
  }); <
/script>

1.2 文件上传基础标签与 accept 过滤

① 标签属性说明

② 自定义上传按钮(项目标准写法)

原生 input 样式丑陋,业务统一隐藏输入框,按钮触发文件弹窗:

<!-- 单图选择 -->
<
button class = "selectSingle" > 选择单张图片 < /button> <
input type = "file"
id = "singleFile"
accept = "image/*"
style = "display:none;" >

  <
  !--多图选择 -->
  <
  button class = "selectMulti" > 多选多张图片 < /button> <
input type = "file"
id = "multiFile"
accept = "image/*"
multiple style = "display:none;" >

  <
  script >
  $(function () {
    // 点击按钮唤起文件选择弹窗
    $(".selectSingle").click(() => $("#singleFile").click());
    $(".selectMulti").click(() => $("#multiFile").click());
  }); <
/script>

1.3 change 事件与 File 文件对象解析

① change 关键特性

② File 对象核心属性

属性 单位/用途
name 文件全名,用于错误提示
size 字节数,校验图片最大限制
type MIME 类型,判断是否为图片

③ 获取文件基础示例

$("#singleFile").on("change", function (e) {
  // 用户未选择文件,直接终止逻辑
  const fileList = e.target.files;

  if (fileList.length === 0) return;
  const file = fileList[0];

  console.log("文件名:", file.name, "大小:", file.size, "类型:", file.type);
});

1.4 FileReader 本地预览原理与通用封装

① 核心区分

FileReader 仅做前端本地预览,不发送网络请求,不会上传服务器;作用:将本地图片转为 base64 字符串,直接赋值 img.src 实现即时预览。

② 完整执行流程

  1. 用户选中文件触发 change,拿到 File 对象
  2. 执行空/类型/大小边界校验
  3. 实例化 FileReader,绑定 onload 成功、onerror 失败回调
  4. 调用 readAsDataURL(file) 异步读取本地文件
  5. 读取完成得到 base64 地址,渲染页面图片

③ 全局复用预览工具函数

/**
 * 读取图片生成base64预览地址
 * @param {File} file 图片文件对象
 * @returns Promise<string> base64地址
 */
function getPreviewUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = e => resolve(e.target.result);
    reader.onerror = () => reject("文件损坏,无法读取预览");
    reader.readAsDataURL(file);
  });
}

上面这个函数只负责「把 File 变成 base64」,它自己不会把图画到页面上。真正渲染要三步:拿到 File → await 调用函数 → 把字符串塞进 innerHTML。完整写在 input 的 change 事件里长这样(用模板字符串拼 img):

// 写在 input 的 change 事件里,外层函数要加 async
$("#fileInput").on("change", async function (e) {
  // ① 拿到用户选的那张 File;没选到就直接退出,别往下走
  const file = e.target.files[0];
  if (!file) return;

  // ② await 调用封装好的函数,拿到 base64 地址
  //    getPreviewUrl 返回 Promise,所以必须 await;外层函数也要写 async
  const preview = await getPreviewUrl(file);

  // ③ 用模板字符串(反引号 `)把整个 <img> 拼出来,变量写在 ${preview} 里
  //    一行搞定:建标签 + 设 src + 设样式 + 塞进预览容器
  $("#previewBox").html(
    `<img src="${preview}" style="width:120px;height:120px;object-fit:cover;border-radius:8px;">`
  );
});

三个要点:① getPreviewUrl(file) 返回的是 Promise,前面必须 await,外层函数得写成 async function;② 用模板字符串(反引号 `)把 <img> 整段拼出来、变量写在 ${preview} 里,比 createElement + 设属性 + appendChild 三步短一大截;③ 拼好的字符串用 .html() 塞进容器,浏览器立刻渲染,全程不发网络请求。

1.5 单图完整业务:预览 + jQuery AJAX 上传

① 单图上传演示(真实接口)

① 选图
② 本地校验+预览
③ 上传真实接口
④ 拼接域名显示线上图片

上面这个演示接的是真实接口,不是假数据。你可以真的选一张本地图片试试:先看到本地预览(这一步不联网),点上传后才真的发请求,成功后图片会换成服务器上的永久地址。

② 上传前校验(把坏文件挡在门外)

边界场景 判断写法 处理方式
空文件(0 字节)
file.size === 0
提示并 return,不进预览
非图片(绕过 accept)
!file.type.startsWith("image/")
提示并 return
超大图片(5MB 上限)
file.size > 5 * 1024 * 1024
提示并 return,禁止预览/上传
损坏图片
reader.onerror
捕获异常,提示"文件损坏"
没选文件
files.length === 0
上传按钮置灰 disabled

为什么要在最前面校验?因为一旦放行,后面就要白跑一遍"读文件、传网络",纯浪费。accept="image/*" 只是"文件弹窗默认帮你过滤图片",它拦不住懂行的人——把一个 .txt 改后缀成 .jpg 就能绕过。所以必须用 file.type(浏览器自己判断的真实类型)二次校验。

③ 单图上传知识点详解

1. 为什么要把 input[type=file] 藏起来,用按钮唤起?

< input type = "file"
id = "fileInput"
accept = "image/*"
style = "display:none;" > →真正的选文件框, 先藏起来看不见
$("#selectBtn").click(function () { $("#fileInput").click(); });
// 自定义按钮,偷偷点一下隐藏的 input

原生的文件选择框长得丑,而且每个浏览器长得都不一样,没法统一成设计稿的样子。所以项目里统一做法:把 input type="file"display:none 藏起来,自己画一个好看的按钮,按钮被点的时候,在代码里偷偷 .click 一下那个隐藏的 input,文件选择弹窗就弹出来了。这叫"借壳"。

2. change 事件的 3 个坑(新手必踩)

$("#fileInput").on("change", function (e) {
  // ★ 文件对象(伪数组,可能为空)
  const file = e.target.files[0];

  // file.name 文件名(弹提示用)
  // file.size 字节数(校验大小用)
  // file.type MIME 类型(判断是不是图片用,如 image/png)
});

三个坑,个个要背:
坑 1:如果你连续两次选同一个文件change 事件根本不触发,因为浏览器觉得"你选的东西没变"。解决办法:打开弹窗前先 input.value = "" 清空,强制让它"每次都是新文件"。
坑 2:你点开了选择框,又直接关掉,这时 files.length 是 0,必须判空拦截,不然读 undefined 会报错。
坑 3:每次触发 change,都要把上一次的预览图、按钮状态全部重置,否则会出现"新文件选了,但页面还显示着旧图"的错位。

3. FileReader 本地预览 vs 线上上传(本质区别)

FileReader 本地预览 $.ajax 线上上传
不发任何网络请求 POST 请求真发到服务器
把图片转成 base64 临时字符串 把原始 File 对象通过 FormData 传走
页面一刷新就没了 服务器存下来,返回永久地址
只是给用户"提前看一眼效果" 完成数据真正的持久化
// getPreviewUrl 已在 1.4 ③ 封装好,这里直接调用(单图、多图通用)
const preview = await getPreviewUrl(file);

// base64 塞给 img.src,本地预览秒出,不联网
img.src = preview;

FileReader.readAsDataURL 做的事,就是把你硬盘上那张图,在浏览器里翻译成一长串 data:image/png;base64,xxxx 的字符串,然后 img.src 直接吃这个字符串就能显示。这一步完全不联网,所以预览秒出。

4. FormData 上传(字段名 + 拼域名是关键)

// 造一个快递袋(空的)
const formData = new FormData();

// ★ 字段名必须和接口文档一致
formData.append("file", targetFile);

// 用 jQuery 发请求
$.ajax({
      // POST 提交
      url: "http:                           // jiekou.jinghongs.top/exhibitio
      type: "POST",

      // 数据就是那个快递袋
      data: formData,

      // ★ 关闭 jQuery 自动转字符串(必写)
      processData: false,

      // ★ 关闭默认请求头(必写)
      contentType: false,
      success: function (res) {
        // 判断成功
        if (res.code === 1) {
          $("#preview").attr("src", "http:  // jiekou.jinghongs.top" + res.ur
        }
      }
});

三个必背点,少一个都传不上去:
FormData.append("file", 文件),字段名 file 必须和接口文档一字不差。
② 用 jQuery 传 FormData,processData: falsecontentType: false 这两行缺一不可
③ 判断成功看 res.code === 1,返回的 res.url 是相对路径,必须拼域名

1.6 多图完整业务:预览删除 + 串行上传

① 业务背景

绝大多数后端接口一次仅接收一张图片,多选图片需循环串行逐个调用上传接口,禁止一次性传入多文件。

② 数据存储结构

let imgList = [{
  // 用于上传
  file: File对象,

  // 本地临时预览
  preview: "base64字符串",

  // 上传成功后端返回线上地址
  onlineUrl: ""
}];

③ 多图上传演示

① 选多张图
② 本地预览每张
③ 逐张上传
④ 上传完成显示线上图

② 多图上传的完整流程

选完图后,JS 自动做三件事:校验 → 存数组 → 等用户点上传。结构长这样:

// ========== 基础配置 ==========
// const 表示常量,不会被重新赋值
// 接口域名
const BASE_URL = "https://jiekou.jinghongs.top";

// 最大文件大小 5MB(字节)
const MAX_SIZE = 5 * 1024 * 1024;

// ========== 数据存储 ==========
// let 表示变量,值会被修改
// 存放用户选中的 File 对象,用于上传
let fileArr = [];

// 存放上传成功后的图片 URL
let successImgArr = [];

// ========== 1. 点击"选择图片"按钮 ==========
// 触发隐藏的 input[type="file"] 的点击事件,打开文件选择窗口
$("#selectBtn").click(function () {
  $("#fileInput").click();
});

// ========== 2. 用户选择文件后触发 change 事件 ==========
$("#fileInput").change(function () {
  // 每次重新选择,先清空之前的文件数组
  fileArr = [];
  // this.files 是 FileList 对象,转成真正的数组
  const files = Array.from(this.files);

  // 没选文件的情况
  if (files.length === 0) {
    $("#status").text("没有选中图片");

    // 结束函数
    return;
  }

  // 逐个检查每个文件是否合法
  for (let i = 0; i < files.length; i++) {
    const file = files[i];

    // 检查1:文件大小为 0(空文件)
    if (file.size === 0) {
      alert(file.name + ' 是空文件,跳过');

      // 跳过当前文件,继续下一个
      continue;
    }

    // 检查2:不是图片类型
    if (!file.type.startsWith('image/')) {
      alert(file.name + ' 不是图片,跳过');
      continue;
    }

    // 检查3:超过 5MB 限制
    if (file.size > MAX_SIZE) {
      alert(file.name + ' 超过5MB,跳过');
      continue;
    }

    // 所有检查通过,加入数组
    fileArr.push(file);
  }

  // 如果一张合法图片都没有
  if (fileArr.length === 0) {
    $("#status").text("没有合法图片");
    return;
  }

  // 显示选择结果
  $("#status").text("已选择 " + fileArr.length + " 张图片,点击上传");
  // 清空 input 的值,这样同一张图片可以重复选择触发 change
  this.value = '';
});

// ========== 3. 删除已上传的图片 ==========
// 使用事件委托:因为删除按钮是动态生成的,所以绑定在父容器上
$("#successPreviewBox").on("click", ".del-btn", function () {
  // $(this) 是点击的那个删除按钮
  // .parent() 找到它的父元素(.img-item 容器)
  const $card = $(this).parent();
  // 计算这个容器在兄弟元素中排第几个(用来对应数组下标)
  const index = $("#successPreviewBox .img-item").index($card);
  // 从数组中删除对应项
  if (index > -1) {
    // splice(下标, 删除个数)
    successImgArr.splice(index, 1);
  }
  // 从页面上移除这个图片容器
  $card.remove();
  // 更新状态文字
  if (successImgArr.length === 0) {
    $("#status").text("暂无上传成功的图片");
  } else {
    $("#status").text("删除成功,剩余 " + successImgArr.length + " 张");
  }
});

// ========== 4. 点击"开始上传"按钮 ==========
$("#uploadBtn").click(function () {
  // 没选图片就提示
  if (fileArr.length === 0) {
    alert("请先选择图片!");
    return;
  }
  // 上传期间禁用按钮,防止重复点击
  $(this).prop("disabled", true);
  $("#selectBtn").prop("disabled", true);
  $("#status").text("开始上传...");
  // 从第 0 张开始逐张上传
  uploadOne(0);
});

// ========== 5. 逐张上传函数(递归调用) ==========
function uploadOne(index) {
  // 递归终止条件:所有图片都传完了
  if (index >= fileArr.length) {
    $("#uploadBtn").prop("disabled", false);
    $("#selectBtn").prop("disabled", false);
    $("#status").text("全部图片上传完成!");
    return;
  }

  // 创建表单数据对象(用于文件上传)
  const formData = new FormData();
  // 把当前这张文件加进去,字段名是 "file"(要和后端对应)
  formData.append("file", fileArr[index]);

  // 发送 AJAX 请求
  $.ajax({
    url: BASE_URL + "/exhibition/Image/upload",
    type: "POST",
    processData: false,
    // 告诉 jQuery 不要处理数据(文件上传必须)
    contentType: false,
    // 告诉 jQuery 不要设 Content-Type(让浏览器自动设 multipart)
    dataType: "json",
    // 期望返回 JSON
    success: function (data) {
      // 后端返回 code === 1 表示成功
      if (data.code === 1) {
        // 把返回的 URL 存进数组
        successImgArr.push(data.url);
        $("#status").text("第 " + (index + 1) + " 张上传成功");
        // 拼接 HTML 并追加到成功区域
        $("#successPreviewBox").append(`
          <div class="img-item">
          <img src="${BASE_URL + data.url}">
          <span class="del-btn">×</span>
          </div>
        `);
      } else {
        // 后端返回失败
        $("#status").text("第 " + (index + 1) + " 张失败:" + (data.msg || "未知错误"));
      }
      // 不管成功失败,继续传下一张(递归)
      uploadOne(index + 1);
    },

    // 网络错误回调
    error: function () {
      $("#status").text("第 " + (index + 1) + " 张上传失败(网络错误)");

      // 失败了也继续
      uploadOne(index + 1);
    }
  });
}

三个关键:校验放最前面(先拦坏文件)→ 递归逐张传(一张传完再传下一张)→ 用后端 URL 显示(不是本地 base64,刷新还在)。

1.7 实战:用在哪 / 常见坑 / 怎么解决

① 可能在什么地方用:用户头像上传、商品详情页传多张实拍图、发布文章时插图、证件照上传审核——只要让用户"选个图传上来",都是这套 FileReader 预览 + FormData 上传 的写法。

② 常见的问题:传完图片显示成裂图(多半是忘了拼域名);点上传没反应(多半是字段名写错或 jQuery 少了 processData:false/contentType:false);连续选同一个文件不触发 change

③ 解决思路:打开浏览器 F12 → Network,看这次上传请求:请求体里有没有文件(Payload)、Response 返回的 code 和 url 长什么样。裂图就手动把 res.url 拼上域名在地址栏打开验证;收不到文件就核对字段名是不是 file。

一句话记住:选文件看 File,本地预览用 FileReader.readAsDataURL,传服务器用 FormData(字段名对、jQuery 要 processData/contentType 两个 false);成功看 code:1,返回的相对路径记得拼域名;所有校验全部放在最前面,先拦坏文件再传。

本页重点

API 作用 参数怎么传 返回/结果 代码示例
File 拿到用户选的那张图 input 的 e.target.files[0] File 文件对象 let file = e.target.files[0];
FileReader.readAsDataURL 本地读文件、不联网 (文件) base64 图片字符串 reader.readAsDataURL(file);
FormData.append 把文件贴标签打包 ("file", 文件) formData 表单数据 formData.append("file", file);
$.ajax 往真实接口发 POST (url,{type:"POST",data,processData:false,contentType:false}) 请求结果 $.ajax({ url: API_URL, type:"POST", data: formData, processData:false, contentType:false });
res.code === 1 判断这次上传成没成 接口返回的字段 成立才成功 if (res.code === 1) { /* 成功 */ }