主题
图片上传与网站换肤
本页对应 1-5图片上传和网站换肤案例,共 3 段视频,约 39 分钟。
学习目标
- 能说明图片上传的三步流程(选择图片 → 组装 FormData → 调用上传接口),并独立写出完整可运行的上传代码。
- 能使用
input[type=file]的change事件,配合普通函数中的this.files[0]准确获取用户选中的 File 对象,并解释此处不能用箭头函数的原因。 - 能根据接口文档判断请求体格式,使用
new FormData()与append(键, 值)组装数据,键名严格与文档参数名一致,并将该对象设置到请求配置的data属性上。 - 能通过浏览器网络面板核验请求中的表单数据与响应结果,取得服务器返回的图片 URL,并将其渲染到页面元素上。
- 能运用 label 的
for属性关联隐藏的 file 输入框,实现“点击自定义按钮即触发文件选择”的效果,并将事件正确绑定在 input 元素上。 - 能使用
localStorage的setItem/getItem完成图片 URL 的持久化与回显,并用逻辑与短路(含提升优先级的括号)避免无缓存时把null写入背景图而产生 404 错误。
知识点
目标与整体思路
图片上传的目标,是把本地图片传到服务器,再让网页通过服务器返回的 URL 显示图片。
为什么需要:浏览器里直接选中的图片只是本地临时文件,刷新或换设备就不一定还能访问;上传到服务器后,服务器保存文件并返回在线 URL,页面再用这个 URL 显示,才能“随时随地访问”。
怎么想:不要把“选图片”和“显示图片”混成一步。正确链路是:选文件 → 装进 FormData → 调上传接口 → 拿返回 URL → 设置给 img.src。
怎么做:先准备文件选择元素,监听 change,拿到 File 对象;再按接口文档用 FormData 组装;最后提交并渲染。
易错点:图片上传传的是“文件流/表单数据”,不是普通 JSON 对象;接口要求 FormData 时,不能直接传一个普通对象。
第一步:用文件选择元素拿到图片文件
为什么需要:用户必须先从本地选择一张图片,浏览器才能拿到对应的文件对象。
怎么想:用 <input type="file"> 选择文件,选择变化时触发 change 事件;事件回调里通过 this.files 取文件列表,再用索引 [0] 取第一张。
怎么做:注意回调要写成普通函数,因为要用 this 指向当前 input。转写稿里“chan/劝局”应纠正为 change,“fas/files”应纠正为 files,“尾数组”应纠正为“伪数组”。
易错点:箭头函数没有自己的 this,这里容易拿不到当前元素;this.files 是伪数组,有 length 和数字索引,不能直接当单个文件用。
html
<input type="file" id="upload">
<img class="image" alt="服务器返回的图片">
<script>
document.querySelector('#upload').addEventListener('change', function () {
// this 指向 input;files 是伪数组,[0] 才是选中的文件
const file = this.files[0]
console.log(file)
})
</script>调试时转写稿的做法是:先打印 this,确认指向正确;再打印 this.files,看到类似 {0: File, length: 1} 的伪数组;最后写 this.files[0],看到 File 对象,里面有 name 等属性,说明图片已经选中。
第二步:用 FormData 按接口文档装数据
为什么需要:图片是文件,不是以前的数字和字符串。接口文档如果要求 body 参数为 FormData,就必须用浏览器内置的 FormData 来装文件。
怎么想:转写稿里的“fo对象/方倍/封配/f date”都应纠正为 FormData。它是“表单数据”对象,日常开发常用来传文件。
怎么做:先 new FormData(),再用 append(键, 值) 添加数据。键名不能乱写,要去接口文档确认;转写稿中接口参数名是 image,所以写 append('image', file)。
易错点:append 的键名必须和接口文档一致;值放选中的 File 对象,不要放 this.files 整个伪数组;不要误写成普通对象直接提交。
js
const file = this.files[0]
const data = new FormData()
data.append('image', file) // 'image' 来自接口文档第三步:提交并拿到服务器返回的 URL
为什么需要:FormData 只是浏览器里组装好的数据,还要发给服务器,服务器保存图片后才返回可访问的 URL。
怎么想:接口地址、请求方法、参数位置都看接口文档。转写稿里“exes”应纠正为 axios;接口要求 body 参数是 FormData,所以把 data 设置到请求的 data 位置。
怎么做:用 axios.post(接口地址, data) 提交,在 .then 里接收响应,按接口返回路径取 URL。转写稿中是从返回的 data 里找到 url,具体路径以接口文档/控制台为准。
易错点:不要自己猜返回结构;上传成功后要去“网络”面板看请求体,确认是表单数据、键是 image、值是二进制文件;预览里看到“上传成功”和 URL 才说明接口通了。
js
axios.post('上传接口地址', data).then(res => {
// 具体路径按接口返回确认,转写稿是在 data 里取 url
const url = res.data.data.url
console.log(url)
})第四步:把 URL 设置给 img 的 src 并验证
为什么需要:服务器返回的是图片 URL,页面还要把它加载出来,用户才能看到上传结果。
怎么想:用 img 标签显示,把 URL 设置到 src 属性。转写稿中先 console.log(res),展开找到 URL,再复制属性路径,避免手写错。
怎么做:document.querySelector('.image').src = url。换一张图片再选,src 会变化,页面也会渲染新图,说明链路完整。
易错点:src 写错、URL 取错层级都会显示不出来;如果后续做“网站换肤”,则是把 URL 设置给 body 的 background-image,写法要带 url(...);若用 localStorage 缓存 URL,存和取的 key 要一致。转写稿里“local storage”即 localStorage,“set item/get item”即 setItem/getItem。
小结
图片上传的核心思路:先用文件选择元素拿到文件对象,再装入 FormData,发给服务器,得到图片在服务器的 URL,最后通过 img 标签加载显示。
答案
- 先用文件选择元素,获取到文件对象,然后装入 FormData 表单对象中,再发给服务器,得到图片在服务器的 URL 网址,再通过 img 标签加载图片显示
案例目标与实现思路
为什么需要:网站换肤要允许用户选择本地图片作为页面背景,并且刷新或重新打开页面后皮肤仍然存在。
怎么想:需求拆成两件事:一是“换肤”,把用户选的图片上传到服务器,拿到在线 URL 后设置给 body 背景;二是“持久化”,把 URL 存到 localStorage,页面打开时再读出来使用。
怎么做:先运行备课代码看效果,再按“选择图片 → 上传拿 URL → 设置背景 → 保存 URL → 页面加载读取并判断”的顺序实现。
易错点:不要保存本地文件路径,要保存服务器返回的在线 URL;刷新后皮肤丢失,通常就是只设置了背景、没有做本地持久化。
选择图片与上传:从 input 到 FormData
为什么需要:背景图必须先从用户本地选出来,再传给服务器,服务器存好后返回可访问的 URL。
怎么想:用 input type="file" 选择图片,监听 change 事件;事件回调里通过 this.files[0] 拿到文件。接口文档要求 FormData 格式时,用 new FormData() 组装数据,键名按接口文档写,本案例是 image。
怎么做:页面中“更换皮肤”常通过 label 的 for 属性关联隐藏的 input,点击 label 等同于点击 input,但事件仍要绑在 input 上。
js
const fileInput = document.querySelector('.upload');
fileInput.addEventListener('change', function () {
// 必须用普通函数,才能用 this 拿到 input
const file = this.files[0];
const data = new FormData();
data.append('image', file); // 键名按接口文档
axios.post('按接口文档填写上传地址', data).then((res) => {
const url = res.data.url;
// 这里先拿到服务器返回的图片 URL
console.log(url);
});
});易错点:回调写成箭头函数会导致 this 不是 input;this.files 是伪数组,要加索引 [0];FormData.append 的键名不能乱写;上传接口的地址、方法以接口文档为准。调试时可打开网络面板,确认请求体是表单数据、键是 image,预览里能看到上传成功和返回 URL。
设置 body 背景图
为什么需要:拿到服务器 URL 后,要让它成为整个页面的背景。
怎么想:背景图是样式,设置到 document.body.style.backgroundImage,值必须是 url(图片地址) 形式。
怎么做:
js
document.body.style.backgroundImage = `url(${url})`;易错点:漏写 url() 或小括号;把变量名写错;以为设置给 body 标签本身而不是 style。设置后可在审查元素里看到 body 应用了背景图。
用 localStorage 持久化 URL
为什么需要:只设置背景图,刷新页面后样式会丢失。
怎么想:上传成功拿到 URL 时顺手保存;页面一打开就读取,有值才设置背景。
怎么做:
js
// 上传成功后保存
localStorage.setItem('image', url);
// 页面打开时读取
const url = localStorage.getItem('image');
url && (document.body.style.backgroundImage = `url(${url})`);易错点:存和取的 key 要一致,本案例都用 image;localStorage 存的是字符串;保存的是服务器 URL,不是 file 对象。调试时可在“应用/本地存储空间”里查看键值是否写入。
无缓存判断与逻辑短路调试
为什么需要:第一次打开页面时没有缓存,getItem 得到 null。如果直接拼成 url(null) 设置背景,浏览器会去请求错误地址,出现 404。
怎么想:只有 URL 存在时才执行设置背景的代码。可以用 if,也可以用逻辑与短路。
怎么做:正确写法要给赋值整体加小括号:
js
url && (document.body.style.backgroundImage = `url(${url})`);错误写法:
js
// url && document.body.style.backgroundImage = `url(${url})`;易错点:逻辑与优先级高于赋值,不加括号会被解析成错误结构而报错;小括号是为了保证右侧赋值作为一个整体被短路控制。调试时删除本地存储再刷新,可看到 404;加括号后无缓存不报错,有缓存刷新仍生效。
小结
localStorage.getItem('key') 是取值,localStorage.setItem('key', 'value') 是赋值。本案例的核心就是:上传图片拿 URL → 设置 body 背景 → 保存 URL → 页面加载读取并判断后再设置。
课件图示



