手把手學習使用 Rails 5.2 ActiveStorage (DirectUpload + ProgressBar)

栏目: Ruby · 发布时间: 6年前

内容简介:本文為使用由於我們在這個實作會練習使用
手把手學習使用 Rails 5.2 ActiveStorage (DirectUpload + ProgressBar)

本文為使用 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:ListBuckets3:PutObjects3:GetObjects3: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 其包含一個 coverimages 但是這次我們使用不一樣的流程來完成。 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");
});
  1. 加入 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 元素 並使用 stimuluscontroller

<p>
  <strong>Images</strong>
  <divdata-controller="uploads">
    <inputtype="file"multiple="true"data-action="change->uploads#start">
  </div>
</p>

這裡我們預計使用一個 input ,當其取得檔案的時候在搭配 stimulus 執行對應的操作。接著,我們先來處理上傳檔案的部分。

4 確認 controller 中 paramsbefore_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>

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Programming From The Ground Up

Programming From The Ground Up

Jonathan Bartlett / Bartlett Publishing / 2004-07-31 / USD 34.95

Programming from the Ground Up is an introduction to programming using assembly language on the Linux platform for x86 machines. It is a great book for novices who are just learning to program as wel......一起来看看 《Programming From The Ground Up》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具