likes
comments
collection
share

【NestJS应用从0到1】8.文件上传及图片压缩

作者站长头像
站长
· 阅读数 25

在业务中上传文件及图片是个很普遍的需求,比如说头像,富文本,Banner的图片内容。在上传之前我们可能也需要对文件进行一定的操作,如图片压缩,裁切等等。 如果我们将图片上传到自己服务器,对存储有一定的要求,其实现在很多云服务商都有OSS,还有免费的存储空间,不用白不用是吧。很多的免费图床都是上传到免费的OSS。 我们可以将文件经过后端服务器对图片文件进行压缩后上传到七牛云OSS,OSS会回调返回文件路径,所以我的应用并不需要专门的文件存储模块。(七牛云的图片也支持压缩,但是要钱。虽然很便宜,但是没必要,我自己处理就行)

文件上传基本都是使用FormData上传,这个项目也不例外。

在这之前

我的项目是Nest+Fastify,不是Express哈,如果你使用Express需要在部分关键位置做对应调整。

你需要了解:

Code it

在code之前看下模块目录,总共分为3个大部分:

  • handler 用于文件处理,目前只有图片压缩
  • oss 服务,用于和具体的oss服务商对接
  • upload 模块的具体实现

【NestJS应用从0到1】8.文件上传及图片压缩

安装及配置 fastify-multipart

安装

npm i @fastify/multipart

配置

只是简单示意,会省去很多其他的引用,你只需要参考 fastifyMultipart注册即可 在main.ts中引入 @fastify/multipart


import fastifyMultipart from '@fastify/multipart';
// ...
async function bootstrap() {
    // ... 
  // 注册 multipart ,实现文件上传
  app.register(fastifyMultipart, {
    attachFieldsToBody: true,
    limits: {
      fieldNameSize: 100, // 字段名的最大长度
      fieldSize: 1000000, // 字段值的最大长度(1MB)
      fields: 10, // 允许的最大字段数
      fileSize: 1000000, // 单个文件的最大大小(1MB)
      files: 1, // 允许的最大文件数
      headerPairs: 2000, // 允许的最大头部对数
    },
  });
}

对接OSS-七牛云

调用七牛云的API进行文件上传

import { Injectable } from '@nestjs/common';
import * as qiniu from 'qiniu';
import { ConfigService } from '@nestjs/config';

export interface OssUploadOption {
  fileName: string;
  folderPath: string;
}
@Injectable()
export class QiNiuOssService {
  private accessKey: string;
  private secretKey: string;
  private bucket: string;
  private domain: string;

  constructor(private configService: ConfigService) {
    this.accessKey = this.configService.get<string>('OSS_QINIU_AK');
    this.secretKey = this.configService.get<string>('OSS_QINIU_SK');
    this.bucket = this.configService.get<string>('OSS_QINIU_BUCKET');
    this.domain = this.configService.get<string>('OSS_QINIU_DOMAIN');
  }

  async upload(file: Buffer, uploadOption: OssUploadOption): Promise<string> {
    const mac = new qiniu.auth.digest.Mac(this.accessKey, this.secretKey);
    const options = {
      scope: this.bucket,
    };
    const putPolicy = new qiniu.rs.PutPolicy(options);
    const uploadToken = putPolicy.uploadToken(mac);

    const config = new qiniu.conf.Config();
    // 七牛的config.zone已弃用
    // config.zone = qiniu.zone.Zone_z2; // 根据你存储空间所在的区域选择合适的 Zone

    config.regionsProvider = qiniu.httpc.Region.fromRegionId('z2');
    const formUploader = new qiniu.form_up.FormUploader(config);
    const putExtra = new qiniu.form_up.PutExtra();

    const { fileName, folderPath } = uploadOption;
    const filename = `${folderPath}${folderPath && !folderPath.endsWith('/') ? '/' : ''}${fileName}`;
    return new Promise((resolve, reject) => {
      formUploader
        .put(uploadToken, filename, file, putExtra)
        .then(({ data, resp }) => {
          if (resp.statusCode === 200) {
            console.log(data);
            resolve(`${this.domain}/${data.key}`);
          } else {
            reject();
            throw new Error('上传失败');
          }
        })
        .catch(() => {
          reject();
          throw new Error('上传失败');
        });
    });
  }
}

