图片在上传七牛前,做图片的压缩优化。这样运营在上传图片时,方便将比较大的图片压缩优化,减少H5请求图片的大小,以提高加载速度。
- 七牛cdn的前端js sdk在图片上传七牛前,可以设置压缩选项。
可以参考developer.qiniu.com/kodo/1283/j… ,但是因为是基于canvas这个前端浏览器的api,压缩效果一般。
压缩之后的大小有时比压缩前更大。要设置noCompressIfLarger将这样的压缩舍去。所以这样的方式不行。
- 用nodejs在中间处理下图片压缩效果比较好。
前端页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>图片上传</title>
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<body>
<div class="upload-container">
<label class="btn">
<input name="file"
type="file"
id="uploadInput"
accept="image/*"
required="required">
</label>
<button class="btn" style="line-height: 22px;" id="submit" onclick="submitData()">确认上传</button>
</div>
<div class="image-container">
<div><img id="before" src="" alt=""></div>
</div>
<div id="resultData" style="display: none;">
<div>接口响应数据:</div>
<pre id="result"></pre>
</div>
<script>
let file;
uploadInput.onchange = function (e) {
if(e.target.files[0]) {
file = e.target.files[0];
getImgUrl('before', window.URL.createObjectURL(this.files[0]))
}
}
async function submitData() {
if(!file) {
return
}
const res = await upload(file)
const data = res.data
result.innerHTML = JSON.stringify(data, null, 2);
resultData.style.display = 'block';
}
function upload(file) {
const formData = new FormData()
formData.append('file', file)
return axios({
method: 'POST',
url: '/uploadImg',
data: formData
})
}
function getImgUrl(id, src) {
const img = document.getElementById(id)
img.src = src
}
</script>
</body>
</html>
Node.js服务
import fs from 'fs';
import { dirname, join } from 'path';
import FormData from 'form-data';
import axios from 'axios';
import Koa from 'koa';
import Router from 'koa-router';
import multer from 'koa-multer';
import bodyparser from 'koa-bodyparser';
import { fileURLToPath } from 'url';
import tinyImage from './tiny/index.js';
import {
delFile
} from "./tiny/promiseApi.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = new Koa();
const router = new Router();
app.use(bodyparser());
const getDateStr = () => {
const d = new Date();
let month = (d.getMonth()+1);
month = month > 9 ? month : '0' + month
let day = d.getDate();
day = day > 9 ? day : '0' + day
return d.getFullYear() + '' + month + '' + day
}
const storage = multer.diskStorage({
// 存储的位置
destination: join(__dirname, "source"),
// 文件名
filename(req, file, cb) {
cb(null, getDateStr() + '_' + new Date().valueOf() + '_' + file.originalname);
},
});
const upload = multer({ storage });
//上传图片到七牛
const uploadImgToQiniu = (file, bufferStream, uptoken) => {
//withCredentials 禁止携带cookie,带cookie在七牛上有可能出现跨域问题
const axiosInstance = axios.create({withCredentials: false});
let data = new FormData();
let type = file.filename.split('.');
data.append('token', uptoken);
// 如果有七牛的accessKey和secretKey,则获取token这个请求可以省略。accessKey和secretKey可以存储在nodejs,没有暴露在前端的风险。
data.append('file', bufferStream);
data.append('key', 'image/' + getDateStr() + '/' + new Date().valueOf() + '.' + type[type.length-1]);
return axiosInstance({
headers: {
'Content-Type': 'multipart/form-data'
},
method: 'POST',
url: 'https://xxxx.qiniup.com/',
data: data,
timeout: 60000,
onUploadProgress: (progressEvent)=> {
let imgLoadPercent = Math.round(progressEvent.loaded * 100 / progressEvent.total);
console.log(file.originalname + '上传进度:' + imgLoadPercent)
},
})
}
router.post("/uploadImg", upload.single("file"), async (ctx) => {
let file = ctx.req.file;
let filepath = '';
try {
const imageRes = await tinyImage(file.filename);
filepath = imageRes.filepath;
} catch (error) {
console.error('压缩 error: ', error)
console.log('压缩图片出错,跳过压缩');
}
let uptoken = '';
let result = {};
try {
let bufferStream = fs.createReadStream(filepath)
result = await uploadImgToQiniu(file, bufferStream, uptoken);
} catch (error) {
console.error('qiniu error: ', error)
ctx.body = JSON.stringify({
msg: '上传图片到七牛出错',
code: 101
});
return;
} finally {
// 上传完后,删除压缩后的图片
await delFile(filepath);
}
ctx.body = JSON.stringify(result.data)
});
app.use(router.routes()).use(router.allowedMethods());
app.use((ctx) => {
ctx.set("Content-Type", "text/html");
ctx.body = fs.readFileSync(join(__dirname, "index.html"));
});
app.listen(3333, () => {
console.log("服务启动: http://localhost:3333");
});
node层压缩逻辑的实现, ./tiny/index.js
import imagemin from 'imagemin';
import path, { dirname } from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import {getImageminPlugins} from './plugins.js';
import {
delFile,
uploadFile,
updateFile,
writeFile
} from "./promiseApi.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const options = {
skipLargerFile: true,
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 75,
progressive: true,
},
pngquant: {
quality: [0.8, 0.9],
speed: 4,
},
svgo: {
plugins: [
{
name: 'removeViewBox',
},
{
name: 'removeEmptyAttrs',
active: false,
},
],
}
};
// 先本地用ImageMin压缩
export async function localImageMinFile(filePath, buffer) {
let content
try {
content = await imagemin.buffer(buffer, {
plugins: getImageminPlugins(options),
})
let size = content.byteLength
const oldSize = buffer.byteLength
if (options.skipLargerFile && size > oldSize) {
content = buffer
size = oldSize
}
return content;
} catch (error) {
console.error('imagemin error:' + error)
}
}
export default async function tinyImage (picPath) {
let filepath = path.join(__dirname, '../source', picPath);
let pathStr = filepath;
const source = fs.readFileSync(filepath);
let imageminFilepath = '';
let imageIsErr = false;
try {
const content = await localImageMinFile(filepath, source)
if(content) {
imageminFilepath = path.join(__dirname, '../source', 'imagemin_' + picPath);
fs.writeFileSync(imageminFilepath, content);
// 写完后,删除原图片
await delFile(filepath);
pathStr = imageminFilepath;
}
} catch (error) {
imageIsErr = true;
console.log('写入ImageMin压缩后的文件失败:', error)
}
return { filepath: pathStr };
}
plugins.js逻辑
import imageminGif from "imagemin-gifsicle";
import imageminPng from "imagemin-pngquant";
import imageminOptPng from "imagemin-optipng";
import imageminJpeg from "imagemin-mozjpeg";
import imageminSvgo from "imagemin-svgo";
import imageminWebp from "imagemin-webp";
import imageminJpegTran from "imagemin-jpegtran";
export const isBoolean = (arg) => {
return typeof arg === "boolean";
};
export const isNotFalse = (arg) => {
return !(isBoolean(arg) && !arg);
};
export const isRegExp = (arg) =>
Object.prototype.toString.call(arg) === '[object RegExp]'
export const isFunction = (arg) =>
typeof arg === 'function'
export function getImageminPlugins(options) {
const {
gifsicle = true,
webp = false,
mozjpeg = false,
pngquant = false,
optipng = true,
svgo = true,
jpegTran = true,
} = options;
const plugins = [];
if (isNotFalse(gifsicle)) {
const opt = isBoolean(gifsicle) ? undefined : gifsicle;
plugins.push(imageminGif(opt));
}
if (isNotFalse(mozjpeg)) {
const opt = isBoolean(mozjpeg) ? undefined : mozjpeg;
plugins.push(imageminJpeg(opt));
}
if (isNotFalse(pngquant)) {
const opt = isBoolean(pngquant) ? undefined : pngquant;
plugins.push(imageminPng(opt));
}
if (isNotFalse(optipng)) {
const opt = isBoolean(optipng) ? undefined : optipng;
plugins.push(imageminOptPng(opt));
}
if (isNotFalse(svgo)) {
const opt = isBoolean(svgo) ? undefined : svgo;
plugins.push(imageminSvgo(opt));
}
if (isNotFalse(webp)) {
const opt = isBoolean(webp) ? undefined : webp;
plugins.push(imageminWebp(opt));
}
if (isNotFalse(jpegTran)) {
const opt = isBoolean(jpegTran) ? undefined : jpegTran;
plugins.push(imageminJpegTran(opt));
}
return plugins;
}
在nodejs里面往类似七牛cdn服务器上传图片,如果不用七牛的npm包,需要构造form表单提交。
import fs from 'fs';
// node读取本地文件
const f = fs.readFileSync(filepath);
const bolb = new Blob([f]);
// 构造form提交
let data = new FormData();
data.append('file', bolb);
axios({
headers: {
'Content-Type': 'multipart/form-data'
},
method: 'POST',
url: 'xxxx',
data: data,
timeout: 60000,
onUploadProgress: (progressEvent)=> {},
})
注意点: 发送带数据的请求时,数据可以是以下类型:
- string
- object
- ArrayBuffer
- ArrayBufferView
- URLSearchParams
- Form Data
- File
- Blob
- Stream
- Buffer
注意: Stream 和 Buffer 仅适用于 Node,而 Form Data、File 和 Blob 仅适用于浏览器
Nodejs也没有FormData这个Web API,要使用'form-data' npm包。
import fs from 'fs';
import FormData from 'form-data';
// node读取本地文件
let bufferStream = fs.createReadStream(filepath)
// 构造form提交
let data = new FormData();
data.append('file', bufferStream);
axios({
headers: {
'Content-Type': 'multipart/form-data'
},
method: 'POST',
url: 'xxxx',
data: data,
timeout: 60000,
onUploadProgress: (progressEvent)=> {},
})