You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

R语言ShinyApp本地与部署后运行不一致问题求助

Shiny计时器部署至shinyapp.io后重复按键识别失效问题解决

问题概述

开发的实验室用Shiny计时器应用,本地运行时通过if (!e.repeat)实现了忽略重复按键的功能,但部署到shinyapp.io后该逻辑失效,长按按键会持续触发多次事件,导致数据重复添加。应用核心功能:

  • 按键按下启动单次计时,首次按键启动实验总计时器
  • 按键释放时保存计时数据至DT::dataTableOutput
  • 支持导出CSV数据

问题原因

  1. keypress事件在不同浏览器环境中对e.repeat属性的支持存在差异,shinyapp.io的运行环境未正确传递该属性
  2. 代码中存在重复的事件监听逻辑,且部分脚本存在语法错误(如keydown事件脚本中的多余字符b)
  3. 事件绑定的逻辑冗余,同一按键的不同事件监听可能导致冲突

解决方案

  1. 将依赖keypress的事件替换为keydown事件,keydown对e.repeat的支持更稳定
  2. 修复脚本中的语法错误
  3. 精简事件监听逻辑,避免重复绑定同一按键事件

修改后的完整代码

依赖库

# install.packages("shinythemes")
# install.packages("shiny")
# install.packages("DT")
# install.packages("lubridate")
# install.packages("keys")
# install.packages("vtable")
# install.packages('rsconnect')
library(shinythemes)
library(shiny)
library(DT)
library(lubridate)
library(keys)
library(vtable)

应用代码

my_options <- options(digits.secs = 3) # 设置计时器精度

ui <- fluidPage(
  title = NULL,
  lang = NULL,
  hr(),
  # 合并keydown事件监听,统一处理重复按键判断
  tags$script(HTML('document.addEventListener("keydown", function(e) {
    if (!e.repeat) {
      // 回车键启动实验总计时器
      if (e.keyCode === 13) {
        Shiny.setInputValue("t_exp_timer", TRUE, {priority: "event"});
      }
      // 空格键启动单次计时
      if (e.keyCode === 32) {
        Shiny.setInputValue("start", TRUE, {priority: "event"});
      }
      // S键停止计时
      if (e.keyCode === 83) {
        Shiny.setInputValue("stop", TRUE, {priority: "event"});
      }
    }
  });')),
  # 空格键释放时的统一处理
  tags$script(HTML('document.addEventListener("keyup", function(e) {
    if (e.keyCode === 32) {
      Shiny.setInputValue("lapsing_timer", TRUE, {priority: "event"});
      Shiny.setInputValue("reset", TRUE, {priority: "event"});
    }
  });')),
  titlePanel("Tail Flick Latency StopWatch"),
  sidebarPanel(
    textOutput('stopwatch')
  ),
  sidebarPanel(
    textOutput('exp_stopwatch')
  ),
  
  tags$hr(),
  mainPanel(
    DT::dataTableOutput("TailFlickTrials")
  ),
  downloadButton('download',"Download the data")
)


# 初始化响应式数据框
v <- reactiveValues()
v$df <- data.frame(Start_Time = numeric(), 
                   End_Time = numeric(), 
                   TimeLapsed = numeric(), 
                   stringsAsFactors = FALSE)

server <- function(input, output, session) {
  
  exp_timer <- reactiveVal(0)
  exp_timer_active <- reactiveVal(FALSE)
  timer <- reactiveVal(0)
  active <- reactiveVal(FALSE)
  tmp_Start_Time <- numeric(0)
  tmp_End_Time <- numeric(0)

  observe({
    invalidateLater(100, session)
    isolate({
      if(active())
      {
        timer(round(timer()+0.1,3))
      }
    })
  })
  
  observe({
    invalidateLater(100, session)
    isolate({
      if(exp_timer_active())
      {
        exp_timer(round(exp_timer()+0.1,2))
      }
    })
  })
  
  # 单次计时启动(空格键按下)
  observeEvent(input$start,{
    timer(0)
    tmp_End_Time <- numeric(0)
    tmp_Start_Time <- Sys.time()
    # 添加新行,仅填充开始时间
    new_row <- head(v$df[NA,], 1)
    new_row[c('Start_Time')] <- list(Start_Time = tmp_Start_Time)
    v$df <- rbind(v$df, new_row)
  })
  
  # 单次计时结束(空格键释放)
  observeEvent(input$reset,{
    timer(0)
    tmp_Start_Time <- numeric(0)
    tmp_End_Time <- Sys.time()
    # 计算并填充耗时
    tmp_TimeLapsed <- round(as.numeric(difftime(tmp_End_Time, v$df[nrow(v$df), 1], units ="secs")),3)
    v$df[nrow(v$df), 2] <- tmp_End_Time
    v$df[nrow(v$df), 3] <- tmp_TimeLapsed
  })

  observeEvent(input$lapsing_timer, {active(TRUE)})

  output$stopwatch <- renderText({
    paste("Time Lapsed: ", seconds_to_period(timer()))
  })
  
  observeEvent(input$t_exp_timer, exp_timer_active(TRUE))

  output$exp_stopwatch <- renderText({
    paste("Time Lapse Since first Pre-Test Trial: ", seconds_to_period(exp_timer()))
  })
  
  output$TailFlickTrials <- DT::renderDataTable({ 
    v$df
  })
  
  output$download <- downloadHandler(
      filename = function () {
        paste("MyData.csv")
      },
      content = function(file) {
        write.csv(v$df, file)
      }
    )
}

# 运行应用
options(shiny.maxRequestSize=30*1024^2)
options(rsconnect.max.bundle.files = 500000000)
shinyApp(ui = ui, server = server)

关键修改点

  • 将原有的多个keypress事件监听合并为一个keydown事件监听,统一处理e.repeat判断
  • 使用e.keyCode替代e.key,避免字符编码差异导致的事件识别错误
  • 修复了keydown事件脚本中的多余字符b
  • 精简了keyup事件逻辑,合并同一按键的多个事件触发

内容的提问来源于stack exchange,提问作者Jacob White

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 09:01:08