配置环境变量

环境变量均可以在七云牛的官方文档上按照指引找到

# ----------------------↓ OSS-七牛云 ↓----------------------
OSS_QINIU_AK=your-access-key
OSS_QINIU_SK=your-secret-key
OSS_QINIU_BUCKET=your-space-name
OSS_QINIU_DOMAIN=your-url-domain

可扩展性

建议:为了规范oss , 你可以定义一个oss.service的 interface ,具体的oss服务使用implements进行实现,以保证输出的是统一的Promise<string>格式的数据

export interface OssUploadOption {
  fileName: string;
  folderPath: string;
}

export interface OssService {
  upload(file: Buffer, uploadOption: OssUploadOption): Promise<string>;
}

这样你的具体oss服务需要进行调整为


import { OssService, OssUploadOption } from './oss.service';
export class QiNiuOssService implements OssService {
// TODO
}
 

使用Sharp处理图片

这个很简单,引入sharp然后链式调用处理即可

import * as sharp from 'sharp'; // 这里注意,如果你是TypeScript开发,必须这样用,不然会报错 _default is not a function
import { ResizeOptions } from 'sharp';

export const resizeImage = async (
  image: Buffer,
  resize: null | ResizeOptions,
  quality: number,
): Promise<Buffer> => {
  
  return await sharp(image).resize(resize).jpeg({ quality }).toBuffer();
};

我这里将Sharp的原生ResizeOptions作为 resize类型,以便更好的扩展使用。resizeOptions的各个属性释义如下:

// ResizeOptions:
// width: 可选,指定缩放后的图片宽度(像素)。如果同时指定了 width  height,则 width 优先级更高。
// height: 可选,指定缩放后的图片高度(像素)。如果同时指定了 width  height,则 width 优先级更高。
// fit: 可选,指定如何将图片缩放以适应提供的 width  height。可选值为 FitEnum 枚举类型中的一个,默认值为 'cover'。常见取值:
// cover: 图片会完全覆盖指定区域,可能会被裁剪。
// contain: 图片会完整地包含在指定区域内,可能会留白。
// fill: 图片会填充整个区域,可能会拉伸变形。
// inside: 图片会被缩放以适应指定区域,但不会放大。
// outside: 图片会被缩放以完全包含指定区域,但不会缩小。
// position: 可选,当 fit  'cover'  'contain' 时,指定图片在区域内的位置或对齐方式。可以是数字、字符串或预定义常量,默认值为 'centre'
// background: 可选,当 fit  'contain' 时,指定背景颜色。默认值为黑色不透明 ({r: 0, g: 0, b: 0, alpha: 1}),可以使用 Color 类型指定其他颜色。
// kernel: 可选,指定图片缩小时使用的内核算法。可选值为 KernelEnum 枚举类型中的一个,默认值为 'lanczos3'。不同算法会产生不同的锐化效果。
// withoutEnlargement: 可选,如果图片宽度或高度已经小于指定的尺寸,则不进行放大。相当于 GraphicsMagick  > 几何选项,默认值为 false
// withoutReduction: 可选,如果图片宽度或高度已经大于指定的尺寸,则不进行缩小。相当于 GraphicsMagick  < 几何选项,默认值为 false
// fastShrinkOnLoad: 可选,是否利用 JPEG  WebP 格式的 "shrink-on-load" 特性,这可能会在某些图片上产生轻微的摩尔纹。默认值为 true

实现upload服务

upload服务就是获取上传的文件,判断是否为图片,是否需要进行压缩,以及一些自定义的配置项。压缩完毕后上传到oss。

