Skip to content

图片上传与网站换肤 ​

本页对应 1-5图片上传和网站换肤案例,共 3 段视频,约 39 分钟。

学习目标 ​

  1. 能说明图片上传的三步流程(选择图片 → 组装 FormData → 调用上传接口),并独立写出完整可运行的上传代码。
  2. 能使用 input[type=file] 的 change 事件,配合普通函数中的 this.files[0] 准确获取用户选中的 File 对象,并解释此处不能用箭头函数的原因。
  3. 能根据接口文档判断请求体格式,使用 new FormData() 与 append(键, 值) 组装数据,键名严格与文档参数名一致,并将该对象设置到请求配置的 data 属性上。
  4. 能通过浏览器网络面板核验请求中的表单数据与响应结果,取得服务器返回的图片 URL,并将其渲染到页面元素上。
  5. 能运用 label 的 for 属性关联隐藏的 file 输入框,实现“点击自定义按钮即触发文件选择”的效果,并将事件正确绑定在 input 元素上。
  6. 能使用 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 → 页面加载读取并判断后再设置。

课件图示 ​

课件第18页:1. 选择图片 2. FormData对象 内置对象,负责向服务器传递表单数据,

课件第21页:总结 总结 1. label标签使用技巧 2. 网站换肤 上传图片 设置body

课件第20页:需求  网站换肤(核心功能)  上传图片  设置body的背景图片  本

代码示例 ​

示例 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 找不到资源——这就是必须加判断的原因。
  • 先渲染缓存再上传新图,两段设置背景图的代码互不影响,换图后刷新仍保留最后一张。

常见错误 ​

  1. 选择文件后 this 不是 input 元素 → change 回调写成箭头函数,this 指向外层 → 改为普通 function,再用 this.files。
  2. 取不到图片文件对象 → 只写了 this.files,没有按索引取 → 使用 this.files[0]。
  3. 上传接口返回失败或参数为空 → FormData.append 的键名自己随便写 → 按接口文档参数名设置,如 image。
  4. 上传请求体格式不对 → 把文件或普通对象直接传给 body → 先装入 FormData,再设置到请求的 body。
  5. 设置背景图不生效 → backgroundImage 只写了 URL 字符串 → 写成 url(图片地址) 形式。
  6. 刷新后背景图丢失 → 只设置背景图,没有持久化 URL → 上传成功后 localStorage.setItem 保存,页面加载时 getItem 读取。
  7. 首次打开页面控制台报 404 → 无缓存时取到 null,仍把 null 设置给背景图 → 先判断 URL 有值再设置。
  8. 逻辑短路写法报语法错误 → 没加括号,运算符优先级导致解析异常 → 用括号包住要条件执行的赋值表达式。

练习 ​

  1. 给文件选择框绑定 change 事件,并在回调中获取用户选择的图片文件对象,打印其 name。
  2. 根据接口文档要求,使用 FormData 组装图片数据并调用上传接口,成功后打印服务器返回的图片 URL。
  3. 实现“更换背景图”功能:上传成功后把 URL 设置为 body 背景,并保存到 localStorage。
  4. 页面加载时读取背景图 URL;分别测试“有缓存”和“无缓存”两种情况,确保无缓存时不报 404。
  5. 排查一个现象:点击“更换皮肤”文字没有弹出文件选择框,但直接点击隐藏的 input 可以。请定位可能原因并修复。

原视频对照 ​

#视频时长
1图片上传13:45
2案例-网站换肤-核心功能11:50
3案例-网站换肤-缓存URL13:54

小结 ​

  1. 文件选择必须通过 change 事件获取,且回调要用普通函数保留 this。
  2. 用户选择的文件在 this.files[0] 中,不是 this.files 本身。
  3. 是否使用 FormData、键名写什么,必须以接口文档为准。
  4. 图片上传成功后,服务器返回的 URL 才是页面渲染和持久化要使用的数据。
  5. 设置背景图时要用 url(...) 包裹图片地址。
  6. localStorage 保存和读取的 key 必须一致。
  7. 无缓存时读取结果是 null,不能直接拿去设置背景图。
  8. 逻辑短路做条件赋值时,括号用于保证表达式按预期解析。