觉得有帮助的同学记得给个star,谢谢。github地址

文档

在Web中,上传图片是一个常见的需求。在这篇文章中,我们将介绍如何使用整洁架构模式来实现一个简单的图片上传状态管理器。

整洁架构是一种软件架构模式,旨在将应用程序分为不同的层。每个层都有特定的职责和依赖关系,使得整个应用程序更易于维护和测试。

现在,让我们来看看这个图片上传状态管理器的实现。

需求分析

业务场景:

  • 需求A: 管理后台需要上传一张图片,限制大小为5m,传到oss,获取返回的信息,把返回信息保存到服务端
  • 需求B:小程序需要上传一张图片,限制大小为5m,传到oss,获取返回的信息,把返回信息保存到服务端
  • 需求C:App里的H5需要上传一张图片,限制大小为2m, 宽高100px,传到七牛云,获取返回的信息,把返回信息保存到服务端
  • 需求D: ...

从上面的场景看来,这些需求都是大同小异,整体流程基本没变,可以抽象成下图

111.png

根据这个流程图,可以得出Presenter的接口,如下

upload-presenter.png

接着把选图功能做成一个接口

select-service.png

接着把上传功能做成一个接口

upload-service.png

当我们需要切换功能的时候,替换具体实现即可,基于上面的流程抽象出一个选图上传业务组件

状态

首先,让我们来定义视图需要的状态,根据我们的使用场景,可以定义出以下的状态

export type IFile = {
  file: File | WxFilePath; // 上传的源文件
  thumbUrl: string; // 缩略图地址
  id: string; // 唯一id ,自动生成
  url: string;
  name: string; // 上传文件名,由上传函数提供
  status: 'default' | 'pending' | 'failed' | 'successful' | 'aborted';
};

export type IFileList = IFile[];

interface IViewState {
  loading: boolean;
  fileList: IFileList;
}

Presenter类实现

接下来,我们来实现Presenter类。用来和view层交互,提供viewstate和对应的method
完整代码如下:

可以看到我们的Presenter提供了以下几种方法给到view层使用

  • showLoading 切换loading
  • hideLoading
  • select 选择图片
  • upload 上传图片
  • remove 移除已选择图片
  • selectAndUpload 选择并上传
  • replaceAt 替换对应下标的图片

    @injectable()
    export class UploadImagePresenter extends Presenter<IViewState> {
    constructor(
      @inject(SelectImageServiceToken)
      private selectImageService: AbsSelectImageService,
      @inject(UploadServiceToken) private uploadService: AbsUploadService,
    ) {
      super();
      this.state = { loading: false, fileList: [] };
    }
    
    showLoading() {
      this.setState((s) => {
        s.loading = true;
      });
    }
    
    hideLoading() {
      this.setState((s) => {
        s.loading = false;
      });
    }
    
    /**
     * 调用选图服务 添加或者替换filelist
     * @returns
     */
    select(index?: number) {
      if (index !== undefined) {
        if (this.state.fileList[index]) {
          // 选图服务允许返回多个文件
          // 如果指定了下标, 就只选用第一个文件
          return this.selectImageService
            .__selectAndRunMiddleware()
            .then((files) => {
              this.setState((s) => {
                s.fileList[index].file = files[0];
                // 重置状态
                s.fileList[index].status = 'default';
              });
            });
        }
        throw Error(`index:(${index}) not found in fileList`);
      } else {
        // 选图方法
        return this.selectImageService
          .__selectAndRunMiddleware()
          .then((files) => {
            this.setState((s) => {
              s.fileList = [...s.fileList, ...files.map((v) => makeFile(v))];
            });
          });
      }
    
      //
    }
    
    /**
     * 调用上传服务 上传指定文件 更新 fileList 状态
     * 指定了下标就选择对应的文件, 不然就选最后一个文件
     * @param index
     * @returns
     */
    upload(index?: number) {
      const i =
        typeof index === 'number' ? index : this.state.fileList.length - 1;
      const file = this.state.fileList[i];
      if (!file) {
        throw Error(`index: ${index} uploadFile out of index,`);
      }
      if (file.status !== 'successful') {
        this.showLoading();
        return this.uploadService
          .upload(file.file)
          .then((res) => {
            this.setState((s) => {
              s.fileList[i].url = res.url;
              s.fileList[i].name = res.name;
              s.fileList[i].thumbUrl = res.thumbUrl;
              s.fileList[i].status = 'successful';
            });
          })
          .catch((e) => {
            this.setState((s) => {
              s.fileList[i].status = 'failed';
            });
            throw e;
          })
          .finally(() => {
            this.hideLoading();
          });
      }
      // 该文件已经上传成功
      return Promise.resolve(true);
    }
    
    /**
     * 移除文件
     * @param index
     */
    remove(index: number) {
      this.setState((s) => {
        s.fileList.splice(index, 1);
      });
    }
    
    /**
     * 选择图片,并上传最后一张图片
     */
    async selectAndUpload() {
      await this.select();
      await this.upload();
    }
    
    replaceAt(index: number, file: IFile) {
      this.setState((s) => {
        s.fileList[index] = {
          ...s.fileList[index],
          ...file,
        };
      });
    }
    }
    