可配置: 这里可以注意upload方法接收两个参数,一个是file文件,另一个是配置项 UploadOption,为了便于其他服务或者controller调用时能够自定义控制项

服务层实现


import { Injectable } from '@nestjs/common';
import { QiNiuOssService } from './oss/qiniu.oss.service';
import { ResizeOptions } from 'sharp';
import { resizeImage } from './handler/image.handler';
import { MultipartFile } from '@fastify/multipart';

export interface UploadOption {
  resize?: null | ResizeOptions; // 是否修改图片尺寸
  quality?: number; // 压缩图片的质量
  // oss?: 'qiniu'; // 上传到 oss
  fileName?: string; // 文件名称,无需后缀
  fileNameRandom?: boolean; // 是否随机文件名称,在fileName不存在时生效
  folderPath?: string; // 文件夹路径
}

@Injectable()
export class UploadService {
  constructor(private qiniuOssService: QiNiuOssService) {}

// 默认质量为80 进行压缩,传入null则不会进行压缩
  async upload(file: MultipartFile, option: UploadOption = { quality: 80 }) {
    let fileBuff: Buffer = await file.toBuffer();
      // 判断是否是图片
    if (
      file.mimetype.startsWith('image/') &&
      option &&
      (option.resize || option.quality)
    ) {
      fileBuff = await resizeImage(fileBuff, option.resize, option.quality);
    }
    const filename = `${option.fileName || (option.fileNameRandom ? Math.random().toString(36).slice(2) : file.filename.slice(0, file.filename.lastIndexOf('.')))}${file.filename.slice(file.filename.lastIndexOf('.'))}`;

    return await this.qiniuOssService.upload(fileBuff, {
      fileName: filename,
      folderPath: option.folderPath,
    });
  }

controller 实现

定义一个 /upload/file API,客户端使用该接口进行文件上传

需要注意,服务层我们第二个参数是自定义的UploadOption,可通过客户端上传时增加配置内容。使用generateOption方法将请求中的数据转换成UploadOption类型

import { Controller, Post, Req } from '@nestjs/common';
import { UploadOption, UploadService } from './upload.service';
import { FastifyRequest } from 'fastify';

@Controller('upload')
export class UploadController {
  constructor(private readonly uploadService: UploadService) {}

  @Post('file')
  async uploadFile(@Req() req: FastifyRequest) {
    const file = req.body['file']; 
    // 这里从body取是因为 fastifyMultipart配置了 attachFieldsToBody为true,
    const url = await this.uploadService.upload(
      file,
      await this.generateOption(req),
    );
    return url;
  }
  
  // 这是一个工具方法,用于在request数据中获取参数,对上传后的文件进行自定义操作。返回一个UploadOption类型 传递给 upload.service
    private async generateOption(req: FastifyRequest): Promise<UploadOption> {
    const option: UploadOption = {
      fileNameRandom: true,
    };
    if (req.body['fileNameRandom']) {
      option.fileNameRandom = !req.body['fileNameRandom'].value;
    }
    if (req.body['folderPath']) {
      option.folderPath = req.body['folderPath'].value || null;
    }
    if (req.body['width'] || req.body['height']) {
      option.resize = {};
      if (req.body['width']) {
        option.resize.width = Math.max(
          Math.min(parseInt(req.body['width'].value), 1920),
          100,
        );
      }
      if (req.body['height']) {
        option.resize.width = Math.max(
          Math.min(parseInt(req.body['height'].value), 1080),
          100,
        );
      }
    }
    // quality 只允许30-100 。默认80
    if (req.body['quality']) {
      option.quality =
        Math.max(Math.min(parseInt(req.body['quality'].value), 30), 100) || 80;
    }
    return option;
  }
}

效果

上传的FormData

【NestJS应用从0到1】8.文件上传及图片压缩

接口返回

【NestJS应用从0到1】8.文件上传及图片压缩

完结,撒花❀❀❀❀❀❀

转载自:https://juejin.cn/post/7385109416721072169
评论
请登录