项目
版本

Uppy 构建插件

你已经可以找到一些有用的 Uppy 插件,但有时你可能需要构建自己的插件。插件可以插入上传过程中或渲染自定义用户界面,通常用于:

  • 渲染一些自定义 UI 元素,如 StatusBarDashboard
  • 进行实际的上传工作,如 XHRUploadTus
  • 在上传之前进行工作,例如压缩图像或调用外部 API。
  • 与第三方服务交互以正确处理上传,如 TransloaditAwsS3

下面是一个 自定义插件的完整示例

创建插件

Uppy 有两种创建插件的类。BasePlugin 用于不需要用户界面的插件,而 UIPlugin 用于需要的插件。每个插件都有一个id 和一个 typeid 用于唯一标识插件。type 可以是任何东西——有些插件使用 type 来决定是否对其他插件执行某些操作。例如,当针对内置 Dashboard 插件时,Dashboard 使用 type 来确定在哪里挂载不同的 UI 元素。'acquirer' 类型的插件被挂载到选项卡栏中,而'progressindicator' 类型的插件被挂载到进度条区域。

插件构造函数接收 Uppy 实例作为第一个参数,以及传递给 uppy.use() 的任何选项作为第二个参数。

import BasePlugin from "@uppy/core/lib/BasePlugin.js";

export default class MyPlugin extends BasePlugin {
  constructor(uppy, opts) {
    super(uppy, opts);
    this.id = opts.id || "MyPlugin";
    this.type = "example";
  }
}

方法

插件可以定义方法来执行特定任务。最重要的方法是 install() ,当插件被 .use 时调用。

以下所有方法都是可选的!只定义你需要的方法。

BasePlugin

install()

当插件被 .use 时调用。在这里做任何设置工作,比如附加事件或添加 上传钩子

export default class MyPlugin extends UIPlugin {
  // ...
  install() {
    this.uppy.on("upload-progress", this.onProgress);
    this.uppy.addPostProcessor(this.afterUpload);
  }
}

uninstall()

当插件被移除或 Uppy 实例关闭时调用。这应该撤销在 install() 方法中所做的所有工作。

export default class MyPlugin extends UIPlugin {
  // ...
  uninstall() {
    this.uppy.off("upload-progress", this.onProgress);
    this.uppy.removePostProcessor(this.afterUpload);
  }
}

afterUpdate()

在每次状态更新后带有防抖地调用,所有内容都已挂载后。

addTarget()

使用此方法将你的插件添加到另一个插件的目标。这就是 @uppy/dashboard 如何将其余插件添加到其 UI 中的方式。

UIPlugin

UIPlugin 扩展了 BasePlugin 类,因此它也将包含上述所有方法。

mount(target)

将此插件挂载到 target 元素。target 可以是 CSS 查询选择器、DOM 元素或另一个插件。如果 target 是插件,源(当前)插件将向目标插件注册,后者可以决定如何和在哪里渲染源插件。

此方法可以重写以支持不同的渲染引擎。

render()

渲染此插件的 UI。Uppy 使用 Preact 作为其视图引擎,因此 render() 应返回一个 Preact 元素。render 会在每次状态更改时自动由 Uppy 调用。

onMount()

在 Preact 渲染组件之后调用。

update(state)

在每次状态更新时调用。除非你使用除 Preact 之外的东西构建 UI 插件,否则你很少需要使用这个。

onUnmount()

在元素从 DOM 中移除后调用。可用于做一些清理或其他副作用。

上传钩子

创建上传时,Uppy 会通过上传管道运行文件。管道由三个部分组成,每个部分都可以插入:预处理、上传和后处理。预处理器可用于配置上传插件、加密文件、调整图像大小等,然后再上传它们。上传者执行实际的上传工作,如创建 XMLHttpRequest 对象并发送文件。后处理器在文件完全上传后进行工作。这可能是等待文件在 CDN 上传播,或将一些元数据与其他文件相关联。

每个钩子都是一个接收正在上传的文件 ID 数组的函数,并返回一个表示完成的 Promise。通过 Uppy 方法添加和删除钩子:addPreProcessoraddUploaderaddPostProcessor,以及它们的 remove* 对应方法。通常,应在插件 install() 方法中添加钩子,在 uninstall() 方法中移除。

此外,上传钩子可以触发事件来指示进度。

添加钩子时,请确保事先绑定钩子 fn !否则将无法移除。例如:

class MyPlugin extends BasePlugin {
  constructor(uppy, opts) {
    super(uppy, opts);
    this.id = opts.id || "MyPlugin";
    this.type = "example";
    this.prepareUpload = this.prepareUpload.bind(this); // ← 这里!
  }

  prepareUpload(fileIDs) {
    console.log(this); // `this` 指向 `MyPlugin` 实例。
    return Promise.resolve();
  }

  install() {
    this.uppy.addPreProcessor(this.prepareUpload);
  }

  uninstall() {
    this.uppy.removePreProcessor(this.prepareUpload);
  }
}

或者,您可以将方法定义为类字段:

class MyPlugin extends UIPlugin {
  constructor(uppy, opts) {
    super(uppy, opts);
    this.id = opts.id || "MyPlugin";
    this.type = "example";
  }

