site logo

Marico' space

react antd 前端压缩图片并上传阿里云OSS实践,后端nest.js

前端技术 2024-07-17 22:57:50 572

闲来无事,手撸了个博客站,给孩子和自己用,想偶尔写点东西放上去。(孩子的博客: www.yufangran.com

现在的手机拍照很方便,可是直接上传博客又有些大,而且手机的照片包含很多额外的信息,像地理位置信息,相机参数信息,便不想直接上传原图。于是想,上传时候是否可以压缩图片并删除额外的不必要的信息。

简单说,前端压缩图片并上传阿里云OSS, 思路有二:

  1. 上传服务器,压缩,再上传oss
  2. 前端压缩,直连oss上传图片

其一是传统做法, 简单方便,弊端是比较费带宽。作为一个乞丐版服务器,带宽可怜的很。

其二前端压缩同样方便,但前端直传OSS,尤其压缩后直传案例不是很多。

当然,其他的方案还有很多,本着最省钱,也最擅长的原则,暂时选用方案二。

好了,直接上实现。

过程时序图如下:

具体实现步骤

此处用的是服务端签名直传 web直传, 点击链接可直达OSS服务端签名文档文档

1. server端集成,生成OSS直传配置信息

  • 获取签名信息:

import * as OSS from 'ali-oss';

 async ossUploadSignup() {
    const client = new OSS(this.configService.ossConfig);

    const date = new Date();
    date.setDate(date.getDate() + 1);
    const policy = {
      expiration: date.toISOString(), // 请求有效期
      conditions: [
        ['content-length-range', 0, 1048576000], // 设置上传文件的大小限制
        { bucket: client.options.bucket }, // 限制可上传的bucket
      ],
    };

    // //  跨域才设置
    // res.set({
    //   'Access-Control-Allow-Origin': req.headers.origin || '*',
    //   'Access-Control-Allow-Methods': 'PUT,POST,GET',
    // });

    //签名
    const formData = await client.calculatePostSignature(policy);
    //bucket域名
    const host = `https://${this.configService.ossConfig.bucket}.${
      (await client.getBucketLocation()).location
    }.aliyuncs.com`.toString();

    //回调
    const callback = {
      callbackUrl: this.configService.ossConfig.callbackUrl,
      callbackBody:
        'bucket=${bucket}&filename=${object}&etag=${etag}&size=${size}&mimeType=${mimeType}&imageInfo.height=${imageInfo.height}&imageInfo.width=${imageInfo.width}&imageInfo.format=${imageInfo.format}&sid=${sid}&uuid=${uuid}',
      callbackBodyType: 'application/x-www-form-urlencoded',
    };

    //返回参数
    return {
      expire: new Date(Date.now() + 1000 * 60).toUTCString(),
      policy: formData.policy,
      signature: formData.Signature,
      accessId: formData.OSSAccessKeyId,
      host,
      callback: Buffer.from(JSON.stringify(callback)).toString('base64'),
      dir: this.configService.ossConfig.dir,
    };
  }

oss 上传完成后callback:

  @Post('alioss/callback')
  async callback(@Body() body): Promise<any> {
    // save file info into DB
    return { fileUrl: `/myblog/file/oss/get/${body.filename}` };  // alioss 文件地址本地化,后用302跳转
  }

  • 访问图片:

@Get('myblog/file/oss/get/*/:fileName')
@Header('Cache-Control', '31104000')
async getOssFile(
  @Param('fileName') fileName,
  @Req() req,
  @Res() res,
): Promise<any> { 
  const host = ((await this.fileService.ossUploadSignup()) as any).host || '';
  res.redirect(`${host}/${req.path.replace('/myblog/file/oss/get/', '')}`);
}

2. 前端web直传

话不多说, 直接上代码

import { Button, Upload, UploadFile } from 'antd';
// 省略部分依赖



export const FileUploader:  = () => {
// ... 省略部分state effect 
  const beforeUpload = async (file) => {
    const fileType = getFileTypeFromExt(getFileExt(file.name));
    if (!(await fileChecking(file))) return; // 检查文件类型, Marico在blog中就只接受图片和视频

    if (fileType === 'image') {
      const _file: any = await compressImage(file);  // 压缩图片
      if (_file) {
        _file.name = file.name;
        _file.lastModifiedDate = new Date();
        return _file;
      } else {
        console.log(`compress image failed`);
      }
    }
    return false;
  };

//  压缩图片主方法
  const compressImage = async (file) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);

    return new Promise((resolve) => {
      reader.onload = function (event) {
        const img = new Image(); // 创建Image对象
        img.src = event.target?.result as string;

        img.onload = async function () {
          // 创建Canvas元素
          const canvas = document.createElement('canvas');
          const _width = img.width;
          const _height = img.height;
          const ratio = _width / _height;

          const width = _width > 1980 ? 1980 : _width; // 压缩后图片尺寸, 此处Marico依据图片宽度按比例压缩
          const height = width / ratio;  // 根据宽高比, 计算压缩后图片高度

          canvas.width = width;
          canvas.height = height;

          const ctx = canvas.getContext('2d');
          ctx?.drawImage(img, 0, 0, width, height);

          // 将Canvas转换回Blob文件对象
          canvas.toBlob(
            function (blob) {
              // 这里的blob就是压缩后的图片文件
              resolve(blob);
            },
            'image/webp',  // 此处笔者选择的是webp格式, 经过几轮测试, 发现在图片质量和图片大小方面,jpg,png,webp中webp最佳,就选它了
            0.8 // 0.8 是压缩质量,可以根据需要调整
          );
        };
      };
    });
  };

  const handleUploadChange = async (info) => {
    const fileIssue = fileChecking(info.file, false);
    if (!fileIssue) return;

    onUploadChange?.(info);
    if (info.file.status === 'done') {
       // 文件上传成功
    } else if (info.file.status === 'error') {
     // 文件上传失败
    } else if (info.file.status === 'removed') {
      // 文件被移除
    } else {
     // ....
    }
  };

 
  // 此处是重点, Antd 组件直传OSS参数配置
  // 查询了好多资料,各种上传处理,都太复杂了,自认为这个最简单了
  // ossSignature 是server端拿到的签名信息,怎么获取不多赘述
  const uploadParams: (file: UploadFile<any>) => Record<string, unknown> = (file: UploadFile) => ({
    key: ossSignature?.dir + getFileName(file.name),
    policy: ossSignature?.policy,
    OSSAccessKeyId: ossSignature?.accessId,
    success_action_status: 200,
    signature: ossSignature?.signature,
    callback: ossSignature?.callback
  });

// Antd 上传组件 
// 用法不多赘述,直接看antd文档即可
  return (       
      <Upload action={ossSignature?.host} fileList={file2upload} showUploadList maxCount={maxCount} multiple={maxCount > 1} accept={getAcceptation()} onChange={handleUploadChange} beforeUpload={beforeUpload} data={uploadParams}>
        <Button icon={<UploadOutlined />}>
          点击上传
        </Button>
      </Upload>
  );
};

至此, 服务端签名, 前端web直传阿里云oss 实现图片压缩上传阿里云OSS就完成了。