图片上传处理辛酸史
个人博客的图片需求
最近在写个人博客(前后台都得自己来摸索)
博客文章嘛,总得配个封面或者内容插图是吧!所以就摸索一下 图片上传
、图片存储
及发起请求 展示图片
的全过程吧,也记录一下整个探索历程的经过
处理方式及进行阶段
因为想到 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;
});
}
-
一阶段
:通过监听<input type="file" />
的change
方法,采用上面getBase64
方法已经能获取到图片的 Base64 的编码了,马上兴高采烈地往后端传 —— BUT,现实总是残酷的,去后端一看请求到达的打印提示,请求体数据量过大
!!!我再去请求前一看,一张图片的 Base64 的编码就2MB+
了。数据量太大了,直接写入数据库也太占地方了,对数据库的性能也不太好吧!(需要另寻他法) -
二阶段
:既然是 Base64 编码体积太大,那进行一下压缩呗!于是就有了上面的compress
方法,原图本是1920*1080
的,通过调用compress(base64Str, 100, 0.5)
方法,将图片压缩至宽度100px
大小,再看 Base 64 编码体积15.7K
,嗯,大小差不多了!!!ok,再去请求展示一下?!一测试,炸了,直接马赛克!(懵懂的我以为是无损压缩呢!)咋不过过脑子呢?正常图片放大都会糊,又不是 svg 这类矢量图。(再换)
这种方式也不是没有应用空间,比如说:个人头像
这种很小张的小图片,在没有给予放大预览功能的条件下,存个 20pixel 大小的图片就已经够用了,那么这种情况就可以采用转 Base64编码再压缩处理,直接放进数据库存储也可;另外,雪碧图
也可这样处理。 -
三阶段
:看了一些他人的博客文章,都是通过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 可以正常加载,喵喵喵???)结果如下:
但控制台一样没有输出
,通过如下代码,再测试一下:$(".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 组件直接封装好了样式,就偷个懒了!
在上面内容的基础上,只对前端部分作更改说明
- 在博客项目中因为对 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);
- 然后就是监听 el-upload 组件的 on-change 事件了,返回的参数为
file
和fileList
,然后这一点,我又卡住了!因为我想的虽然是 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 对象的name
、originalname
等等这些属性值,毕竟访问对象里不存在的属性值都是undefined
(此时的 file 是空对象啊)。
等我睡了一个午觉起来,再去MDN
查了一下FormData()
的 api ,看到 append 可以传递三个参数,可以指定文件名(那年轻的脑子里就冒出来一个想法:给个文件名是不是就可以正常返回了?),去试试。这一试,服务器的错误提示就来了:大致意思就是formData.append() 方法的第二个参数不是 blob 对象
;到这儿,才终于知道问题在哪儿了!虽然没有详细去了解 blob 对象的相关内容,但反正就是数据传值有问题了。
然后才知道 el-upload 组件将原生 file 对象封装在返回参数的 raw 属性下(file.raw
),这家伙,折磨得我哟,吐血!
替换为formData.append("file", file.raw);
后,喜极而泣!!! - 在前面的 post 请求提交 formData 后会自动刷新页面的情况,用 axios 倒并没有出现。所以这得去专门了解一下了,看看什么时候有时间能找人去请教一下吧!有知道的小伙伴,也麻烦跟我说一声!