上传选择、拖拽、剪裁功能

227 阅读7分钟

前言

平时我们上传文件、拖拽图片,基本上都是用 antd、elementUI 等库,非常方便,但这不代表我们不需要了解这些东西,实际上图片这些知识点也不是很多,了解总比不知道强

假设我们碰到一个场景,我们的页面效果很简单,就展示一些好看的图片,还支持上传,不需要使用那么庞大的库,客户就要快,此时我们没必要引用那么大的 antd、elementUI 等库,我们直接写一个又快效率又高,那么的话,引入三方库后,还要担心打包文件偏大客户嫌弃问题

下面就介绍上传按钮的自定义、拖拽、剪裁功能

上传选择按钮效果调整

我们先写一个 upload 的样式 css,实际可以使用图片,更适合我们的场景,这里就简写了

//这个样式作为 upload 的样式
.upload {
  width: 200px;
  height: 200px;
  box-shadow: 1px 1px 10 10 #333;
  background: linear-gradient(red, green, blue);
  cursor: pointer;
}

看起来长这样,实际可以用图片代替

image.png

我们将 input 嵌入到 div 中,div 作为样式,input 填充父视图,并隐藏,这样就可以实现点击上传了

<div className="upload">
    <input
        style={{
            width: "100%",
            height: "100%",
            opacity: 0,
        }}
        type="file"
        onChange={(e) => e.target.files)}
    />
</div>

实际上传,还可以通过下面这样,隐藏 input 和 他的时间,通过点击指定的节点,然后响应 input 的 dom(input 的 dom 可以再其他任意地方,也可以点击的时候新创建一个,选择完毕删除),这样也同样实现上传按钮效果的自定义

<div
    className="upload"
    onClick={() => {
        const dom = document.querySelector("input");
        if (!dom) return;
        dom.click();
    }}
>
    <input
        type="file"
        style={{ opacity: 0, pointerEvents: "none" }}
        onChange={(e) => onInputChanged(e.target.files)}
    />
</div>

onChange 回调

onChange 回调中 e.target 就是我们 input 组件,其中的 files 就是我们选择的图片 file 数组,可以使用 file 上传,如果是文件夹,则 file 的 type 则为空,可以通过该参数过滤、错误提示等

const onInputChanged = (fileList: FileList | null) => {
    if (!fileList) return;
    const files = Array.from(fileList).filter((e) => e.type);
    //过滤掉 type 为空的,那不是文件,可能是文件夹,想要文件夹需要一些额外的三方库支持
    console.log(fileList, files);
};

上传拖拽实现

图片拖拽实际上就是走的 onDrop 方法,通过这个方法,可以获取到拖拽进来的文件集合,.dataTransfer.files 可以获取到拖拽进来的文件

但是有一个问题,就是浏览器有一个默认行为,一些浏览器中,拖拽进来的图片默认会开启一个新的窗口打开图片,我们可以通过 preventDefault 方法阻止用户的默认行为,onDragEnteronDragOveronDragLeaveonDrop我们均阻止一下即可,否则仍然可能另起窗口打开图片

psonDragEnter拖拽进入时回调、 onDragOver拖拽进入持续触发、onDragLeave拖拽离开元素触发、onDrop在元素中松手触发,实际上以前我也写过一个 threejs 的浏览器拖拽到 3d 场景的组件库功能,实际上就用到了此类方法衔接拆分

<div
    className="upload"
    onDragEnter={(e) => {
        //拖拽进入出发
        // console.log("onDrag", e);
        e.preventDefault();
    }}
    onDragOver={(e) => {
        //拖拽进入持续触发
        // console.log("onDragOver", e);
        e.preventDefault();
    }}
    onDragLeave={(e) => {
        //拖拽离开元素触发
        // console.log("onDragLeave", e);
        e.preventDefault();
    }}
    onDrop={(e) => {
        //拖拽到div里面
        //e.dataTransfer.files 保存的就是拖拽进来转化的file信息
        console.log("onDrop", e.dataTransfer.files);
        e.preventDefault();
        //拿到拖拽进来的图片,直接回调即可
        onInputChanged(e.dataTransfer.files);
    }}
    onClick={() => {
        const dom = document.querySelector("input");
        if (!dom) return;
        dom.click();
    }}
>
    ...input
</div>

图片剪裁功能

