Active Storage 与 GraphQL 的结合:直接上传
第二部分请点击此处查看。
快乐的人,才能写出快乐的代码
首先我要郑重声明:我是一个快乐的人。
这种幸福是多维度的,有些维度比其他维度更有价值(抱歉,我今天说的不是最有价值的那些维度)。
例如,我很高兴在工作中能够使用Rails 6这样“前沿”(如果可以这样形容一个 15 年前的框架的话 😉)技术从头开始构建一个项目。
信不信由你,我从事 Rails 生产项目已经 5 年了,但我甚至都没接触过 Rails 5,只用过 Rails 4!
“遗留 Rails 应用程序”是我的一个专长(顺便说一句,这也是我即将在RailsConf会议上发表的主题)。
这段黑暗时期已于今年一月结束:我跑了gem install rails --prerelease && rails new ***。
(实际上,确实如此rails new *** -d postgresql --skip-action-mailbox --skip-action-text --skip-action-cable --skip-sprockets --skip-spring --skip-test --skip-bundle。)
我正在开发的项目并不是一个全新的代码库;它包含很多最初为 Rails 4 编写的代码。
它以GraphQL API 作为客户端(Web 和移动应用程序)的主要入口点。
为了将旧代码库移植到新应用中,我们从CarrierWave迁移到了Active Storage。整个过程非常顺利。虽然 Active Storage 还有一些 功能缺失 ,但它也有自身的优势,并且秉承了Rails 一贯的简洁性。
注意:如果您是 Active Storage 的新手,请查看我一年前与同事一起写的文章:“Rails 5.2:Active Storage 及更多”。
因此,现在是时候来看看 Active Storage 最显著的优势了:开箱即用的直接上传功能。
主动存储之前的生命
首先,让我来告诉你我们在 Rails 4 中是如何处理文件上传的。GraphQL 规范和graphqlRuby gem 都没有规定如何正确地处理文件上传。
有一个开源规范,它用包括Ruby在内的多种语言实现了该规范。它“描述”了Upload标量类型,利用Rack 中间件的一些技巧将上传的文件作为变量传递,并且几乎是透明地运行的。
听起来像是“即插即用”。理论上是这样。但实际上,它变成了“即插即用-失败-修复-失败-修复”:
- 客户端实现存在缺陷(尤其是 React Native 客户端)。
Upload非严格类型(不关心对象的实际类型)带来的副作用- 对 Apollo 的依赖(是的,我们在新版本中已经和 Apollo 说了“再见!”;但这又是另一个故事了)。
不出所料(也无需担心😉),我们决定放弃这种权宜之计,改用传统的 REST 来上传文件。
现在,Active Storage 应运而生,它支持直接上传。
指导上传🎥
顺便问一下,“直接上传”是什么?
该术语通常与云存储服务(例如 Amazon S3)一起使用,其含义如下:客户端不是使用 API 服务器上传文件,而是使用 API 服务器生成的凭证直接将文件上传到云存储。
好消息——Active Storage 提供了一个服务器端 API 来处理直接上传,以及一个开箱即用的前端 JS 客户端。
另一个好消息是——这个 API 是抽象的,可以与 Active Storage 支持的任何服务(例如文件系统、S3、GCloud、Azure)配合使用。这很棒:您可以在本地使用文件系统,在生产环境中使用 S3,而无需进行各种复杂的if配置else-。
好消息往往伴随着坏消息。坏消息是,Active Storage(以及 Rails 本身)对 GraphQL 一无所知,它依赖自身的 REST API 来获取直接上传凭据。
我们需要哪些条件才能在 GraphQL 中实现这一切?
首先,能够使用 GraphQL API(通过mutation)获取直接上传凭据。
其次,尽可能多地重用框架中的 JavaScript 代码,避免重复造轮子,这将非常好。
createDirectUpload突变...
遗憾的是,Rails 没有提供服务器端直接上传实现的任何文档。
我们只有以下代码的源代码DirectUploadsController:
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
render json: direct_upload_json(blob)
end
private
def blob_args
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :metadata).to_h.symbolize_keys
end
def direct_upload_json(blob)
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
url: blob.service_url_for_direct_upload,
headers: blob.service_headers_for_direct_upload
})
end
请查看该checksum参数:这是 Active Storage 的众多隐藏功能之一——内置的文件内容验证。
当客户端请求直接上传时,它可以指定文件的校验和(以Base64编码的MD5哈希),服务(例如 Active Storage 本身或 S3)稍后将使用此校验和来验证上传的文件内容。
让我们回到 GraphQL。
GraphQL mutation 与 Rails controller 非常相似,因此将上面的代码转换为 mutation 非常简单:
class CreateDirectUpload < GraphQL::Schema::Mutation
class CreateDirectUploadInput < GraphQL::Schema::InputObject
description "File information required to prepare a direct upload"
argument :filename, String, "Original file name", required: true
argument :byte_size, Int, "File size (bytes)", required: true
argument :checksum, String, "MD5 file checksum as base64", required: true
argument :content_type, String, "File content type", required: true
end
argument :input, CreateDirectUploadInput, required: true
class DirectUpload < GraphQL::Schema::Object
description "Represents direct upload credentials"
field :url, String, "Upload URL", null: false
field :headers, String,
"HTTP request headers (JSON-encoded)",
null: false
field :blob_id, ID, "Created blob record ID", null: false
field :signed_blob_id, ID,
"Created blob record signed ID",
null: false
end
field :direct_upload, DirectUpload, null: false
def resolve(input:)
blob = ActiveStorage::Blob.create_before_direct_upload!(input.to_h)
{
direct_upload: {
url: blob.service_url_for_direct_upload,
# NOTE: we pass headers as JSON since they have no schema
headers: blob.service_headers_for_direct_upload.to_json,
blob_id: blob.id,
signed_blob_id: blob.signed_id
}
}
end
end
# add this mutation to your Mutation type
field :create_direct_upload, mutation: CreateDirectUpload
现在,要从服务器检索直接上传的有效负载,您的 GraphQL 客户端必须执行以下请求:
mutation {
createDirectUpload(input: {
filename: "dev.to", # file name
contentType: "image/jpeg", # file content type
checksum: "Z3Yzc2Q5iA5eXIgeTJn", # checksum
byteSize: 2019 # size in bytes
}) {
directUpload {
signedBlobId
}
}
}
以及一些 JavaScript
免责声明:以下JS实现只是一个草图,尚未经过实际测试(因为我的项目中没有使用任何Rails的JS代码)。我只检查了它能否编译。
要上传文件,客户必须执行以下步骤:
- 获取文件元数据(文件名、大小、内容类型和校验和)
- 通过 API 请求直接上传凭据和 blob ID –
createDirectUploadmutation - 使用凭据上传文件(不涉及 GraphQL,HTTP PUT 请求)。
对于步骤 1 和 3,我们可以重用 Rails 自带的 JS 库中的一些代码(不要忘记添加"@rails/activestorage"到你的package.json)。
我们来编写一个getFileMetadata函数:
import { FileChecksum } from "@rails/activestorage/src/file_checksum";
function calculateChecksum(file) {
return new Promise((resolve, reject) => {
FileChecksum.create(file, (error, checksum) => {
if (error) {
reject(error);
return;
}
resolve(checksum);
});
});
}
export const getFileMetadata = (file) => {
return new Promise((resolve) => {
calculateChecksum(file).then((checksum) => {
resolve({
checksum,
filename: file.name,
content_type: file.type,
byte_size: file.size
});
});
});
};
FileChecksum该类负责计算所需的校验和,并由 Active Storage 在DirectUpload类中使用。
现在您可以使用此函数来构建 GraphQL 查询有效负载:
// pseudo code
getFileMetadata(file).then((input) => {
return performQuery(
CREATE_DIRECT_UPLOAD_QUERY,
variables: { input }
);
});
现在是时候编写一个函数,将文件直接上传到存储服务了!
import { BlobUpload } from "@rails/activestorage/src/blob_upload";
export const directUpload = (url, headers, file) => {
const upload = new BlobUpload({ file, directUploadData: { url, headers } });
return new Promise((resolve, reject) => {
upload.create(error => {
if (error) {
reject(error);
} else {
resolve();
}
})
});
};
我们完整的客户端代码示例如下:
getFileMetadata(file).then((input) => {
return performQuery(
CREATE_DIRECT_UPLOAD_QUERY,
variables: { input }
).then(({ directUpload: { url, headers, signedBlobId }) => {
return directUpload(url, JSON.parse(headers), file).then(() => {
// do smth with signedBlobId – our file has been uploaded!
});
});
});
看来我们成功了!希望这对您构建出色的 Rails + GraphQL 项目有所帮助。
想要查看更实际的示例,请查看我们 React Native 应用中的这段代码片段:https://gist.github.com/Saionaro/7ee0e2c02749e2729dc429c9e9bfa7f3
总之,或者说,该如何处理……signedBlobId
让我举一个简单的例子来说明我们在应用程序中如何使用签名 blob ID——即attachProfileAvatarmutation:
class AttachProfileAvatar < GraphQL::Schema::Mutation
description <<~DESC
Update the current user's avatar
(by attaching a blob via signed ID)
DESC
argument :blob_id, String,
"Signed blob ID generated via `createDirectUpload` mutation",
required: true
field :user, Types::User, null: true
def resolve(blob_id:)
# Active Storage retrieves the blob data from DB
# using a signed_id and associates the blob with the attachment (avatar)
current_user.avatar.attach(blob_id)
{user: current_user}
end
end
就是这样!
请访问https://evilmartians.com/chronicles阅读更多开发者文章!
文章来源:https://dev.to/evilmartians/active-storage-meets-graphql-direct-uploads-3n38