在UploadImagePresenter这个类中我们依赖了两个抽象服务类:AbsSelectImageService
和AbsUploadService。

通过依赖注入的方式来替换真实的选图服务和上传服务。实现了视图,状态,服务等几个层级的解耦

比如我们的选图服务可以是 “浏览器选图”,也可以是“微信小程序选图”

上传服务可以是传到阿里云oss,或者七牛或者自己的服务器等等

选图服务


import { IMiddleware, MiddlewareRunner } from '@lujs/middleware';

export type WxFilePath = string;

export type SelectRes = (File | WxFilePath)[];
/**
 * 微信选图返回图片路径
 */
interface ISelect {
  (): Promise<SelectRes>;
}

/**
 * 基础选图服务
 */
export abstract class AbsSelectImageService {
  abstract select(): Promise<SelectRes>;

  middlewareRunner = new MiddlewareRunner<SelectRes>();

  __selectAndRunMiddleware() {
    return this.select().then((fs) => this.middlewareRunner.run(fs));
  }

  useMiddleware(middleware: IMiddleware<SelectRes>) {
    this.middlewareRunner.use(middleware);
  }
}

/**
 * 选图函数生成器
 */
interface BrowserInputSelect {
  // 接受的参数
  accept?: string;
  capture?: boolean;
  multiple?: boolean;
}

export class SelectFnFactor {
  static buildBrowserInputSelect(option?: BrowserInputSelect) {
    const DefaultBrowserInputSelect = {
      accept: 'image/*',
      capture: false,
      multiple: false,
    };

    const opt = {
      ...DefaultBrowserInputSelect,
      ...option,
    };
    return () => {
      let isChoosing = false;
      return new Promise<File[]>((resolve, reject) => {
        const $input = document.createElement('input');
        $input.setAttribute('id', 'useInputFile');
        $input.setAttribute('type', 'file');
        $input.style.cssText =
          'opacity: 0; position: absolute; top: -100px; left: -100px;';

        $input.setAttribute('accept', opt.accept);

        document.body.appendChild($input);

        const unMount = () => {
          // eslint-disable-next-line no-use-before-define
          $input.removeEventListener('change', changeHandler);
          document.body.removeChild($input);
        };

        const changeHandler = () => {
          isChoosing = false;
          if ($input.files) {
            const fs = [...$input.files];
            unMount();
            resolve(fs);
          }

          // 允许重复选择一个文件
          $input.value = '';
        };

        $input.addEventListener('change', changeHandler);

        // 取消选择文件
        window.addEventListener(
          'focus',
          () => {
            setTimeout(() => {
              if (!isChoosing && $input) {
                unMount();
                reject(new Error('onblur'));
              }
            }, 300);
          },
          { once: true },
        );
        $input.click();
        isChoosing = true;
      });
    };
  }

  /**
   * 微信小程序选图
   */
  static buildWxMPSelect(
    option: {
      count?: number;
      sourceType?: ('album' | 'camera')[];
    } = {},
  ): ISelect {
    return () => {
      // eslint-disable-next-line no-undef
      if (wx === undefined) {
        throw Error('wx is undefined');
      } else {
        // eslint-disable-next-line no-undef
        return wx
          .chooseMedia({
            ...option,
            mediaType: ['image'],
          })
          .then((value) => value.tempFiles.map((v) => v.tempFilePath));
      }
    };
  }
}

const browserInputSelect: ISelect = SelectFnFactor.buildBrowserInputSelect({
  accept: 'image/*',
});

/**
 * 浏览器 input 选择服务
 */
export class SelectImageServiceBrowserInput extends AbsSelectImageService {
  select = () => browserInputSelect();
}

选图中间件

我们可以通过内置的中间件来限制图片的大小,尺寸等等

export class MySelectImageService extends AbsSelectImageService {
  constructor() {
    super();
    // 选图中间件,检查图片大小
    this.useMiddleware(
      SelectImageMiddlewareFactor.checkSize({ max: 100 * 1024 }),
    ); // 限制最大为100k
  }


  select() {
    // 自定义选图功能, 使用浏览器的input来选择图片
    const browserInputSelect = SelectFnFactor.buildBrowserInputSelect({
      accept: 'image/*',
    });
    return browserInputSelect();
  }
}

上传服务

export abstract class AbsUploadService {
  abstract upload(files: File | WxFilePath): Promise<ImageRes>;
}

觉得有帮助的同学记得给个star,谢谢。github地址


lujs
226 声望4 粉丝