内容简介:本文為使用由於我們在這個實作會練習使用
本文為使用 Rails ActiveStorage 的實作範例筆記。詳細介紹請參考Active Storage 概要,本文僅針對官方教學提供一個對照的實作記錄,如需部署至 Heroku 請參考 在 Heroku 使用 Active Storage 。
建立 Rails 專案與安裝
# 這邊為了後續介紹與 stimulus 搭配我們直接先帶入 --webpack=stimulus 參數 $ rails new active_storage_sample --webpack=stimulus --skip-coffee --skip-test $ rails active_storage:install $ rails db:migrate # 設定 config/storage.yml 提供的方式 # 設定 config/environments 環境使用的方式 # 完整範例 https://github.com/andyyou/active-stroage-sample
由於 active_storage 會使用兩張 table 記錄資料所以需要 migrate 。
我們在這個實作會練習使用 aws s3 來儲存檔案, 即便不預先設定還是可以在使用本地磁碟的方式測試 。
如果您不想在這邊練習使用 aws 可以直接跳至下一節。
# 加入 s3 access key $ EDITOR=vim rails credentials:edit
config/storage.yml 設定
amazon: service: S3 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> region: ap-northeast-2 bucket: your_own_bucket
開啟 Gemfile 加入 aws-sdk-s3 並執行 bundle
安裝。
Active Storage 的核心功能需要以下權限: s3:ListBucket
、 s3:PutObject
、 s3:GetObject
和 s3:DeleteObject
。如果你設定了其它上傳選項,如 ACL 設定,則可能需要額外的權限。
注意:記得要設定 config/environments/development.rb
config.active_storage.service = :amazon
標準 Form Post 方式
新增圖片(單檔/多檔)
使用官方提供的標準方式上傳檔案
# 建立 event scaffold 我們將練習使用 單檔上傳、多檔上傳、upload_direct 參數 $ rails g scaffold event name $ rails db:migrate
active_storage 的使用方式非常簡單:
1 調整 models/event.rb
class Event< ApplicationRecord has_one_attached :cover # 單檔 has_many_attached :banners # 多檔 end
2 調整 views/events/_form.html.erb
<divclass="field"> <%=form.label:cover%> <%=form.file_field:cover%> </div> <divclass="field"> <%=form.label:banners%> <%=form.file_field:banners,multiple:true%> </div>
3 調整 controllers/events_controller.rb
# premit cover 和 banners def event_params params.require(:event).permit(:name, :cover, banners: []) end # 以下為說明,不需使用於範例 # 若要附加圖片可以使用 attach @event.attach(params[:cover]) # 同步刪除頭像和實際資源檔案。 @event.cover.purge # 透過 Active Job 非同步刪除相關模型和實際資源檔案。 @event.cover.purge_later
4 為了觀察結果與使用呈現的相關 helpers ,調整 views/events/show.html.erb
加上
<p> <strong>Cover:</strong> <div> <%=image_tag@event.coverif@event.cover.attached? %> </div> </p> <p> <strong>Banners:</strong> <div> <%@event.banners.eachdo|banner| %> <%=image_tagbanner%> <%end%> </div> </p>
以上就是最基本的使用方式,我們可以啟動 rails s
並瀏覽 localhost:3000/events
來觀察目前的結果。
調整圖片尺寸
1 要使用調整圖片尺寸的功能須先安裝 mini_magick
,在 Gemfile 解開註解並安裝。
# Use ActiveStorage variant gem 'mini_magick', '~> 4.8'
2 接著就可以在 views/events/show.html.erb 使用 .variant(resize: '100x100')
方法。
<%=image_tag @event.cover.variant(resize:'100x100')if @event.cover.attached? %>
刪除圖片
1 config/routes.rb 新增路由
resources :events do delete :destroy_cover, on: :member end
2 controllers/events_controller.rb 新增 action 與設定 :set_event
before_action :set_event, only: [:show, :edit, :update, :destroy, :destroy_cover] def destroy_cover @event.cover.purge respond_to do |format| format.html { redirect_to event_url(@event), notice: 'Event Cover was successfully destroyed.' } format.json { head :no_content } end end
3 views/events/show.html.erb 加入刪除按鈕
<%=link_to'刪除',destroy_cover_event_path(@event),method::deleteif@event.cover.attached? %>
多檔上傳刪除單張圖片
1 config/routes.rb 新增路由
resources :events do delete :destroy_cover, on: :member # DELETE /events/:id/banners/:banner_id delete '/banners/:banner_id' => 'events#destroy_banner', as: :destroy_banner, on: :member end
2 controllers/events_controller.rb 新增 action 和設定 :set_event
before_action :set_event, only: [:show, :edit, :update, :destroy, :destroy_cover, :destroy_banner] def destroy_banner @event.banners.find(params[:banner_id]).purge respond_to do |format| format.html { redirect_to event_url(@event), notice: 'Event banner was successfully destroyed.' } format.json { head :no_content } end end
3 views/events/show.html.erb
<%@event.banners.eachdo|banner| %> <%=image_tagbanner.variant(resize:'100x100') %> <%=link_to'刪除',destroy_banner_event_path(@event,banner),method::delete%> <%end%>
多檔上傳刪除多張圖片
1 config/routes.rb 新增路由
# DELETE /events/:id/banners delete :destroy_banners, on: :member
完整路由
Rails.application.routes.draw do resources :events do delete :destroy_cover, on: :member # DELETE /events/:id/banners delete :destroy_banners, on: :member # DELETE /events/:id/banners/:banner_id delete '/banners/:banner_id' => 'events#destroy_banner', as: :destroy_banner, on: :member end # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html end
. controllers/events_controller.rb 新增 action 和設定 :set_event
before_action :set_event, only: [ :show, :edit, :update, :destroy, :destroy_cover, :destroy_banner, :destroy_banners ] def destroy_banners params[:event][:banners].each do |banner_id| @event.banners.find(banner_id).purge end respond_to do |format| format.html { redirect_to event_url(@event), notice: 'Event banners was successfully destroyed.' } format.json { head :no_content } end end
3 views/events/show.html.erb
<%=form_for(@event,url:destroy_banners_event_path,method::delete)do |form|%> <%@event.banners.eachdo |banner|%> <!--重點:使用 checkbox 勾選並一次刪除 --> <%=check_box_tag:banners, banner.id,false,name:'event[banners][]'%> <%=image_tag banner.variant(resize:'100x100')%> <%=link_to'刪除', destroy_banner_event_path(@event, banner),method::delete%> <%end %> <%if @event.banners.attached? %> <inputtype="submit"value="刪除多張圖片"> <%end %> <%end %>
到此我們已經示範了完整的基本使用方式。
Direct Upload
預設的 active storage 的流程是將圖片先送到後端,一併處理建立資料庫紀錄和上傳。但如何使用雲端服務的話,這個流程就顯得多此一舉。因此 active storage 也提供 direct upload 的方式直接把圖片從使用者端直接送往雲端服務。而我們接著要實作 ajax 方式的範例也會使用 direct upload。
安裝 activestorage.js
1 安裝套件
# 這裡我們使用 webpacker 的方式,如果需要其他方式請參考官方教學 $ yarn add activestorage
2 新增 _javascript/packs/direct upload.js
import * as ActiveStorage from 'activestorage'; ActiveStorage.start();
3 views/layouts/application.html.erb 加入 pack
<%=javascript_pack_tag'direct_upload', 'data-turbolinks-track':'reload' %>
標準 Direct Upload 使用方式
為了範例單純,這邊我們建立一個新的 Post scaffold 其包含一個 cover
和 images
但是這次我們使用不一樣的流程來完成。 cover
我們使用標準的 Direct Upload 作法, images
我們整合 ajax 與 stimulus
的作法。
1 建立 scaffold
$ rails g scaffold post title $ rails db:migrate
2 models/post.rb 加上設定
class Post< ApplicationRecord has_one_attached :cover has_many_attached :images end
3 _views/posts/_form.html.erb_ 加上
<divclass="field"> <%=form.label:cover%> <%=form.file_field:cover,direct_upload:true%> </div>
到這邊除了 direct_upload
參數跟原本的作法沒有不同,但使用 direct_upload
之後我們多了一些 hooks 可以使用。
direct_upload: true 會在渲染的 HTML 加上 data-direct-upload-url 屬性。
4 controllers/posts_controller.rb 加入 permit
def post_params params.require(:post).permit(:title, :cover, images: []) end
5 完成的 _javascript/packs/direct upload.js 如下,這是官方提供的範例
import * as ActiveStorage from 'activestorage'; ActiveStorage.start(); addEventListener("direct-upload:initialize", event => { const { target, detail } = event; const { id, file } = detail; target.insertAdjacentHTML("beforebegin", ` <div id="direct-upload-${id}" class="direct-upload direct-upload--pending"> <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div> <span class="direct-upload__filename">${file.name}</span> </div> `); }); addEventListener("direct-upload:start", event => { const { id } = event.detail; const element = document.getElementById(`direct-upload-${id}`); element.classList.remove("direct-upload--pending"); }); addEventListener("direct-upload:progress", event => { const { id, progress } = event.detail; const progressElement = document.getElementById(`direct-upload-progress-${id}`); progressElement.style.width = `${progress}%`; }); addEventListener("direct-upload:error", event => { event.preventDefault(); const { id, error } = event.detail; const element = document.getElementById(`direct-upload-${id}`); element.classList.add("direct-upload--error"); element.setAttribute("title", error); }); addEventListener("direct-upload:end", event => { const { id } = event.detail; const element = document.getElementById(`direct-upload-${id}`); element.classList.add("direct-upload--complete"); });
- 加入 css
.direct-upload { display: inline-block; position: relative; padding: 2px 4px; margin: 0 3px 3px 0; border: 1px solid rgba(0, 0, 0, 0.3); border-radius: 3px; font-size: 11px; line-height: 13px; } .direct-upload--pending { opacity: 0.6; } .direct-upload__progress { position: absolute; top: 0; left: 0; bottom: 0; opacity: 0.2; background: #0076ff; transition: width 120ms ease-out, opacity 60ms 60ms ease-in; transform: translate3d(0, 0, 0); } .direct-upload--complete .direct-upload__progress { opacity: 0.4; } .direct-upload--error { border-color: red; } input[type=file][data-direct-upload-url][disabled] { display: none; }
整合 Stimulus
1 views/layouts/application.html.erb 加入 application
<%=javascript_pack_tag'application','data-turbolinks-track':'reload'%>
2 新增 javascript/controllers/uploads_controller.rb
import { Controller } from 'stimulus'; export default class extends Controller{ connect() { console.log('connect to uploads'); } start() { console.log('start upload'); } }
3 views/posts/show.html.erb 加入我們的 upload 元素 並使用 stimulus
的 controller
。
<p> <strong>Images</strong> <divdata-controller="uploads"> <inputtype="file"multiple="true"data-action="change->uploads#start"> </div> </p>
這裡我們預計使用一個 input
,當其取得檔案的時候在搭配 stimulus 執行對應的操作。接著,我們先來處理上傳檔案的部分。
4 確認 controller 中 params 和 before_action
是否取得我們需要的資料,修改 _controllers/posts controller.rb ,我們會需要使用 ajax 來對 update
發出請求,所以我們需要對其做一些調整。一個流程我們從路由開始,我們沿用 update
,接著 controller#action
的行為。再回到前端處理。
注意:本文旨是在協助您練習可能的作法,不一定適合您的正式環境。
# PATCH/PUT /posts/1 # PATCH/PUT /posts/1.json def update respond_to do |format| if @post.update(post_params) format.html { redirect_to @post, notice: 'Post was successfully updated.' } format.json { # 遵循慣例參數為陣列,但 DirectUpload 一次只會負責一張圖片 image = ActiveStorage::Blob.find_signed(post_params[:images].first) # 從後端取的圖片(resize)的網址 image_url = Rails.application.routes.url_helpers.rails_representation_url(image.variant(resize: '100x100'), only_path: true) render json: { status: :ok, url: image_url, id: image.id } } else format.html { render :edit } format.json { render json: @post.errors, status: :unprocessable_entity } end end end
5 新增 javascript/libs/uploader.js 。這裡為了可以顯示進度,我們參考官方教學的作法。
注意:如果您是直接跳至本節,請記得安裝 activestorage.js
import { DirectUpload } from 'activestorage'; export default class { constructor(file, url, element) { this.file = file; this.url = url; this.element = element; this.directUpload = new DirectUpload(this.file, this.url, this); } upload() { return new Promise((resolve, reject) => { this.directUpload.create((error, blob) => { if (error) { reject(error); } else { resolve(blob); } }); }); } directUploadWillStoreFileWithXHR(request) { request.upload.addEventListener("progress", event => this.directUploadDidProgress(event)); } directUploadDidProgress(e) { let progress = this.element.querySelector('.progress-bar'); progress.style.width = ((e.loaded / e.total) * 100) + '%'; } }
6 views/posts/show.html.erb 由於 js 需要一些參數,這邊我們使用 stimulus 的 data api
<p> <strong>Images</strong> <div data-controller="uploads" data-uploads-model="<%= @post.to_json %>" data-uploads-direct-upload-url="<%= rails_direct_uploads_path %>" > <inputtype="file"multiple="true"data-action="change->uploads#start"> </div> </p>
7 javascript/controllers/uploads_controller.js
import { Controller } from 'stimulus'; import Uploader from 'libs/uploader'; export default class extends Controller{ start(event) { const { target } = event; const _this = this; [...target.files].forEach(file=> { // 準備 image 容器與 progress bar let wrapper = document.createElement('div'); wrapper.classList.add('img-wrapper'); wrapper.insertAdjacentHTML('afterbegin', ` <div class="progress"> <div class="progress-bar" style="width: 0%;"></div> </div> `); const insertTarget = _this.element.querySelector('input[type=file]'); _this.element.insertBefore(wrapper, insertTarget); // 開始上傳 const uploader = new Uploader(file, _this.directUploadUrl, wrapper); uploader.upload() .then(blob=> { console.log(blob, _this.model); // 更新資料庫 fetch(`/posts/${_this.model.id}.json`, { headers: { 'X-CSRF-Token': _this.csrf, 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, method: 'PUT', body: JSON.stringify({ post: { images: [blob.signed_id] } }), credentials: 'same-origin' }) .then(res=> res.json()) .then(data=> { wrapper.innerHTML = ` <div class="lds-dual-ring"></div> `; let img = document.createElement('img'); img.src = data.url; img.onload = ()=> { wrapper.innerHTML = ''; wrapper.appendChild(img); wrapper.insertAdjacentHTML('beforeend', ` <a href="/posts/${_this.model.id}/images/${data.id}" data-action="click->uploads#destroy"> 刪除 </a> `); }; }); }); }); target.value = ''; } get model() { return JSON.parse(this.data.get('model')); } get directUploadUrl() { return this.data.get('directUploadUrl') } get csrf() { return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } }
8 scss 的部分
.uploads { display: flex; flex-wrap: wrap; } .img-wrapper { display: inline-flex; border: 1px solid #d9d9d9; min-width: 100px; min-height: 100px; border-radius: 3px; margin-right: 15px; padding: 5px; align-items: center; flex-direction: column; justify-content: center; .progress { width: 80%; height: 10px; background-color: #ccc; border-radius: 5px; position: relative; .progress-bar { position: absolute; top: 0; left: 0; bottom: 0; opacity: 0.8; border-radius: 5px; background: #0076ff; transition: width 120ms ease-out, opacity 60ms 60ms ease-in; transform: translate3d(0, 0, 0); } } } .lds-dual-ring { display: inline-flex; width: 64px; height: 64px; justify-content: center; align-items: center; } .lds-dual-ring:after { content: " "; display: block; width: 46px; height: 46px; margin: 1px; border-radius: 50%; border: 5px solid #327ccb; border-color: #327ccb transparent #327ccb transparent; animation: lds-dual-ring 1.2s linear infinite; } @keyframes lds-dual-ring { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
9 刪除功能 - config/routes.rb
resources :posts do delete '/images/:image_id' => 'posts#destroy_image', as: :destroy_image, on: :member end
10 controllers/posts_controller.rb
before_action :set_post, only: [:show, :edit, :update, :destroy, :destroy_image] # DELETE /posts/1/images/2 def destroy_image @post.images.find(params[:image_id]).purge render json: { status: :ok } end
11 javascript/controllers/uploads_controller.js 加入刪除功能
destroy(e) { e.preventDefault(); const url = e.target.href; fetch(url, { headers: { 'X-CSRF-Token': this.csrf, 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, method: 'DELETE', credentials: 'same-origin' }) .then(res=> res.json()) .then(data=> { e.target.parentElement.remove(); }); }
12 調整 views/posts/show.html.erb
<p> <strong>Images</strong> <divclass="uploads" data-controller="uploads" data-uploads-model="<%=@post.to_json%>" data-uploads-direct-upload-url="<%=rails_direct_uploads_path%>" > <%@post.images.eachdo |image|%> <divclass="img-wrapper"> <%=image_tag image.variant(resize:'100x100')%> <ahref="<%=destroy_image_post_path(@post, image)%>"data-action="click->uploads#destroy">刪除</a> </div> <%end %> <inputtype="file"multiple="true"data-action="change->uploads#start"> </div> </p>
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- RecyclerView使用指南(一)—— 基本使用
- 如何使用Meteorjs使用URL参数
- 使用 defer 还是不使用 defer?
- 使用 Typescript 加强 Vuex 使用体验
- [译] 何时使用 Rust?何时使用 Go?
- UDP协议的正确使用场合(谨慎使用)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。