12k
All articles

Uppy 6.0 与可恢复文件上传

使用Uppy和Tus设置可续传文件上传,了解Uppy 6.0中S3签名与core包的变化,并查看从5.x升级的关键步骤。

OpenReplay Team
OpenReplay Team
Uppy 6.0 与可恢复文件上传

Uppy 是一款开源的 JavaScript 文件上传组件。搭配 @uppy/tus 插件使用时,即使连接中断或标签页被关闭,它也能恢复大文件上传,用户无需从头重新发送文件。

大多数团队都是在收到同一类 bug 反馈后才开始寻找现成的库:某位用户在酒店 Wi-Fi 下上传一个 2 GB 的视频,进度到了 90% 时连接断开,上传进度随即归零。如果你是参照我们的构建文件上传拖放区或 React 自定义文件上传等指南自行开发的上传组件,那么到了这一步,这种做法就不够用了。

本文将介绍可恢复上传的重要性、Uppy 的最简配置、Uppy 6.0 的变化,以及从 5.x 升级所需的工作量。

核心要点

  • Tus 是一种基于 HTTP 的开放协议,用于实现可恢复上传。连接中断或标签页关闭都不会导致进度丢失:上传会从服务器已接收的最后一个字节处继续。
  • Uppy 6.0 围绕三种签名模式(getCredentials、signRequest 和 companionEndpoint)重写了 @uppy/aws-s3,每种配置只能使用其中一种。
  • 在 Uppy 6.0 中,@uppy/utils、@uppy/store-default、@uppy/companion-client 和 @uppy/provider-views 已并入 @uppy/core,以子路径导出(subpath exports)的形式提供。
  • 使用 uppy 元包(meta-package)或 CDN 构建包的项目,迁移到 6.0 时几乎无需修改代码。
  • 自托管 Companion 的用户必须先将服务端升级到 Companion 7,再将客户端升级到 @uppy/core 6,因为 OAuth 令牌现在通过 Companion 的 WebSocket 传输。此外,Companion 7 基于 Express 5 运行。

为什么选择 Uppy,而不是自己实现上传组件?

基于单个 fetch 或 XMLHttpRequest POST 请求自行实现的上传组件无法断点续传。一旦连接中断,所有已发送的字节都将作废。Tus 在协议层面解决了这个问题:客户端先询问服务器已经收到了多少数据,然后只发送剩余部分。借助会话回放(session replay),缺少这一能力的代价一目了然:上传失败后,你会看到用户回到文件选择器,重新选择同一个大文件。

Tus 可恢复上传确实需要服务端配合。@uppy/tus 文档推荐使用 tusd,Tus 服务器既可以将文件存储在本地,也可以存储到 S3 等外部存储中。

使用 Dashboard 和 Tus 的最简 Uppy 配置

只需三个包即可搭建一个完整的可恢复上传组件:@uppy/core 负责管理状态,@uppy/dashboard 提供 UI,@uppy/tus 负责传输。

import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Tus from '@uppy/tus';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';

new Uppy()
	.use(Dashboard, { inline: true, target: 'body' })
	// Point this at your own tus server, e.g. a tusd instance
	.use(Tus, { endpoint: 'https://your-tus-server/files/' });

inline: true 会将 Dashboard 直接渲染到目标元素中,而不是以模态框形式打开。如果你更希望自行构建进度 UI,可以参阅我们关于构建上传进度条的文章,其中介绍了相关的底层事件。

Google Drive 等远程来源需要经由 Uppy 的服务端组件 Companion 处理。Companion 在服务器端拉取文件,因此文件数据不会经过用户的浏览器。这里有一个细节需要注意:@uppy/tus 中的 chunkSize 设置仅对从用户设备发送的文件生效。经由 Companion 的上传遵循 Companion 自身的 chunkSize 设置,因此如果需要限制每个请求的大小,两处都要设置。

默认情况下,@uppy/tus 会按一系列延迟间隔重试失败的分块,并限制同时进行的上传数量。当服务器返回 HTTP 429 时,其默认的 onShouldRetry 逻辑会暂停所有排队中的请求,等待一段时间后再重试,并依次使用 retryDelays 中的延迟值。

Uppy 6.0 新特性