代码示例
示例 1:图片上传(选择图片 → FormData → 上传 → 渲染)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片上传</title>
</head>
<body>
<input type="file" class="upload">
<br>
<img class="image" alt="上传后的图片" width="200">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 上传图片接口地址,来自接口文档
const UPLOAD_URL = '请替换为接口文档中的上传图片地址'
document.querySelector('.upload').addEventListener('change', function () {
// 1. 选择图片:files 是伪数组,用索引 0 取出 file 对象
const file = this.files[0]
if (!file) return
// 2. 装入 FormData:键名 image 来自接口文档
const data = new FormData()
data.append('image', file)
// 3. 调用上传接口,拿服务器返回的图片 url
axios({
url: UPLOAD_URL,
method: 'post',
data: data
}).then(res => {
const url = res.data.url
// 4. 把 url 设置给 img 的 src 显示出来
document.querySelector('.image').src = url
})
})
</script>
</body>
</html>关键行与易错点
this.files[0]:files是伪数组(有length、属性名是数字),必须用索引取;this能指向 input,前提是回调写成 普通函数,写成箭头函数this就丢了。new FormData()+append(键, 值):图片是文件不是数字字符串,接口文档要求form-data格式时必须用它;键名不能乱写,要照抄接口文档(这里是image)。axios({ url, method:'post', data }):FormData 对象直接放在data属性上,axios 会自动带上正确的请求头,不要自己手写Content-Type。res.data.url:服务器返回结构一般是{ message, data: { url } },路径写错就是undefined,src为空图片不显示。
示例 2:网站换肤核心功能(label 触发 + 设置 body 背景图)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>网站换肤</title>
<style>
body {
min-height: 100vh;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* 文件选择框默认样式丑,隐藏起来,用 label 代替点击 */
.upload { display: none; }
.change-skin {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 16px;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<!-- label 的 for 关联 input 的 id,点 label 等于点 input -->
<label class="change-skin" for="upload">更换皮肤</label>
<input type="file" class="upload" id="upload">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const UPLOAD_URL = '请替换为接口文档中的上传图片地址'
// 事件一定要绑在 input 上,不是绑在 label 上
document.querySelector('.upload').addEventListener('change', function () {
// 1.1 选择图片
const file = this.files[0]
if (!file) return
// 1.2 按接口文档要求的 form-data 格式组装
const data = new FormData()
data.append('image', file)
axios({
url: UPLOAD_URL,
method: 'post',
data: data
}).then(res => {
const url = res.data.url
// 1.3 把 url 设置给 body 的背景图
document.body.style.backgroundImage = `url(${url})`
})
})
</script>
</body>
</html>关键行与易错点
label for="upload"对应input id="upload":这就是“点文字也能选文件”的技巧;input用display: none隐藏后,事件照样能触发。- 事件监听仍然写在
.upload(input)上,绑到 label 上this.files就拿不到了。 document.body.style.backgroundImage = \url(${url})`:样式里必须套一层url(...)`,直接赋地址字符串不生效;反引号里的小括号别漏。data: data可以简写成data(变量名刚好同名),但别把data写成params,params是拼在 url 上的查询参数。
示例 3:换肤 + localStorage 持久化(含逻辑与短路写法)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>网站换肤 - 持久化</title>
<style>
body {
min-height: 100vh;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.upload { display: none; }
.change-skin {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 16px;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<label class="change-skin" for="upload">更换皮肤</label>
<input type="file" class="upload" id="upload">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const UPLOAD_URL = '请替换为接口文档中的上传图片地址'
// ===== 2.2 读取图片 url(页面打开时立即执行)=====
const cachedUrl = localStorage.getItem('image')
// 逻辑与短路:有值才设置背景图,为 null 就不执行,避免 404
cachedUrl && (document.body.style.backgroundImage = `url(${cachedUrl})`)
document.querySelector('.upload').addEventListener('change', function () {
const file = this.files[0]
if (!file) return
const data = new FormData()
data.append('image', file)
axios({
url: UPLOAD_URL,
method: 'post',
data: data
}).then(res => {
const url = res.data.url
// 1.3 换肤
document.body.style.backgroundImage = `url(${url})`
// 2.1 顺手缓存 url
localStorage.setItem('image', url)
})
})
</script>
</body>
</html>关键行与易错点
localStorage.setItem('image', url)/getItem('image'):存取用的 key 必须完全一致,上面存image下面取image,写错就永远是null。- 读取代码直接写在脚本顶层,页面一打开就执行;如果写在
change回调里,刷新后皮肤就恢复不了。 cachedUrl && (...)的小括号不能删:=的优先级低于&&,不加括号会被解析成(cachedUrl && document.body.style.backgroundImage) = url(...),直接语法报错。当然用if (cachedUrl) { ... }也完全可以。- 不做判断时,
null会被拼成url(null)设置给背景,控制台出现 404 找不到资源——这就是必须加判断的原因。 - 先渲染缓存再上传新图,两段设置背景图的代码互不影响,换图后刷新仍保留最后一张。
常见错误
- 选择文件后
this不是 input 元素 →change回调写成箭头函数,this指向外层 → 改为普通function,再用this.files。 - 取不到图片文件对象 → 只写了
this.files,没有按索引取 → 使用this.files[0]。 - 上传接口返回失败或参数为空 →
FormData.append的键名自己随便写 → 按接口文档参数名设置,如image。 - 上传请求体格式不对 → 把文件或普通对象直接传给
body→ 先装入FormData,再设置到请求的body。 - 设置背景图不生效 →
backgroundImage只写了 URL 字符串 → 写成url(图片地址)形式。 - 刷新后背景图丢失 → 只设置背景图,没有持久化 URL → 上传成功后
localStorage.setItem保存,页面加载时getItem读取。 - 首次打开页面控制台报 404 → 无缓存时取到
null,仍把null设置给背景图 → 先判断 URL 有值再设置。 - 逻辑短路写法报语法错误 → 没加括号,运算符优先级导致解析异常 → 用括号包住要条件执行的赋值表达式。
练习
- 给文件选择框绑定
change事件,并在回调中获取用户选择的图片文件对象,打印其name。 - 根据接口文档要求,使用
FormData组装图片数据并调用上传接口,成功后打印服务器返回的图片 URL。 - 实现“更换背景图”功能:上传成功后把 URL 设置为
body背景,并保存到localStorage。 - 页面加载时读取背景图 URL;分别测试“有缓存”和“无缓存”两种情况,确保无缓存时不报 404。
- 排查一个现象:点击“更换皮肤”文字没有弹出文件选择框,但直接点击隐藏的 input 可以。请定位可能原因并修复。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 图片上传 | 13:45 |
| 2 | 案例-网站换肤-核心功能 | 11:50 |
| 3 | 案例-网站换肤-缓存URL | 13:54 |
小结
- 文件选择必须通过
change事件获取,且回调要用普通函数保留this。 - 用户选择的文件在
this.files[0]中,不是this.files本身。 - 是否使用
FormData、键名写什么,必须以接口文档为准。 - 图片上传成功后,服务器返回的 URL 才是页面渲染和持久化要使用的数据。
- 设置背景图时要用
url(...)包裹图片地址。 localStorage保存和读取的 key 必须一致。- 无缓存时读取结果是
null,不能直接拿去设置背景图。 - 逻辑短路做条件赋值时,括号用于保证表达式按预期解析。