内容简介:本文為使用由於我們在這個實作會練習使用
本文為使用 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协议的正确使用场合(谨慎使用)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Is Parallel Programming Hard, And, If So, What Can You Do About
Paul E. McKenney
The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!