简体   繁体   English

"如何在 Shiny App 的 downloadHandler 中使用 callr::r_bg"

[英]How to use callr::r_bg within a downloadHandler in a Shiny App

The scenario I'm emulating with the below minimal example is allowing a user to engage with a Shiny App (click the numericInput<\/code> control and see server-side events occur) while a long-running download is occurring (simulated with Sys.sleep(10)<\/code> within downloadHandler<\/code> ).我用下面的最小示例模拟的场景是允许用户在长时间运行下载时使用 Shiny 应用程序(单击numericInput<\/code>控件并查看发生的服务器端事件)(使用Sys.sleep(10)<\/code>在downloadHandler<\/code>中)。

In a synchronous setting, when the "Download" button is clicked, the user can still interact with UI elements, but other Shiny calculations (in this case, renderText<\/code> ), get put in a queue.在同步设置中,当单击“下载”按钮时,用户仍然可以与 UI 元素交互,但其他 Shiny 计算(在本例中为renderText<\/code> )会被放入队列中。 I'd like the asynchronous setting, where the download occurs in the background, and users can still interact with the UI elements and<\/strong> get desired output (eg renderText<\/code> ).我想要异步设置,下载发生在后台,用户仍然可以与 UI 元素交互并<\/strong>获得所需的输出(例如renderText<\/code> )。

I'm using callr::r_bg()<\/code> to achieve asynchronicity within Shiny, but the issue is that my current code of the downloadHandler is incorrect ( mtcars<\/code> should be getting downloaded, but the code is unable to complete the download, 404 error message), I believe it's due to the specific way in which downloadHandler<\/code> expects the content()<\/code> function to be written, and the way I've written callr::r_bg()<\/code> is not playing nicely with that.我正在使用callr::r_bg()<\/code>在 Shiny 中实现异步,但问题是我当前的 downloadHandler 代码不正确(应该下载mtcars<\/code> ,但代码无法完成下载,404 错误消息) ,我相信这是由于downloadHandler<\/code>期望编写content()<\/code>函数的特定方式,而我编写callr::r_bg()<\/code>的方式并不能很好地适应它。 Any insights would be appreciated!任何见解将不胜感激!

Reference:<\/strong>参考:<\/strong>

https:\/\/www.r-bloggers.com\/2020\/04\/asynchronous-background-execution-in-shiny-using-callr\/<\/a> https:\/\/www.r-bloggers.com\/2020\/04\/asynchronous-background-execution-in-shiny-using-callr\/<\/a>

Minimal Example:<\/strong>最小的例子:<\/strong>

library(shiny)

ui <- fluidPage(
  downloadButton("download", "Download"),
  
  numericInput("count",
               NULL,
               1,
               step = 1),
  
  textOutput("text")
)

server <- function(input, output, session) {

  long_download <- function(file) {
    Sys.sleep(10)
    write.csv(mtcars, file)
  }
  
  output$download <- downloadHandler(
    filename = "data.csv",
    content = function(file) {
      x <- callr::r_bg(
        func = long_download,
        args = list(file)
      )
      return(x)
    }
  )
  
  observeEvent(input$count, {
    output$text <- renderText({
      paste(input$count)
    })
  })
  
}

shinyApp(ui, server)

I figured out a solution, and learned the following things:我想出了一个解决方案,并学到了以下几点:

  • Because downloadHandler doesn't have a traditional input$X , it can be difficult to include reactivity in the traditional way.因为 downloadHandler 没有传统的input$X ,所以很难以传统方式包含反应性。 The workaround was to present the UI as a hidden downlodButton masked by an actionButton which the user would see.解决方法是将 UI 呈现为隐藏的actionButton ,被用户将看到的downlodButton掩盖。 Reactivity was facilitated in the following process: user clicks actionButton -> reactive updates -> when the reactive finishes ( reactive()$is_alive() == FALSE ), use shinyjs::click to initiate the downloadHandler在以下过程中促进了反应性:用户单击 actionButton -> 反应性更新 -> 当反应性完成时( reactive()$is_alive() == FALSE ),使用shinyjs::click启动downloadHandler
  • Instead of placing the callr function within the downloadHandler, I kept the file within the content arg.我没有将callr函数放在 downloadHandler 中,而是将文件保存在内容 arg 中。 There seems to be some difficulties with scoping because the file needs to be available within the content function environment范围界定似乎有些困难,因为文件需要在content功能环境中可用
  • I'm using a reactive function to track when the background job (the long-running computation) is finished to initiate the download using the syntax: reactive()$is_alive()我正在使用反应函数来跟踪后台作业(长时间运行的计算)何时完成以使用以下语法启动下载: reactive()$is_alive()
  • The invalidateLater() and toggling of a global variable ( download_once ) is important to prevent the reactive from constantly activating. invalidateLater()和全局变量( download_once )的切换对于防止反应性不断激活很重要。 Without it, what will happen is your browser will continually download files ad infinitum -- this behavior is scary and will appear virus-like to your Shiny app users!没有它,将会发生什么是您的浏览器会无限地不断下载文件——这种行为是可怕的,对于您的 Shiny 应用程序用户来说,它看起来像病毒一样!
  • Note that setting global variables is not a best practice for Shiny apps (will think of a better implementation)请注意,设置全局变量不是 Shiny 应用程序的最佳实践(会想到更好的实现)

