发布于 2026-01-06 6 阅读
0

Active Storage 与 GraphQL 的结合:直接上传

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突变...

GraphiQL 中的突变预览

GraphiQL 中的突变预览

遗憾的是,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
Enter fullscreen mode Exit fullscreen mode

请查看该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
Enter fullscreen mode Exit fullscreen mode

现在,要从服务器检索直接上传的有效负载,您的 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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

以及一些 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
      });    
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

FileChecksum该类负责计算所需的校验和,并由 Active Storage 在DirectUpload类中使用。

现在您可以使用此函数来构建 GraphQL 查询有效负载:

// pseudo code
getFileMetadata(file).then((input) => {
  return performQuery(
    CREATE_DIRECT_UPLOAD_QUERY,
    variables: { input }
  );
});
Enter fullscreen mode Exit fullscreen mode

现在是时候编写一个函数,将文件直接上传到存储服务了!

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();
      }
    })
  });
};
Enter fullscreen mode Exit fullscreen mode

我们完整的客户端代码示例如下:

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!
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

看来我们成功了!希望这对您构建出色的 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
Enter fullscreen mode Exit fullscreen mode

就是这样!


请访问https://evilmartians.com/chronicles阅读更多开发者文章

文章来源:https://dev.to/evilmartians/active-storage-meets-graphql-direct-uploads-3n38