Uppy 6.0 有三项主要变化:@uppy/aws-s3 被彻底重写,四个辅助包被合并到 @uppy/core 中,@uppy/instagram 被移除。根据 Uppy 6.0 发布公告,共有六个包进行了主版本号升级:@uppy/core、@uppy/companion、@uppy/aws-s3、@uppy/tus、@uppy/components 和 uppy。

在 Uppy 5 中,旧版 @uppy/aws-s3 插件最多有八个回调函数,所需的回调组合还会因配置不同而变化:分片上传还是单次上传、是否使用 Companion、在浏览器中签名还是在服务器上签名。重写后的版本用三种签名模式取代了这一切,你只需选择其中一种:

模式签名位置是否需要 Companion
getCredentials在浏览器中(SigV4),使用从你的端点获取的临时凭证否
signRequest你自己的签名服务否
companionEndpointCompanion是
import Uppy from '@uppy/core';
import AwsS3 from '@uppy/aws-s3';

new Uppy().use(AwsS3, {
	// Swap this for an R2, MinIO or Spaces endpoint
	s3Endpoint: 'https://my-bucket.s3.eu-west-1.amazonaws.com',
	region: 'eu-west-1',
	getCredentials: async () => fetch('/s3-credentials').then((r) => r.json()),
});

上传到 S3 不再需要 Companion。使用 getCredentials 时,浏览器会自行为每个请求签名,并且该插件兼容任何 S3 兼容服务,包括 Cloudflare R2、MinIO 和 DigitalOcean Spaces。@uppy/aws-s3 参考文档列出了 getCredentials 必须返回的字段,因此在上面的示例中,你的 /s3-credentials 端点必须返回完全一致的数据结构。发布公告还提到,此次重写修复了重试退避、凭证过期、离线检测以及分片上传暂停/恢复等方面长期存在的 bug。该插件的 CI 测试现在基于真实的 MinIO 服务器运行,而不再使用 mock。

如何从 Uppy 5 升级到 6?

如果你使用的是 uppy 元包或 CDN 构建包,升级到 6.0 几乎无需修改代码。其他用户请按以下清单逐项处理,完整细节请参阅官方迁移指南:

  • 单独安装的包: 将 @uppy/utils、@uppy/store-default、@uppy/companion-client 和 @uppy/provider-views 的导入改为对应的 @uppy/core 子路径。旧包已在 npm 上被标记为弃用(deprecated)。
  • 自托管 Companion: 先升级到 Companion 7,再将客户端升级到 @uppy/core 6。Companion 7 仍然支持旧版 Uppy 客户端,但 Uppy 6 客户端无法与 Companion 6 或更早版本配合使用。Companion 7 基于 Express 5 构建,无法作为中间件在 Express 4 应用中运行,因此请在切换之前先将应用迁移到 Express 5。companion.socket() 现在需要将 companionOptions 作为第二个参数传入。使用托管版 Companion 的用户无需任何操作。
  • Tus 错误处理: 请重新检查你的 upload-error 相关代码,因为 response 现在会被填充。
- import { fetcher } from '@uppy/utils'
+ import { fetcher } from '@uppy/core/utils'

- import { RequestClient } from '@uppy/companion-client'
+ import { RequestClient } from '@uppy/core/companion-client'

CompanionClientProvider 和 CompanionClientSearchProvider 也已被移除,请改为从 @uppy/core/companion-client 导入 Provider。此次合并旨在避免一种特定的故障:lockfile 中保留了旧版 @uppy/utils,同时又存在较新的 @uppy/core,导致同一个依赖树中出现同一个包的两个副本。

从 Uppy 6.0 开始,@uppy/tus 会将服务器的状态码和响应体传递给 upload-error 事件以及 file.response。早期版本在触发错误时不包含服务器的响应。

uppy.on('upload-error', (file, error, response) => {
	if (error.isNetworkError) return showOfflineNotice(); // placeholder helper
	console.log(response?.status);
	console.log(response?.body.xhr.responseText);
});

在以下两种情况下,response 仍为 undefined:服务器从未响应(例如服务器无法访问),或者文件是经由 Companion 上传的远程文件。

在 React 及其他框架中使用 Uppy

借助 Uppy 的框架封装,组件可以直接订阅 Uppy 状态,无需手动绑定事件。在 React 中,@uppy/react 提供的 useUppyState hook 接收一个 Uppy 实例和一个 selector,并在所选状态片段发生变化时重新渲染组件。完整 API 请参阅 React 集成文档。

