这页讲怎么让用户选完图片后立刻在页面上看到预览,不用等上传完。整个流程分两阶段:先本地预览(纯浏览器操作,不发网络请求),再真正上传到服务器(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。
普通键值对无法传递二进制图片文件,FormData 是浏览器专门用于文件上传的对象。
// 追加文件语法
// 上传硬性规则:
// 1. POST 请求,禁止手动设置 Content-Type
// 2. 浏览器自动生成 multipart/form-data 分隔头部
// 3. 手动覆盖会导致后端收不到图片
formData.append('字段名', File对象);
发送 FormData 必须增加两项配置,缺失会上传失败:
$.ajax({
url: "接口地址",
type: "POST",
data: formData,
// ★ 关闭 jQuery 自动序列化参数
processData: false,
// ★ 关闭默认表单请求头,交给浏览器自动处理
contentType: false
});
所有代码依赖 jQuery,页面头部先引入 CDN,业务代码写在 $(function() {}) DOM 就绪函数中:
< script src = "https: // cdn.jsdelivr.net/npm/jquery@3.
<
script >
$(function () {
// 所有业务代码写在这里
}); <
/script>
原生 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>
| 属性 | 单位/用途 |
|---|---|
| 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);
});
FileReader 仅做前端本地预览,不发送网络请求,不会上传服务器;作用:将本地图片转为 base64 字符串,直接赋值 img.src 实现即时预览。
/**
* 读取图片生成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() 塞进容器,浏览器立刻渲染,全程不发网络请求。
上面这个演示接的是真实接口,不是假数据。你可以真的选一张本地图片试试:先看到本地预览(这一步不联网),点上传后才真的发请求,成功后图片会换成服务器上的永久地址。
| 边界场景 | 判断写法 | 处理方式 |
|---|---|---|
| 空文件(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(浏览器自己判断的真实类型)二次校验。
< input type = "file"
id = "fileInput"
accept = "image/*"
style = "display:none;" > →真正的选文件框, 先藏起来看不见
$("#selectBtn").click(function () { $("#fileInput").click(); });
// 自定义按钮,偷偷点一下隐藏的 input
原生的文件选择框长得丑,而且每个浏览器长得都不一样,没法统一成设计稿的样子。所以项目里统一做法:把 input type="file" 用
display:none 藏起来,自己画一个好看的按钮,按钮被点的时候,在代码里偷偷 .click 一下那个隐藏的
input,文件选择弹窗就弹出来了。这叫"借壳"。
$("#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,都要把上一次的预览图、按钮状态全部重置,否则会出现"新文件选了,但页面还显示着旧图"的错位。
| 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 直接吃这个字符串就能显示。这一步完全不联网,所以预览秒出。
// 造一个快递袋(空的)
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: false 和 contentType:
false 这两行缺一不可。
③ 判断成功看 res.code === 1,返回的 res.url 是相对路径,必须拼域名。
绝大多数后端接口一次仅接收一张图片,多选图片需循环串行逐个调用上传接口,禁止一次性传入多文件。
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,刷新还在)。
① 可能在什么地方用:用户头像上传、商品详情页传多张实拍图、发布文章时插图、证件照上传审核——只要让用户"选个图传上来",都是这套 FileReader 预览 + FormData 上传 的写法。
② 常见的问题:传完图片显示成裂图(多半是忘了拼域名);点上传没反应(多半是字段名写错或 jQuery 少了 processData:false/contentType:false);连续选同一个文件不触发 change。
③ 解决思路:打开浏览器 F12 → Network,看这次上传请求:请求体里有没有文件(Payload)、Response 返回的 code 和 url 长什么样。裂图就手动把 res.url 拼上域名在地址栏打开验证;收不到文件就核对字段名是不是 file。
| 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) { /* 成功 */ } |