#ruby-on-rails #actioncable
Вопрос:
Я создал некоторый код, которым хотел бы поделиться со всем миром. В частности, с людьми, которые хотят отвечать на длительные запросы, не блокируя свои серверы.
Примером использования является создание файлов PDF или CSV.
Прежде всего, я создал новый Action Cable
канал под названием long_running_requests
.
app/channels/long_running_requests_channel.rb
class LongRunningRequestsChannel lt; ApplicationCable::Channel def subscribed stream_from "long_running_request_#{connection.session_id}" end def unsubscribed # Any cleanup needed when channel is unsubscribed end end
Как вы можете видеть, у нас есть переменная в названии канала connection.session_id
. Это определено в следующем файле.
app/channels/application_cable/connection.rb
module ApplicationCable class Connection lt; ActionCable::Connection::Base identified_by :current_user def connect # code end def session_id @request.session.id end protected def find_verified_user # code end end end
Это означает, что мы можем предоставить каждому пользователю (вошедшему в систему или нет) уникальный канал для передачи данных с помощью их сеанса. Это станет основой для нашего долгосрочного ответа на запрос.
Мы создаем кнопку, которая будет запрашивать создание PDF-файла, что может занять пару секунд.
app/views/welcome/index.html.erb
lt;%= long_running_request_button( url: [:download_pdf, :welcome], request_title: (t 'pdf_download_button_title'), # 'Generate PDF' requested_title: (t 'pdf_generating_button_title'), # 'Generating PDF' (with a spinner) download_title: (t 'pdf_generated_button_title'), # 'Download PDF' new_tab: true) %gt;
The actual code of this method comes from my ApplicationHelper.
app/helpers/application_helper.rb
def long_running_request_button(url:, request_title:, requested_title:, download_title:, new_tab: false) request_uid = SecureRandom.hex # include a random uid into the buttons. We do this so we can have many download-buttons on one page. request_button_id = "request_button_#{session.id}_#{request_uid}" requested_button_id = "requested_button_#{session.id}_#{request_uid}" download_button_id = "download_button_#{session.id}_#{request_uid}" # add params to the given URL url = [{ request_button_id: request_button_id, requested_button_id: requested_button_id, download_button_id: download_button_id, content_disposition: (new_tab ? 'inline' : 'attachment') }] # generate the request button with a title and some javascript # will be shown from the start request_button = link_to url, remote: true, onclick: "longRequestButtonClicked('#{request_button_id}', '#{requested_button_id}')", class: 'button small blue', id: request_button_id do # the title a = content_tag :span do request_title end a end # generate the requestED button with a spinner and a title, will do nothing else but show the user we're doing something # will be shown only when request_button has been clicked requested_button = link_to '#', onclick: 'return false;', disabled: true, class: 'button small dark disabled hide-it', id: requested_button_id do # the spinner a = content_tag :span, class: 'icon spinner light' do '' end # the title b = content_tag :span do requested_title end a b end # generate the actual download button with an icon and a title # will be shown when response came back long_running_requests channel download_button = link_to '#', class: 'button small blue hide-it', target: (new_tab ? '_blank' : ''), id: download_button_id do # the icon a = content_tag :span, class: 'icon download light' do '' end # the title b = content_tag :span do download_title end a b end # return all 3 buttons request_button requested_button download_button end
When the button is clicked, longRequestButtonClicked
is called. All this does is hide the clicked button and show a spinner that says we’re generating the PDF.
function longRequestButtonClicked(requestButtonId, requestedButtonId) { requestButton = document.querySelector(`#${requestButtonId}`); requestedButton = document.querySelector(`#${requestedButtonId}`); // hide the request_button requestButton.classList.add('hide-it'); // show the requested_button requestedButton.classList.remove('hide-it'); }
In the controller that is receiving our request, this is the called method.
app/controllers/welcome_controller.rb
def download_pdf # do your authentication Resque.enqueue(GenerateAndSendPdf, { content_disposition: params[:content_disposition], session_id: session.id.to_s, request_button_id: params[:request_button_id], requested_button_id: params[:requested_button_id], download_button_id: params[:download_button_id], }) head :ok end
It puts the generation of the PDF on a Resque background job and responds with a head :ok
.
The Resque job will create and upload the PDF and transmit the data to the channel where the user is connected to. It also transmits the correct button ids that came from the request. (again) We do this so we can have many download-buttons on one page.
app/jobs/generate_and_send_pdf.rb
class GenerateAndSendPdf # I run on Heroku extend Resque::Plugins::Heroku @queue = :action_cable_broadcast def self.perform(hash) pdf = # your PDF generation pdf_url = # your PDF URL from uploading it somewhere session_id = hash["session_id"] channel_name = "long_running_request_#{session_id}" ActionCable.server.broadcast channel_name, { download_url: pdf_url, request_button_id: hash["request_button_id"], requested_button_id: hash["requested_button_id"], download_button_id: hash["download_button_id"], } end end
long_running_requests
Канал будет вести трансляцию.
app/assets/javascripts/channels/long_running_requests.js
if ($('meta[name=action-cable-url]').length) { App.messages = App.cable.subscriptions.create('LongRunningRequestsChannel', { connected: function() { }, disconnected: function() { }, received: function(data) { downloadUrl = data['download_url'] requestButtonId = data['request_button_id'] requestedButtonId = data['requested_button_id'] downloadButtonId = data['download_button_id'] requestedButton = document.querySelector(`#${requestedButtonId}`); // hide the requested_button requestedButton.classList.add('hide-it'); downloadButton = document.querySelector(`#${downloadButtonId}`); // show the download_button downloadButton.classList.remove('hide-it'); downloadButton.href = downloadUrl; } }); }
И вот ты здесь. Фактическая кнопка загрузки теперь видна и имеет href
ссылку на URL загруженного документа. Нажав на нее, вы загрузите PDF-файл.
Один вопрос, который у меня есть к вам, ребята: это безопасно?
Спасибо.
Комментарии:
1. Краткий обзор кажется мне безопасным. Чтобы убедиться в этом, просто следите за потоком в браузере. Вы должны получать только данные, доступные для конкретного пользователя. Также вам нужно убедиться, что ваше
download_pdf
действие правильно авторизовало пользователя (вручную или с помощью эксперта или другого..). (Также могут возникнуть крайние случаи, но я не вижу, чтобы что-то мигало как неправильное сейчас)2. В коде на моем веб-сайте я на самом деле использую Pundit для аутентификации, но удалил его здесь, чтобы не усложнять его для примера. Я также отследил поток, и все выглядит нормально. Спасибо за ваш комментарий @Maxence.