背景
- 前端上传头像的组件要更新一下,之前是自动将图片进行变形,现在要重新改一下,改成前端显示一个剪切框,可以放大和缩小然后再上传头像。
- 根据老板不造轮子的指示,我上网找了一下轮子,准备用一个叫做 vue-cropper 的轮子。
- 找到轮子感觉事情就变得很简单了,然而坑比我想象的要多。
如何读入本地图片
<div class="file is-centered">
<label class="file-label" style="margin-top:5%">
<input
class="file-input"
type="file"
accept=".jpg, .png, .gif"
@change="chooseAvatar($event)"
/>
<span class="file-cta">
<span class="file-icon">
<i class="fa fa-upload"></i>
</span>
<span class="file-label">选择图片</span>
</span>
</label>
</div>
- 这就是 bulma 上面 file input 的样式了,由于我之前试了几个小时,也没有让其居中显示,就用了这种粗糙的方式居中了。
- 和普通的 html 里的 input 一样的参数,简单说,type 和 accept 限定了选择文件的类型。现在关键就是这个函数了,记住嗷,参数是 $event
- 这个函数 @change 就是在内容变化的时候调用,简单地说,你点这个组件,浏览器会自动跳出来那个让你选择的弹窗,当你选择了一个文件以后,就会调用这个函数了,文件的信息就在 event 里面。
- 接下来就是在 ts 函数里面把文件搞出来了,代码如下:
private chooseAvatar(event: any) {
const that = this;
const reader = new FileReader();
reader.onload = function() {
that.avatarUrl = this.result;
that.$nextTick(function() {
if (
event.target.files[0].type == "image/png" ||
event.target.files[0].type == "image/jpeg"
) {
that.ifUseCropper = true;
} else {
that.ifUseCropper = false;
}
})
};
let imagSrc = window.URL.createObjectURL(event.target.files[0]);
reader.readAsDataURL(event.target.files[0]);
this.imageFile = event.target.files[0];
}
- 由于第一版代码没存,这是用了 vue-cropper 的代码,后面那个 if 就是用来判断文件类型的,因为这个插件只能处理 png、jpg,我们的二次元后端又要上传一些 gif 动图,所以我就分开来处理,判断一下类型,就是 ifUseCropper 这个变量了。
- 整个关于读取图片的步骤已经在上面标注了,其实非常简单。
关于 vue-cropper
- 看一下这个组件的 github 上的 readme 就好了,但是我感觉那里面列出的传入参数默认值好像有的不太对,没有深究,发现不对手动赋值进去就好了。
- 首先关于安装就有一个坑,npm install 了以后,由于 js 和 ts 的原因,import 不识别报错,上网查了一下,要在 src/index.d.ts 下面加上这句话:
declare module 'vue-cropper';
- 然后是 html 上的使用方法了,和所以 vue 组件使用一样,import 然后再 component 里面声明,就可以使用了,使用代码如下:
<div class="vueCropper" v-else>
<VueCropper
ref="cropper"
:img="avatarUrl"
outputType="png"
:autoCrop="true"
:fixedNumber="[1, 1]"
:fixed="true"
></VueCropper>
</div>
.vueCropper {
width: 100%;
height: 128px;
}
- 省略了上面 v-if=“ifUseCropper” 部分的代码,我们直接看怎么用,首先,VueCropper 外面必须加一个有长和搞的 div,这个就是外面的剪切框,然后要给赋进去一些变量,官网有介绍
- 特别注意的是要有一个 ref,之前写过的,这是为了后面调用这个组件里面的函数。官网上也有写,后面可以调用两种函数,分别得到剪切完成图片的 blob 或者 base64
blob、base64、file
- 问题来了,我们传给后端的要是一个 file 啊,就是上面那个 event.target.files[0],结果只能获得 blob 或者 base64。
- 用最简单的话解释一下这三个类型,base64 是字符串,可以和 url 一样被 img 渲染出来,blob 是一个特殊类型,file也是一个特殊类,就是html input type=“file” 得到的东西。
- 上网搜了老半天,抓着后端程序猿一起调,才搞定,先给代码:
private confirmAvatar() {
const that = this;
const formateData = new FormData();
if (this.ifUseCropper) {
(this.$refs.cropper as any).getCropBlob(function(data: any) {
let files = new File([data], "avatar.png", {type: "image/png"});
that.imageFile = files;
formateData.append("files", files);
that.postToDatabase(formateData);
});
} else {
formateData.append("files", this.imageFile);
this.postToDatabase(formateData);
}
}
- 先说一个与坑无关的知识,后台经常有个术语叫表单提交,说的就是这个 FormData,用法很简单啦,就和字典差不多,往里面 append 就行了,一看就会。
- 首先明确,我们要得到的是 blob,然后就是用 blob 创建一个 file,用到了构造函数。第一个参数必须要有中括号,第二个参数是这个文件的名称,有坑,名字必须加上后缀嗷,那个 .png 很致命,后面那个参数是文件的类型。
- 然后还是报错,我找了后台程序猿看,结果他说改了读取方法,直接获取数据啥的,咱也不知道,反正就是后台也有坑,可以把锅甩给后台。
- 然后就是老生常谈的异步的问题了,那个 post 要写在里面,不然还没等给 formteData 加数据,就已经 post 了。
总结
- 至此,咱们终于可以剪图片了。上面代码都是摘出来的,为了突出重点。下面是整个组件的代码,是一个上传图片的弹窗,可以传要上传图片的 API,也可以当一个组件了,有需要可以直接嫖走(我们再 main 里面 import 了 bulma,所以这个没有引用)。
<template>
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{title}}</p>
</header>
<section class="modal-card-body">
<center v-if="!ifUseCropper">
<figure class="image is-128x128">
<img class="is-rounded" :src="avatarUrl" style="width:128px;height:128px" />
</figure>
</center>
<div class="vueCropper" v-else>
<VueCropper
ref="cropper"
:img="avatarUrl"
outputType="png"
:autoCrop="true"
:fixedNumber="[1, 1]"
:fixed="true"
autoCropWidth="128"
></VueCropper>
</div>
<div class="file is-centered">
<label class="file-label" style="margin-top:5%">
<input
class="file-input"
type="file"
accept=".jpg, .png, .gif"
@change="chooseAvatar($event)"
/>
<span class="file-cta">
<span class="file-icon">
<i class="fa fa-upload"></i>
</span>
<span class="file-label">选择图片</span>
</span>
</label>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" @click="confirmAvatar()">保存修改</button>
<button class="button" @click="cancelAvatar()">取消修改</button>
</footer>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import Axios from "axios";
import { VueCropper } from "vue-cropper";
@Component({
components: {
VueCropper
}
})
export default class UserChangeInformation extends Vue {
@Prop() private avatarUrl!: any;
@Prop() private src!: string;
@Prop() private title!: string;
@Prop() private projectId!: string;
private imageFile: any = {};
private ifUseCropper = false;
private cancelAvatar() {
this.$emit("closeAvatar");
}
private confirmAvatar() {
const that = this;
const formateData = new FormData();
if (this.ifUseCropper) {
(this.$refs.cropper as any).getCropBlob(function(data: any) {
let files = new File([data], "avatar.png", {type: "image/png"});
that.imageFile = files;
formateData.append("files", files);
that.postToDatabase(formateData);
});
} else {
formateData.append("files", this.imageFile);
this.postToDatabase(formateData);
}
}
private chooseAvatar(event: any) {
const that = this;
const reader = new FileReader();
reader.onload = function() {
that.avatarUrl = this.result;
that.$nextTick(function() {
if (
event.target.files[0].type == "image/png" ||
event.target.files[0].type == "image/jpeg"
) {
that.ifUseCropper = true;
} else {
that.ifUseCropper = false;
}
})
};
let imagSrc = window.URL.createObjectURL(event.target.files[0]);
reader.readAsDataURL(event.target.files[0]);
this.imageFile = event.target.files[0];
}
private postToDatabase(formateData: any) {
const that = this;
Axios.post(this.src, formateData).then(function(response) {
that.$emit("closeAvatar");
});
}
}
</script>
<style lang="scss">
.vueCropper {
width: 100%;
height: 128px;
}
</style>