记一次图片上传的辛酸史

图片上传处理辛酸史

个人博客的图片需求

最近在写个人博客(前后台都得自己来摸索)

博客文章嘛,总得配个封面或者内容插图是吧!所以就摸索一下 图片上传图片存储 及发起请求 展示图片 的全过程吧,也记录一下整个探索历程的经过

处理方式及进行阶段

因为想到 Base64 编码可以直接用于 img 元素的 src 属性用于显示图片信息,所以自然而然地想到了这个方法。
相关代码如下:

// file 文件转 Base64 => 得到 file 的 Base64 编码 (返回的是 Promise 对象)
function getBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
  });
}

// 此方法是利用 canvas 对获取的 Base64 编码进行压缩处理
/**
 - base64Str 是未经压缩的 base64 编码
 - w 是压缩后图片的宽度像素值(number)
 - quality 压缩处理后的图片质量,用于 HTMLCanvasElement.toDataURL() 方法
*/
function compress(base64Str, w, quality) {
  let getMimeType = urlData => {
    let arr = urlData.split(",");
    let mime = arr[0].match(/:(.*?);/)[1]; // 截去 "data:image/xxx" 的前缀 "data"
    return mime;
  };
  let newImage = new Image(); // 同 document.createElement("img");
  let imgWidth, imgHeight;

  let promise = new Promise(resolve => (newImage.onload = resolve)); // img 元素加载完成触发
  newImage.src = base64Str; // src 置为源 Base64 编码
  return promise.then(() => {
  	// 原始图片的宽高
    imgWidth = newImage.width;
    imgHeight = newImage.height;
    let canvas = document.createElement("canvas");
    let ctx = canvas.getContext("2d");
    if (Math.max(imgWidth, imgHeight) > w) {
      // 设置 canvas 画布的宽高(保持图片原比例大小缩放)
      if (imgWidth > imgHeight) {
        canvas.width = w;
        canvas.height = (w * imgHeight) / imgWidth;
      } else {
        canvas.height = w;
        canvas.width = (w * imgWidth) / imgHeight;
      }
    } else {
      canvas.width = imgWidth;
      canvas.height = imgHeight;
    }
    ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空 canvas 画布
    ctx.drawImage(newImage, 0, 0, canvas.width, canvas.height); // 绘制图片
    let base64 = canvas.toDataURL(getMimeType(base64Str), quality);
    return base64;
  });
}
  1. 一阶段:通过监听 <input type="file" />change 方法,采用上面 getBase64 方法已经能获取到图片的 Base64 的编码了,马上兴高采烈地往后端传 —— BUT,现实总是残酷的,去后端一看请求到达的打印提示,请求体数据量过大!!!我再去请求前一看,一张图片的 Base64 的编码就 2MB+ 了。数据量太大了,直接写入数据库也太占地方了,对数据库的性能也不太好吧!(需要另寻他法)

  2. 二阶段:既然是 Base64 编码体积太大,那进行一下压缩呗!于是就有了上面的 compress 方法,原图本是 1920*1080 的,通过调用 compress(base64Str, 100, 0.5) 方法,将图片压缩至宽度 100px 大小,再看 Base 64 编码体积 15.7K,嗯,大小差不多了!!!ok,再去请求展示一下?!一测试,炸了,直接马赛克!(懵懂的我以为是无损压缩呢!)咋不过过脑子呢?正常图片放大都会糊,又不是 svg 这类矢量图。(再换)
    这种方式也不是没有应用空间,比如说:个人头像 这种很小张的小图片,在没有给予放大预览功能的条件下,存个 20pixel 大小的图片就已经够用了,那么这种情况就可以采用转 Base64编码再压缩处理,直接放进数据库存储也可;另外,雪碧图 也可这样处理。

  3. 三阶段:看了一些他人的博客文章,都是通过 formData 类来上传文件的,那就用起来!写个 demo 前端就用 jquery + ajax 方式发起上传文件的请求,后端通过 node + express + multer 处理一下,代码如下:

    前端部分:

    <!-- 相关样式什么的就不纠结了,只是功能点 -->
    <input type="file" id="inp">
    <button class="btn">upload</button>
    
    <script>
    let formData = new FormData();
    $("#inp").on("change", function(e) {
       // e.preventDefault();
       let files = this.files;
       for (let i = 0; i < files.length; i++) {
         formData.append("file", files[i]);
       }
     });
    
     $(".btn").on("click", () => {
       $.ajax({
         type: "POST",
         url: "http://localhost:9527/upload",
         data: formData,
         cache: false,
         contentType: false,
         processData: false,
         success(res) {
           console.log("res", res);
         },
         error(err) {
           console.log(err);
         }
       });
     })
    </script>
    

    后端部分:

    // ====================== app.js ======================
    const express = require("express");
    const router = require("./router");
    const app = express();
    const host = process.env.HOST || "127.0.0.1",
    	port = process.env.PORT || 9527;
    // 允许所有源跨域
    app.use('*', (req, res, next)=>{
      res.header("Access-Control-Allow-Origin", "*");
      res.header("Access-Control-Allow-Methods", "*");
      res.header("Access-Control-Allow-Credentials", "true");
      res.header("Access-Control-Allow-Headers", "Content-Type,Access-Token,adminid");
      res.header("Access-Control-Expose-Headers", "*");
      next();
    }); 
    app.use(router);
    app.listen(port, () => {
    	console.log(`server running at ${host}:${port} ...`);
    });
    
    // ====================== router.js ======================
    const express = require("express"),
      multer = require("multer"),
      router = express.Router();
    
    let storage = multer.diskStorage({
      destination(req, file, cb) {
      	// 可通过 req 携带存储路径,自定义存储位置
        cb(null, "./img/common"); // 图片存储路径
      },
      filename(req, file, cb) {
        cb(null, file.originalname); // 使用原文件名,反正是个小博客,自己注意一下命名就好,重复上传的文件就不会有多份了
      }
    });
    
    const upload = multer({
      storage
    });
    
    router.post("/upload", upload.single("file"), (req, res) => {
      let url = `http://${req.headers.host}/img/common/${req.file.originalname}`;
      res.send({
        code: 200,
        msg: "success",
        url
      });
    });
    
    router.get("/img/*", (req, res) => {
      res.sendFile(__dirname + "/" + req.url);
      console.log(`Request for ${req.url} recived...`);
    });
    
    module.exports = router;
    

    上面第三阶段的代码,实现图片快速上传至服务器并存储,通过 get 请求也能拿到对应的图片信息,这就完了吗?
    不,惊喜来了:

    Chrome 浏览器中,post /upload 请求是成功的,但是 failed to load reponse data,即对于我后端响应的结果加载失败了,换用 Firefox 浏览器看看呢?(看到一些文章说是返回的数据过大导致的,换用 Firefox 可以正常加载,喵喵喵???)结果如下:
    response
    但控制台一样 没有输出,通过如下代码,再测试一下:

    $(".btn").on("click", () => {
    	$.ajax({
    		type: "POST",
    		url: "http://localhost:9527/upload",
    		data: formData,
    		cache: false,
    		contentType: false,
    		processData: false,
    		success(res) {
    	       console.log("res", res);
    	       if(res.code === 200){
    				let img = new Image();
    				img.onload = () => {
    					img.src = res.url;
    					document.body.appendChild(img);
    				}
    		   }
        	},
         	error(err) {
           		console.log(err);
         	}
       	});
    })
    

    用 Chrome 浏览器上传图片,发现页面上会闪一下刚刚上传的图片,然后又立马消失。所以,就应该是图片上传成功后,会自动刷新页面导致重新加载的问题。那么只要解决页面重载的情况,拿到返回的 url 再插入数据库存储就可以大大节约数据库的存储空间了。
    今天就写到这儿吧,都凌晨 1:10 了。明天早上起来再看看怎么解决这个问题吧。