Code Solution:代码解决方案:

library(shiny)
library(callr)
library(shinyjs)

ui <- fluidPage(
  shinyjs::useShinyjs(),
  #creating a hidden download button, since callr requires an input$,
  #but downloadButton does not natively have an input$
  actionButton("start", "Start Long Download", icon = icon("download")),
  downloadButton("download", "Download", style = "visibility:hidden;"),

  p("You can still interact with app during computation"),
  numericInput("count",
               NULL,
               1,
               step = 1),
  
  textOutput("text"),
  
  textOutput("did_it_work")
)

long_job <- function() { 
  Sys.sleep(5)
}

server <- function(input, output, session) {
  
  #start async task which waits 5 sec then virtually clicks download
  long_run <- eventReactive(input$start, {
    #r_bg by default sets env of function to .GlobalEnv
    x <- callr::r_bg(
      func = long_job,
      supervise = TRUE
    )
    return(x)
  })
  
  #desired output = download of mtcars file
  output$download <- downloadHandler(filename = "test.csv",
                                     content = function(file) {
                                       write.csv(mtcars, file)
                                     })
  
  #output that's meant to let user know they can still interact with app
  output$text <- renderText({
    paste(input$count)
  })
  
  download_once <- TRUE
  
  #output that tracks progress of background task
  check <- reactive({
    invalidateLater(millis = 1000, session = session)
    
    if (long_run()$is_alive()) {
      x <- "Job running in background"
    } else {
      x <- "Async job in background completed"
    
      if(isTRUE(test)) {
        shinyjs::click("download")
        download_once <<- FALSE
      }
      invalidateLater(millis = 1, session = session)
    }
    return(x)
  })
  
  output$did_it_work <- renderText({
    check()
  })
}

shinyApp(ui, server)

Thanks @latlio for your great answer.感谢@latlio 的精彩回答。 I think it cloud be easily improved.我认为它的云很容易改进。 invalidateLater should be used very carefully and only WHEN needed. invalidateLater应该非常小心地使用,并且只在需要时使用。 I only once invalidateLater and moved it to logical part where we are waiting for results.我只有一次invalidateLater并将其移至我们正在等待结果的逻辑部分。 Thus we are NOT invalidating the reactivity infinitely.因此,我们不会无限地使反应性无效。

library(shiny)
library(callr)
library(shinyjs)

ui <- fluidPage(
  shinyjs::useShinyjs(),
  #creating a hidden download button, since callr requires an input$,
  #but downloadButton does not natively have an input$
  actionButton("start", "Start Long Download", icon = icon("download")),
  downloadButton("download", "Download", style = "visibility:hidden;"),
  
  p("You can still interact with app during computation"),
  numericInput("count",
               NULL,
               1,
               step = 1),
  
  textOutput("text"),
  
  textOutput("did_it_work")
)

long_job <- function() { 
  Sys.sleep(5)
}

server <- function(input, output, session) {
  
  #start async task which waits 5 sec then virtually clicks download
  long_run <- eventReactive(input$start, {
    #r_bg by default sets env of function to .GlobalEnv
    x <- callr::r_bg(
      func = long_job,
      supervise = TRUE
    )
    return(x)
  })
  
  #desired output = download of mtcars file
  output$download <- downloadHandler(filename = "test.csv",
                                     content = function(file) {
                                       write.csv(mtcars, file)
                                     })
  
  #output that's meant to let user know they can still interact with app
  output$text <- renderText({
    paste(input$count)
  })
  
  #output that tracks progress of background task
  check <- reactive({
    if (long_run()$is_alive()) {
      x <- "Job running in background"
      invalidateLater(millis = 1000, session = session)
    } else {
      x <- "Async job in background completed"
      shinyjs::click("download")
    }
    return(x)
  })
  
  output$did_it_work <- renderText({
    check()
  })
}

shinyApp(ui, server)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 在 callr::r() 和 callr::r_bg() 中使用加载了 load_all() 的 R package 的函数 - Use functions of an R package loaded with load_all() in callr::r() and callr::r_bg() 如何使用R Shiny downloadHandler下载ggplotly图? - How to download a ggplotly plot with R Shiny downloadHandler? 如何使用变量contentType实现R Shiny downloadHandler? - How to implement R Shiny downloadHandler with variable contentType? 如何在 shiny 模块中使用 downloadButton 和 downloadHandler - how to use downloadButton and downloadHandler inside a shiny module R Shiny downloadHandler返回app html而不是绘图或数据 - R Shiny downloadHandler returns app html rather than plots or data 在闪亮的应用程序中删除下载文件中的第一列:downloadHandler 问题 - Dropping the first column in the downloaded file within shiny app: downloadHandler problem 将r Shiny ObserveEvent与downloadHandler一起使用 - Using r shiny observeEvent with a downloadHandler 如何将用户输入传递给 R shiny 下载处理程序中的文件名 - How to pass user input to filename in R shiny downloadhandler R:如何在闪亮的应用程序中使用 ggplot 标签中的数学符号? - R: How to use mathematical notation in ggplot labels within a shiny app? 闪亮的 R:如何在闪亮的应用程序中使用 for 循环 - shiny R: how to use for loop in shiny app
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM