把这页想成"快递寄一件衣服":你在店里先拿在手里看一眼(本地预览),确认没破没脏,再打包交给快递员(上传服务器),最后快递给你一个取件地址(线上图片 URL)。整个流程就四步:选图 → 本地预览 → 上传 → 拼地址显示。难点不在上传本身,而在上传前要把空文件、假图片、超大图、坏图统统挡在门外。本页要牢牢记住三个 API:File(你选的那个文件)、FileReader(本地预览)、FormData(打包上传)。
上面这个演示接的是真实接口,不是假数据。你可以真的选一张本地图片试试:先看到本地预览(这一步不联网),点上传后才真的发请求,成功后图片会换成服务器上的永久地址。输出框里会把每一步都打出来,照着它理解下面的讲解。
// 请求:POST http://jiekou.jinghongs.top/exhibition/Image/upload → 把文件寄到这个网址
// 请求体:FormData(字段名 file,值为 File 对象) → 就像一个快递袋,袋上贴着标签 file
// 成功响应(接口原样返回):→ 服务器处理完,回给你这么一段 JSON
{
"code": 1, // 1 = 成功,0 = 失败 → 本接口规定 code 是 1 才算传成功
"msg": "上传成功", // 一句给人看的提示语
"url": "/uploads/image/d12119a4759fa909dcc4283221c4c773.jpg" // ← 相对路径!→ 只写了"楼号",没写"小区名"
}
// ★★★ 重点:返回的 url 是【相对路径】,必须拼接域名才能显示:
const BASE_URL = "http://jiekou.jinghongs.top"; // 先把"小区名"(域名)单独存成一份
const fullUrl = BASE_URL + res.url(); // 小区名 + 楼号,拼成完整网址
// fullUrl = "http://jiekou.jinghongs.top/uploads/image/...jpg" → 拼好后这就是能直接打开的地址
// 不拼接的话,图片会以【当前页面】为根目录去找,必然 404 → 直接拿楼号当网址,浏览器当然找不到
记住三个动作:① 判断成功看 res.code === 1(不是 0,本接口规定 1 才是成功);② 接口返回的 res.url 长这样 /uploads/image/xxx.jpg,它是个"相对路径",就像快递只写了"3 号楼"没写小区名;③ 必须把
域名拼接上去:"http://jiekou.jinghongs.top" +
res.url。如果你偷懒直接把 res.url 塞进 img.src,图片会以"你现在打开这个 html
的文件夹"为根去找,结果就是裂图。凡是接口返回以 / 开头的路径,都要按项目域名拼接后才能用。
| 边界场景 | 判断写法 | 处理方式 |
|---|---|---|
| 空文件(0 字节) |
file.size === 0 // 文件大小是 0 字节 → 空文件,拒收 |
提示并 return,不进预览 |
| 非图片(绕过 accept) |
!file.type.startsWith("image/") // 浏览器认出的真实类型不是 image/ 开头 → 假图片,拒收
|
提示并 return |
| 超大图片(5MB 上限) |
file.size > 5 * 1024 * 1024 // 文件字节数超过 5MB → 太大,拒收 |
提示并 return,禁止预览/上传 |
| 损坏图片 |
reader.onerror // 读取过程中出错 → 文件损坏,走这个回调提示 |
捕获异常,提示"文件损坏" |
| 没选文件 |
files.length === 0 // 一个文件都没选 → 上传按钮先置灰 |
上传按钮置灰 disabled |
为什么要在最前面校验?因为一旦放行,后面就要白跑一遍"读文件、传网络",纯浪费。accept="image/*" 只是"文件弹窗默认帮你过滤图片",它拦不住懂行的人——把一个 .txt 改后缀成 .jpg 就能绕过。所以必须用 file.type(浏览器自己判断的真实类型)二次校验。如果你只信 accept 不信 file.type,就会把伪图片传上去,服务器那边报错,锅又回到前端。
<input type="file" id="fileInput" accept="image/*" style="display:none;"> → 真正的选文件框,先藏起来看不见
$("#selectBtn").click( => $("#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 本地预览 | fetch / $.ajax 线上上传 |
|---|---|
| 不发任何网络请求 | POST 请求真发到服务器 |
| 把图片转成 base64 临时字符串 | 把原始 File 对象通过 FormData 传走 |
| 页面一刷新就没了 | 服务器存下来,返回永久地址 |
| 只是给用户"提前看一眼效果" | 完成数据真正的持久化 |
// 封装成 Promise,单图、多图都能复用 → 把"读本地文件"包成一个能 await 的函数
function getPreviewUrl(file) { // 传进来一个文件
return new Promise((resolve, reject) => { // 返回一个"将来会有结果"的承诺
const reader = new FileReader(); // 新建一个"本地读取器" → 浏览器提供的读文件工具
reader.onload = e => resolve(e.target.result); // 读完 → 把 base64 交出去
reader.onerror = => reject("文件损坏,无法读取预览"); // 读失败 → 走失败分支
reader.readAsDataURL(file); // ★ 开始异步读本地文件 → 读成一串 base64
});
}
const preview = await getPreviewUrl(file); // 等它读完 → 拿到那张图的 base64
img.src = preview; // 直接拿来当图片地址 → 浏览器就显示出来了
FileReader.readAsDataURL 做的事,就是把你硬盘上那张图,在浏览器里翻译成一长串
data:image/png;base64,xxxx 的字符串,然后 img.src 直接吃这个字符串就能显示。这一步完全不联网,所以预览秒出。如果你误以为"预览一下也要上传服务器",那就大错特错了——预览只是给用户看,上传才是真提交。
const formData = new FormData(); // 造一个快递袋(空的)
formData.append("file", targetFile); // ★ 字段名必须和接口文档一致(本接口:file)→ 把文件贴标签 file 装进去
// 原生 fetch 写法:→ 不用 jQuery 时的原生写法
fetch("http://jiekou.jinghongs.top/exhibition/Image/upload", { // 把快递寄到这个网址
method: "POST", // 用 POST 方式提交
body: formData // 浏览器自动生成 multipart 请求头,别手动设 → 把整个快递袋塞进去
})
.then(res => res.json) // 服务器回话后,把它解析成 JSON
.then(res => { // 拿到解析好的结果
if (res.code === 1) { // ★ 判断 code === 1 才算成功 → 本接口规定 1 才成功
const fullUrl = "http://jiekou.jinghongs.top" + res.url(); // ★ 拼接域名!→ 相对路径前补上域名
$("#preview").attr("src", fullUrl); // 把图片的 src 换成这个完整地址
}
});
// jQuery $.ajax 写法(课件原版,多两个必写项):
$.ajax({ // 用 jQuery 发请求
url: "http://jiekou.jinghongs.top/exhibition/Image/upload", // 寄到这个网址
type: "POST", // POST 提交
data: formData, // 数据就是那个快递袋
processData: false, // ★ 关闭 jQuery 自动把参数转成字符串,必写 → 别让它把文件转成乱码
contentType: false, // ★ 关闭默认请求头,让浏览器自动生成 multipart 边界,必写 → 让浏览器自己设请求头
success: function (res) { // 成功回调,res 是服务器回的 JSON
if (res.code === 1) { // 同样看 code 是不是 1
$("#preview").attr("src", "http://jiekou.jinghongs.top" + res.url); // 拼域名后贴到 img 上
}
}
});
三个必背点,少一个都传不上去:
① FormData.append("file", 文件),这里的字段名 file
必须和接口文档一字不差。如果文档写的是别的名字(比如 img),你就得照文档写;写错字段名,后端收不到文件。
② 用 jQuery 传 FormData,processData: false 和 contentType:
false 这两行缺一不可。因为 jQuery
默认会把数据"序列化"成字符串,还会自动加一个请求头,这两件事都会破坏文件上传。如果你少写其中一行,就会发现"请求发出去了,后端却收不到图片",排查半天。
③ 判断成功看 res.code === 1,返回的 res.url 是相对路径,必须拼域名。另外上传期间要把按钮锁住(disabled + 文字改"上传中"),防止用户狂点重复提交。
// 数据:每张图一个对象,记录文件、本地预览、线上地址
let imgList = [ // 一个数组,装所有选的图
{ file: File对象, preview: "base64...", onlineUrl: "" } // 每项:原始文件 + 本地预览 + 传完后存线上地址
];
// 批量按钮:for + await,一张传完再传下一张(串行)
$("#batchUpload").click(async function () { // 点"批量上传"按钮
$btn.prop("disabled", true).text("正在逐张上传..."); // 先把按钮锁上,文字改成上传中
try { // 万一中间出错,也要能走到 finally
for (let i = 0; i < imgList.length; i++) { // 一张一张循环
imgList[i].onlineUrl = await uploadOneFile(imgList[i].file); // ★ await 等这张传完 → 这张拿到线上地址了,才传下一张
}
alert("全部" + imgList.length + "张上传完成"); // 全传完弹个提示
} catch (err) { // 任何一张传失败,都进这里
alert("上传中断: " + err); // 把错误弹出来
} finally { // 不管成功失败都执行
$btn.prop("disabled", false).text("批量逐张上传"); // 无论成败都恢复按钮 → 解锁,文字还原
}
});
// 删除某张图:imgList.splice(下标, 1) 后重新渲染 → 从数组里删一项,再重画一遍列表
绝大多数后端接口一次只收一张图。所以你选了 5 张,就得循环调用 5 次单图接口。关键是用 for + await 做成串行:一张传完,再传下一张。如果你图省事用 forEach 配 Promise 一把全发出去(并发),服务器可能直接限流、连接被打满,或者顺序乱掉。串行慢一点,但稳。
① 可能在什么地方用:用户头像上传、商品详情页传多张实拍图、发布文章时插图、证件照上传审核——只要让用户"选个图传上来",都是这套 FileReader 预览 + FormData 上传 的写法。
② 常见的问题:传完图片显示成裂图(多半是忘了拼域名);点上传没反应(多半是字段名写错或 jQuery 少了 processData:false/contentType:false);连续选同一个文件不触发 change;传了个改后缀的假图片也被放过去了。
③ 解决思路:打开浏览器 F12 → Network,看这次上传请求:请求体里有没有文件(Payload)、Response 返回的 code 和
url 长什么样。裂图就手动把 res.url 拼上域名在地址栏打开验证;收不到文件就核对字段名是不是 file;预览不出图就检查 file.type 是不是以 image/
开头。