追加内容

关于昨天发现的提交 FormData 数据 会导致页面自动重新刷新的问题,在我今天的努力下,还是没有解决~ (╦_╦)

然后,因为我的博客项目是基于 @vue/cli 脚手架搭建的,所以又去测试一下 vue + axios + ElementUI 的 el-upload 组件 + multer 组合的上传图片功能。因为 el-upload 组件直接封装好了样式,就偷个懒了!

在上面内容的基础上,只对前端部分作更改说明

  1. 在博客项目中因为对 axios 有了一层封装,因而就不适合 FormData 类型的 post 请求,所以,对上传图片的请求方式进行单独封装:
    import axios from "./request";
    // 拦截器内层的请求方法; 这里就把他们写到这一个文件里面了(虽然我项目里面不是这样干的)
    function postUploadImage(url, data) {
      return axios({
        url,
        data,
        method: "post",
        headers: {
          cache: false,
          "Content-Type": "multipart/form-data", // 这条不加会默认是这个格式,保险起见还是写上
          contentType: false, // 告诉 axios 不要去更改 Content-Type 请求头
          processData: false // 告诉 axios 不要去处理发送的数据(重要参数)
        }
      });
    }
    
    export const uploadImage = data => postUploadImage("/api/images/upload_img", data);
    
  2. 然后就是监听 el-upload 组件的 on-change 事件了,返回的参数为 filefileList,然后这一点,我又卡住了!因为我想的虽然是 element 进行了封装,但是返回的参数 file 应该是原生的 file,然后就开始了 漫漫长征路,直接上代码吧:
    function handleChangeFile(file) {
    	let formData = new FormData();
    	let pro = new Promise(resolve => { // 后面打印 formData 一直空对象,我还以为是因为 append 失败
    		formData.append("file", file); // ========= 就这里,巨坑
    		resolve();
    	});
    	pro.then(() => {
    		postUploadImage(formData).then(res => {
    			if (res.success) {
    				console.log(res);
    			} else {
    				this.$message.warning(res.msg);
    			}
    		}); // .catch 错误处理已经在拦截器处理了
    	});
    }
    
    尝试了很多遍之后,后台就一直拿不到图片数据,而且,处理了请求的响应结果中:imgUrl: "http://localhost:9527/images/undefined",也就是拿不到 file 对象的 nameoriginalname 等等这些属性值,毕竟访问对象里不存在的属性值都是 undefined(此时的 file 是空对象啊)。
    等我睡了一个午觉起来,再去 MDN 查了一下 FormData() 的 api ,看到 append 可以传递三个参数,可以指定文件名(那年轻的脑子里就冒出来一个想法:给个文件名是不是就可以正常返回了?),去试试。这一试,服务器的错误提示就来了:大致意思就是 formData.append() 方法的第二个参数不是 blob 对象;到这儿,才终于知道问题在哪儿了!虽然没有详细去了解 blob 对象的相关内容,但反正就是数据传值有问题了。
    然后才知道 el-upload 组件将原生 file 对象封装在返回参数的 raw 属性下(file.raw),这家伙,折磨得我哟,吐血!
    替换为 formData.append("file", file.raw); 后,喜极而泣!!!
  3. 在前面的 post 请求提交 formData 后会自动刷新页面的情况,用 axios 倒并没有出现。所以这得去专门了解一下了,看看什么时候有时间能找人去请教一下吧!有知道的小伙伴,也麻烦跟我说一声!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值