剪裁之前,我们先把拿到的图片显示

const file = files[0];
//读取file
const reader = new FileReader();
reader.onload = (e) => {
    const img = document.querySelector("img1") as HTMLImageElement;;
    //直接给指定节点赋值图片
    img.src = e.target.result
};
reader.readAsDataURL(file);

上面完成显示后,剪裁的话,实际上在图片上放置一个可以移动缩放的框就行了,用于确定裁剪区域,还需要写一些相关事件(懒得写),这里就不多介绍了

确定剪裁区域后(x, y, width, height),下面介绍,将剪裁后的图片绘制问题

图片剪裁实际上就用到了 canvas 功能,功能也很简单,只需要使用 canvas 上下文 drawImage 即可

const img = document.querySelector("img1") as HTMLImageElement;
//我们希望图片剪裁到 120 x 120 分辨率
const canvas = document.createElement('canvas')
canvas.width = 120
canva.height = 120
const ctx = canvas.getContext('2d')
//直接剪裁即可,只需要设置原图剪裁区域,和目标绘制区域即可
ctx?.drawImage(img, x, y, width, height, 0, 0, 120, 120)

ps:如果出现canvas一些区域模糊,实际上是一些视网膜屏(多倍像素屏),可以将原图也按照指定比例(scale、radio)缩放绘制,就不会出现问题了(一般移动设备会出现此类问题)

下面介绍写一下 drawImage,其可以理解为,将原图片,将其按照一定裁剪方式,放到另一个空白的内容上,超出部分剪裁,其一共声明了三个方法

//将原图整个放到另一个空白内容上(cancas),原图距离空白纸左侧 dx,上侧 dy距离,绘制图片,因此会有留白(取决于canvas背景),由于没有设置dw、dh,不会缩放,超出部分不会被绘制
drawImage(image: CanvasImageSource, dx: number, dy: number): void;

//将原图整个放到另一个空白内容上(cancas),设置设置相对空白纸的dx,dy,并且设定目标宽度dw、高度dh(原图会被压缩绘制到canvan指定区域)
drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void;

//将原图整个放到另一个空白内容上(cancas),发现多了四个参数sx、sy、sw、sh
//sx、sy、sw、sh相当于对原图区域进行裁剪,将裁剪后的内容,作为图片源绘制到空白纸(canvas)上
//设置设置相对空白内容上(cancas)的dx,dy,并且设定目标宽度、高度(原图会被压缩绘制到canvan指定区域)
drawImage(image: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;

具体参数更细致的如下所示,可以看参考自 MDN,实际上这个地方比较难以理解的主要是下面几个参数

  • image

  • 绘制到上下文的元素。允许任何的画布图像源,例如:HTMLImageElementSVGImageElementHTMLVideoElementHTMLCanvasElementImageBitmapOffscreenCanvas 或 VideoFrame

  • sx 可选

    • 需要绘制到目标上下文中的,源 image 的子矩形(裁剪)的左上角 X 轴坐标。可以使用 3 参数或 5 参数语法来省略这个参数。
  • sy 可选

    • 需要绘制到目标上下文中的,源 image 的子矩形(裁剪)的左上角 Y 轴坐标。可以使用 3 参数或 5 参数语法来省略这个参数。
  • sWidth 可选

    • 需要绘制到目标上下文中的,源 image 的子矩形(裁剪)的宽度。如果不指定,整个矩形(裁剪)从坐标的 sx 和 sy 开始,到 image 的右下角结束。可以使用 3 参数或 5 参数语法来省略这个参数。使用负值将翻转这个图像。
  • sHeight 可选

    • 需要绘制到目标上下文中的,image的矩形(裁剪)选择框的高度。可以使用 3 参数或 5 参数语法来省略这个参数。使用负值将翻转这个图像。
  • dx

    • 源 image 的左上角在目标画布上 X 轴坐标。
  • dy

    • 源 image 的左上角在目标画布上 Y 轴坐标。
  • dWidth

    • image 在目标画布上绘制的宽度。允许对绘制的图像进行缩放。如果不指定,在绘制时 image 宽度不会缩放。注意,这个参数不包含在 3 参数语法中。
  • dHeight

    • image 在目标画布上绘制的高度。允许对绘制的图像进行缩放。如果不指定,在绘制时 image 高度不会缩放。注意,这个参数不包含在 3 参数语法中。