最开始的一个小需求
前两天项目中有个小需求:前端下载后台小哥返回的二进制流文件。
起初接到这个需求时,我感觉这很简单啊(虽然我不会,但可以百度啊,,,,)

整个需求分以下四步:
- 获取文件并读取文件。
- 获取裁剪坐标。
- 裁剪图片。
- 读取裁剪后的图片预览并上传。
获取文件并读取文件
首先来看下上面第一步提到的获取文件。对应就是给input绑定的handleChange事件:
handleChange = (event) => {
let file = event.target.files[0];
let fileReader = new FileReader();
fileReader.onload = (event) => {
this.setState({
file,
dataURL: event.target.result,
});
this.imageRef.current.onload = () => this.drawImage();
};
fileReader.readAsDataURL(file);
};
HTML5 支持从 input[type=file] 元素中直接获取文件信息,也可以读取文件内容。
这里就需要用到了 FileReader ,这个类是专门用来读取本地文件的。纯文本或者二进制都可以读取,但是本地文件必须是经过用户允许才能读取,也就是说用户要在input[type=file]中选择了这个文件,你才能读取到它。
通过 FileReader 我们可以将图片文件转化成 DataURL,就是以 data:image/png;base64开头的一种URL,然后可以直接放在 image.src 里,这样本地图片就显示出来了。
获取裁剪坐标
这里主要是mousedown、mousemove、mouseup事件的结合使用。
mousedown
鼠标按下事件。这里要记录下鼠标按下时的开始坐标,即startX与startY,同时要将标志位startDrag设为true,标识鼠标开始移动。
handleMouseDown = (event) => {
this.setState({
startX: event.clientX,
startY: event.clientY,
startDrag: true,
});
};
mousemove
鼠标移动事件。判断startDrag为true(即鼠标开始移动),然后记录对应移动的距离。
handleMouseMove = (event) => {
if (this.state.startDrag) {
this.drawImage(
event.clientX - this.state.startX + this.state.lastX,
event.clientY - this.state.startY + this.state.lastY
);
}
};
mouseup
鼠标弹起事件。这里要记录下最终鼠标的落点坐标,对应就是lastX与lastY。
handleMouseUp = (event) => {
this.setState({
lastX: event.clientX - this.state.startX + this.state.lastX,
lastY: event.clientY - this.state.startY + this.state.lastY,
startDrag: false,
});
};
裁剪图片
这个时候我们就需要用到canvas了,canvas和图片一样,所以新建canvas时就要确定其高宽。
将图片放置入canvas时需要调用drawImage:
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
具体API使用参考MDN上的drawImage
drawImage = (left = this.state.lastX, top = this.state.lastY) => {
let image = this.imageRef.current;
let canvas = this.canvasRef.current;
let ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
let imageWidth = image.width;
let imageHeight = image.height;
if (imageWidth > imageHeight) {
let scale = canvas.width / canvas.height;
imageWidth = canvas.width * this.state.times;
imageHeight = imageHeight * scale * this.state.times;
} else {
let scale = canvas.height / canvas.width;
imageHeight = canvas.height * this.state.times;
imageWidth = imageWidth * scale * this.state.times;
}
ctx.drawImage(
image,
(canvas.width - imageWidth) / 2 + left,
(canvas.height - imageHeight) / 2 + top,
imageWidth,
imageHeight
);
};
其中这里面我们还加入了scale,这个变量是用来实现图片放大、缩小效果的。
而且会判断图片的宽、高的大小关系,从而实现图片在canvas中对应的适配。
读取裁剪后的图片并上传
这时我们要获取canvas中图片的信息,用toDataURL就可以转换成上面用到的DataURL。
confirm = () => {
let canvas = this.canvasRef.current;
let ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(100, 100, 100, 100);
let avatarCanvas = document.createElement("canvas");
avatarCanvas.width = 100;
avatarCanvas.height = 100;
let avatarCtx = avatarCanvas.getContext("2d");
avatarCtx.putImageData(imageData, 0, 0);
let avatarDataUrl = avatarCanvas.toDataURL();
this.setState({ avatarDataUrl });
this.avatarRef.current.src = avatarDataUrl;
};
然后取出其中base64信息,再用window.atob转换成由二进制字符串。但window.atob转换后的结果仍然是字符串,直接给Blob还是会出错。所以又要用Uint8Array转换一下。
这时候裁剪后的文件就储存在blob里了,我们可以把它当作是普通文件一样,加入到FormData里,并上传至服务器了。
upload = (event) => {
// console.log("文件 url", this.state.avatarDataUrl);
let bytes = atob(this.state.avatarDataUrl.split(",")[1]);
console.log("bytes", bytes);
let arrayBuffer = new ArrayBuffer(bytes.length);
let uInt8Array = new Uint8Array();
for (let i = 0; i < bytes.length; i++) {
uInt8Array[i] = bytes.charCodeAt[i];
}
let blob = new Blob([arrayBuffer], { type: "image/png" });
let xhr = new XMLHttpRequest();
let formData = new FormData();
formData.append("avatar", blob);
xhr.open("POST", "/upload", true);
xhr.send(formData);
};
今天装逼到此结束,谢谢观看!
文章来源:CSDN