Длительные запросы с использованием кабеля действия

#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.