  // 这里!
  prepareUpload = (fileIDs) => {
    // ← this!
    console.log(this); // `this` 指向 `MyPlugin` 实例。
    return Promise.resolve();
  };

  install() {
    this.uppy.addPreProcessor(this.prepareUpload);
  }

  uninstall() {
    this.uppy.removePreProcessor(this.prepareUpload);
  }
}

进度事件

可以为单个文件触发进度事件,以便向用户显示反馈信息。对于上传进度事件,仅发出预期的字节数和已上传的字节数就足够了。Uppy 将处理计算进度百分比、上传速度等。

预处理和后处理进度事件依赖于插件,并且可以指代任何内容,因此 Uppy 不会尝试进行智能处理。处理进度事件可以是确定性或不确定性的。有些处理除了 “未完成” 和 “已完成” 之外没有有意义的进度。例如,发送请求以初始化用作上传目标的服务器端资源。在这种情况下,不确定性的进度是合适的。其他处理具有有意义的进度。例如,加密大文件。在这种情况下,确定性的进度是合适的。

以下是相关事件:

JSX

由于 Uppy 使用 Preact 而不是 React,因此默认的 Babel 配置不适用于 JSX 元素。您需要导入 Preact 的 h 函数,并在文件顶部添加 /** @jsx h */ 注释,告诉 Babel 使用它。

有关 Babel 和 JSX 的更多信息,请参阅 Preact 的入门指南

/** @jsx h */
import { UIPlugin } from "@uppy/core";
import { h } from "preact";

class NumFiles extends UIPlugin {
  render() {
    const numFiles = Object.keys(this.uppy.state.files).length;

    return <div>文件数量: {numFiles}</div>;
  }
}

本地化

对于编写插件时使用的任何用户界面语言,请将其作为字符串提供给 defaultLocale 属性,如下所示:

this.defaultLocale = {
  strings: {
    youCanOnlyUploadFileTypes: "您只能上传: %{types}",
    youCanOnlyUploadX: {
      0: "您只能上传 %{smart_count} 个文件",
      1: "您只能上传 %{smart_count} 个文件",
      2: "您只能上传 %{smart_count} 个文件",
    },
  },
};

这使得它们可以通过本地化包覆盖,或者当用户传递 locale: { strings: youCanOnlyUploadFileTypes: '关于 %{types} 的其他完全不同的内容' } 时直接覆盖。要使此功能正常工作,您还需要在插件构造函数中调用 this.i18nInit()

自定义插件示例

以下是一个完整示例,展示了一个 小型插件 ,该插件在上传之前压缩图像。您可以将 compressorjs 方法替换为您需要做的任何其他工作。这对于异步操作特别有用,如调用外部 API。

import { UIPlugin } from "@uppy/core";
import Translator from "@uppy/utils/lib/Translator";
import Compressor from "compressorjs/dist/compressor.esm.js";

class UppyImageCompressor extends UIPlugin {
  constructor(uppy, opts) {
    const defaultOptions = {
      quality: 0.6,
    };
    super(uppy, { ...defaultOptions, ...opts });

    this.id = this.opts.id || "ImageCompressor";
    this.type = "modifier";

    this.defaultLocale = {
      strings: {
        compressingImages: "正在压缩图片...",
      },
    };

    // 我们在 `this.compress` 内部使用这些,所以它们
    // 不应被覆盖
    delete this.opts.success;
    delete this.opts.error;

    this.i18nInit();
  }

  compress(blob) {
    return new Promise(
      (resolve, reject) =>
        new Compressor(blob, {
          ...this.opts,
          success(result) {
            return resolve(result);
          },
          error(err) {
            return reject(err);
          },
        })
    );
  }

  prepareUpload = (fileIDs) => {
    const promises = fileIDs.map((fileID) => {
      const file = this.uppy.getFile(fileID);
      this.uppy.emit("preprocess-progress", file, {
        mode: "indeterminate",
        message: this.i18n("compressingImages"),
      });

      if (!file.type.startsWith("image/")) {
        return;
      }

      return this.compress(file.data)
        .then((compressedBlob) => {
          this.uppy.log(
            `[Image Compressor] 图像 ${file.id} 压缩前/后的大小: ${file.data.size} / ${compressedBlob.size}`
          );
          this.uppy.setFileState(fileID, { data: compressedBlob });
        })
        .catch((err) => {
          this.uppy.log(`[Image Compressor] 压缩 ${file.id} 失败:`, "warning");
          this.uppy.log(err, "warning");
        });
    });

    const emitPreprocessCompleteForAll = () => {
      fileIDs.forEach((fileID) => {
        const file = this.uppy.getFile(fileID);
        this.uppy.emit("preprocess-complete", file);
      });
    };

    // 为什么一次为所有文件触发 'preprocess-complete',而不是
    // 在每个文件处理后立即触发?
    // 因为会导致 StatusBar 显示一个奇怪的 “上传 6 个文件” 按钮,
    // 当等待所有文件完成预处理时。
    return Promise.all(promises).then(emitPreprocessCompleteForAll);
  };

  install() {
    this.uppy.addPreProcessor(this.prepareUpload);
  }

  uninstall() {
    this.uppy.removePreProcessor(this.prepareUpload);
  }
}

export default UppyImageCompressor;
在本文档中