今回は Rails と Ember.js で作っているブログアプリで記事を作成・更新する際に画像を添付できるようにしていきたいと思います。
最終的には記事をマークダウンで書いて、その中の任意の箇所に画像を挿入できるようにすることを目指していて、その前段階の実装です。
前回はトーストメッセージの実装について書きました。
ActiveStorageを使って画像を1枚添付できるようにする
画像添付にあたっては Rails の ActiveStorage を使っていきます。
ここでは ActiveStorage の細かな導入手順には触れません。 画像を複数枚添付できるようにすることを目指しますが、まずは1枚だけ添付できるようにすることからやっていきます。
まず、Rails 側の実装を進めていきます。 モデルとコントローラを以下のように修正します。
# app/models/post.rb class Post < ApplicationRecord has_one_attached :image # 追加 validates :title, presence: true validates :body, presence: true end
# app/controllers/posts_controller.rb # 省略 private def post_params params.expect(post: [ :title, :body, :image ]) # :image を追加 end end
続いて、Ember.js 側の実装を見ていきます。
ブログの新規投稿画面と更新画面に画像添付のボタンが出るようにします。
input
タグに設定している action の詳細は後ほど確認します。
<!-- web/app/components/post-form.hbs --> <form {{on "submit" @onSubmit}}> <!-- 省略 --> <button type="submit" class="btn btn-primary me-2">{{@submitLabel}}</button> <!-- 以下を追記 --> <input type="file" id="file" name="file" multiple {{on "change" this.uploadImage}} /> </form>
これでひとまず画像添付のためのボタンが表示されるようになりました。
これだけでは Rails 側へ画像添付のリクエストを送れないため、そちらの実装も見ていきます。
その際、npm として提供されている ActiveStorage のパッケージを利用します。ダイレクトアップロード機能を使ってEmber.jsから画像ファイルをサーバーに直接送信できるようにしていきます。 ここでちょっと困ったのがどのようにダイレクトアップロードの機能を使えばいいかが具体的に書かれたドキュメントが見つけられませんでした...。
そのため、ActiveStorage の実装を確認して、ダイレクトアップロードに必要な処理をEmber.js側で実装していきます。
// node_modules/@rails/activestorage/src/direct_upload.js import { FileChecksum } from "./file_checksum" import { BlobRecord } from "./blob_record" import { BlobUpload } from "./blob_upload" let id = 0 export class DirectUpload { constructor(file, url, delegate, customHeaders = {}) { this.id = ++id this.file = file this.url = url this.delegate = delegate this.customHeaders = customHeaders } create(callback) { FileChecksum.create(this.file, (error, checksum) => { if (error) { callback(error) return } const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders) notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr) blob.create(error => { if (error) { callback(error) } else { const upload = new BlobUpload(blob) notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr) upload.create(error => { if (error) { callback(error) } else { callback(null, blob.toJSON()) } }) } }) }) } } function notify(object, methodName, ...messages) { if (object && typeof object[methodName] == "function") { return object[methodName](...messages) } }
ダイレクトアップロードをしたいため、node_modules/@rails/activestorage/src/direct_upload.js
の実装内容を見ていきます。
まず、Ember.js 側で DirectUpload オブジェクトを作成するために最低限必要な引数を確認します。
constructor
の実装を見てみると customHeaders = {}
は省略可能で、file
, url
, delegate
を引数として指定する必要があることが分かります。delegate
について簡単に調べてみると、これを使用するとアップロードの際の進捗や独自のエラーハンドリングなどをフックすることが可能なようです。今回はそこまでは求めないため、{}
を引数にして省略することにします。Ember.js側で考慮しなければいけない引数はfile
, url
の2つになりそうです。
constructor(file, url, delegate, customHeaders = {}) { this.id = ++id this.file = file this.url = url this.delegate = delegate this.customHeaders = customHeaders }
また、node_modules/@rails/activestorage/src/direct_upload.js
の実装から作成した DirectUpload オブジェクトに対して、create
にerror
を第1引数、blob
を第2引数にもつコールバック関数を渡して呼び出せば良さそうなことが分かりました。
これらを考慮して Ember.js 側で画像添付をする際の action を実装していきます。
// app/components/post-form.js import Component from '@glimmer/component'; import { DirectUpload } from '@rails/activestorage/src/direct_upload'; import { action } from '@ember/object'; export default class PostFormComponent extends Component { @action uploadImage(e) { const upload = new DirectUpload( e.target.files[0], 'http://localhost:3000/rails/active_storage/direct_uploads', {}, ); upload.create((error, blob) => { this.args.post.image = blob.signed_id; }); } }
DirectUpload オブジェクトの作成部分を見ていきます。先ほど調べた通り、file
, url
を指定して、delegate
は省略するようにしています。
file
を指している e.target.files
の実態は FileList オブジェクトです。HTMLの <input type="file">
要素でファイルが選択されると、イベントオブジェクト e
の e.target.files
プロパティには FileList オブジェクトが格納されます。
console.log(e.target.files)
を仕込んでブラウザーの開発者ツールで確認してみます。
FileList オブジェクトは配列のように扱うことが可能なため、 e.target.files[0]
とすることで選択されたファイルにアクセスすることができます。
url
に指定しているのはActive Storageが提供するアップロード用のエンドポイントです。※とりあえず localhost:3000
をベタ書きしています。
そして先ほど確認したcreate
の呼び出し部分で、this.args.post.imag
にblob.signed_id
をセットします。こうすることで、フォーム送信時に image パラメータとしてこの signed_id
が送信され、Rails側で has_one_attached :image
に添付されるようになります。
image パラメータはブログ記事の新規作成と更新時に一緒に送られるように修正します。
// web/app/controllers/admin/posts/new.js import { action } from '@ember/object'; import { service } from '@ember/service'; export default class AdminPostsNewController extends Controller { @service router; @service toast; @service session; @action async createPost(event) { event.preventDefault(); const response = await fetch('http://localhost:3000/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.session.token}`, }, body: JSON.stringify({ post: { title: this.model.title, body: this.model.body, image: this.model.image, // 追加 }, }), });
これで Ember.js から Rails に対して画像の情報を一緒に送ることができるようになりました!
最後に画像の表示について見ていきます。
まず、Rails から画像の表示に必要な情報(URL)を提供するように修正します。
// app/views/posts/_post.json.jb { id: post.id, created_at: post.created_at, title: post.title, body: post.body, image_url: post.image.attached? ? rails_blob_url(post.image) : nil }
画像は添付されていない場合もあるため三項演算子で添付されている場合だけ、rails_blob_url
ヘルパーでURLを作成しています。
rails_blob_url
ヘルパーで作成される URL は rails c
でコンソールからも確認できます。
> blob = ActiveStorage::Blob.last > include Rails.application.routes.url_helpers > rails_blob_path(blob, only_path: true) => "/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6ODQsInB1ciI6ImJsb2JfaWQifX0=--85409ae096792a2a9aa26383c7dc0d2dbc861d46/food_konbini_onigiri.png" > blob.signed_id => "eyJfcmFpbHMiOnsiZGF0YSI6ODQsInB1ciI6ImJsb2JfaWQifX0=--85409ae096792a2a9aa26383c7dc0d2dbc861d46"
ここまで何度か signed_id
というものが登場しましたが、これはActiveStorageのBlobのidを暗号化したものです。
また、余談になりますがブラウザから画像を選択した時点で ActiveStorage::Blob
は作成されるため、保存・更新前の段階でも rails c
でコンソールから確認が可能です。
今回のようにフロント側が Rails ではない場合にちゃんと ActiveStorage::Blob
が作成されているかを段階を追って確認できます。
説明が長くなってしまいましたが、あとは image_url
を使って画像を表示させるだけです。
<!-- web/app/components/post.hbs --> <div class="body card-text"> {{@post.body}} {{#if @post.image_url}} <img src={{@post.image_url}} alt=""> {{/if}} </div>
まとめ
Ember.js と Rails で ActiveStorage を使って画像を1枚添付するやり方を見てきました。
実装をしてみて、direct_upload.js の内容を確認した部分が面白くて勉強になりました。ActiveStorage への理解もだいぶ深まったと思います。
次は第2弾として ActiveStorage で複数枚の画像を添付できるようにする部分をまとめたいと思います。