共计 5792 个字符,预计需要花费 15 分钟才能阅读完成。
感觉有帮忙的同学记得给个 star,谢谢。github 地址
文档
在 Web 中,上传图片是一个常见的需要。在这篇文章中,咱们将介绍如何应用整洁架构模式来实现一个简略的图片上传状态管理器。
整洁架构是一种软件架构模式,旨在将应用程序分为不同的层。每个层都有特定的职责和依赖关系,使得整个应用程序更易于保护和测试。
当初,让咱们来看看这个图片上传状态管理器的实现。
需要剖析
业务场景:
- 需要 A: 治理后盾须要上传一张图片,限度大小为 5m,传到 oss,获取返回的信息,把返回信息保留到服务端
- 需要 B:小程序须要上传一张图片,限度大小为 5m,传到 oss,获取返回的信息,把返回信息保留到服务端
- 需要 C:App 里的 H5 须要上传一张图片,限度大小为 2m, 宽高 100px,传到七牛云,获取返回的信息,把返回信息保留到服务端
- 需要 D: …
从下面的场景看来,这些需要都是大同小异,整体流程根本没变,能够形象成下图
依据这个流程图,能够得出 Presenter 的接口, 如下
接着把选图性能做成一个接口
接着把上传性能做成一个接口
当咱们须要切换性能的时候,替换具体实现即可, 基于下面的流程形象出一个选图上传业务组件
状态
首先,让咱们来定义视图须要的状态, 依据咱们的应用场景,能够定义出以下的状态
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 地址