import { useState } from 'react';
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import { useUppyState } from '@uppy/react';

function UploadStatus() {
	const [uppy] = useState(() =>
		new Uppy().use(Tus, { endpoint: 'https://your-tus-server/files/' }),
	);
	const files = useUppyState(uppy, (state) => state.files);
	const progress = useUppyState(uppy, (state) => state.totalProgress);
	return <p>{Object.keys(files).length} files, {progress}% uploaded</p>;
}

useState 的惰性初始化函数确保每个组件实例只创建一次 Uppy 实例,而不是每次渲染都重新创建。Uppy 还提供了 Vue、Svelte 和 Angular 的封装。

何时不应使用 Uppy

对于单个小文件(例如用户头像),无需使用 Uppy 或 Tus。一个普通的表单 POST 请求发送到你自己的端点就足够了,引入 Tus 反而会增加一项需要你运维的服务端集成。如果你之后想使用 Uppy 的 UI,但保留常规的 HTTP 端点,可以使用 @uppy/xhr-upload:它既可以发送标准的 multipart 表单上传,也可以发送 PUT 请求。接收端的实现可参阅我们的 Node 中使用 Multer 上传文件指南。

结语

可恢复上传正是值得引入 Uppy 的原因。Uppy 6.0 让它更易于运维:S3 插件统一为一个,每种配置只使用一种签名模式;核心包也从原先需要保持版本同步的五个精简为一个。入门时,只需安装 @uppy/core、@uppy/dashboard 和 @uppy/tus,将 Tus 插件指向一个 tusd 实例,然后在限速网络下上传一个大文件进行测试。如果你已经在使用 5.x,请优先处理 Companion 和 upload-error 相关的变更,因为它们最有可能导致现有代码出错。

常见问题

@uppy/aws-s3 中的 getCredentials 函数需要返回什么?

它需要返回一个对象,其中 credentials 属性包含以 camelCase 命名的临时凭证 accessKeyId、secretAccessKey 和 sessionToken,以及可选的 expiration;此外还需包含用于签名的 region。AWS STS 返回的这些键名采用 PascalCase,因此需要先进行映射。Uppy 会在插件的整个生命周期内缓存该结果。遇到 ExpiredToken 或 InvalidAccessKeyId 错误时,它会重新获取凭证并重试一次。由于这些凭证保存在浏览器中,请务必严格限定所附加策略的权限范围。

Uppy 6.0 移除了 @uppy/aws-s3 的哪些选项?shouldUseMultipart 还能用吗?

shouldUseMultipart 在 @uppy/aws-s3 6.0 中仍然可用,getChunkSize、allowedMetaFields 和 limit 也同样保留。被移除的选项包括 endpoint、headers、cookiesRule、getTemporarySecurityCredentials、getUploadParameters、signPart、createMultipartUpload、listParts、abortMultipartUpload、completeMultipartUpload、uploadPartBytes 和 retryDelays。现有的 Companion 配置只需将 endpoint 重命名为 companionEndpoint 即可完成迁移。headers 和 cookiesRule 没有直接的替代方案:同源 Cookie 仍然会发送到 Companion,但 bearer token 或跨站 Cookie 需要其他方案,例如在 Companion 前部署一个负责鉴权的代理。

Tus 可恢复上传与 @uppy/golden-retriever 有什么区别?

Tus 让服务器保留已接收的字节,从而使传输得以继续;而 Golden Retriever 则在崩溃或标签页关闭后,恢复浏览器端的文件选择和 Uppy 状态。在 Uppy 6.0 中,Golden Retriever 默认将元数据、状态以及不超过 10 MiB 的本地文件存储在 IndexedDB 中,并在必要时回退到 localStorage 存储元数据。更大的文件需要使用可选的 Service Worker 存储,它在标签页关闭后仍可保留数据,但无法在浏览器重启后保留。

Uppy 6 客户端能否连接旧版 Companion 服务器?

不能。Uppy 6 客户端需要 Companion 7,因为 OAuth 令牌现在通过 Companion 的 WebSocket 连接传输,而不再通过 window.opener。Companion 7 为 6.0 之前的 Uppy 客户端保留了 postMessage 作为回退方案,因此请先部署 Companion 7,再升级客户端。在服务端,向 companion.socket() 传入与 companion.app() 相同的选项对象。使用托管版 Companion 的用户无需任何更改。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.