计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
build-shiny-module【效率与协作】使用 NS() 构建具有正确命名空间隔离的可复用 Shiny 模块。涵盖模块 UI/server 对、响应式返回值、模块间通信及嵌套模块组合。适用于从不断增长的 Shiny 应用中 提取可复用组件、构建在多处使用的 UI 小部件、将复杂响应式逻辑封装在清晰接口后, 或从更小的可测试单元组合更大的应用。
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
创建具有正确命名空间隔离、响应式通信和可组合性的可复用 Shiny UI/server 模块对。
在编写代码之前,定义模块接受什么和返回什么:
Module: data_filter
Inputs: reactive dataset, column names to filter on
Outputs: reactive filtered dataset
UI: filter controls (selectInput, sliderInput, dateRangeInput)
预期结果: 清晰的契约,指定响应式输入、响应式输出和 UI 元素。
失败处理: 如果接口不清晰,模块可能过于宽泛。将其拆分为职责单一的更小模块。
#' Data Filter Module UI
#'
#' @param id Module namespace ID
#' @return A tagList of filter controls
#' @export
dataFilterUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(
ns("column"),
"Filter column",
choices = NULL
),
uiOutput(ns("filter_control")),
actionButton(ns("apply"), "Apply Filter", class = "btn-primary")
)
}
关键规则:
<name>UI 约定idns <- NS(id)ns() 包裹每个 inputId 和 outputIdtagList() 以允许灵活放置预期结果: 创建命名空间化输入/输出元素的 UI 函数。
失败处理: 如果两次使用模块时 ID 冲突,检查每个 ID 是否都用 ns() 包裹。常见遗漏:renderUI() 或 uiOutput() 内部的 ID——这些也需要 ns()。
#' Data Filter Module Server
#'
#' @param id Module namespace ID
#' @param data Reactive expression returning a data frame
#' @param columns Character vector of filterable column names
#' @return Reactive expression returning the filtered data frame
#' @export
dataFilterServer <- function(id, data, columns) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
# Update column choices when data changes
observeEvent(data(), {
available <- intersect(columns, names(data()))
updateSelectInput(session, "column", choices = available)
})
# Dynamic filter control based on selected column
output$filter_control <- renderUI({
req(input$column)
col_data <- data()[[input$column]]
if (is.numeric(col_data)) {
sliderInput(
ns("value_range"),
"Range",
min = min(col_data, na.rm = TRUE),
max = max(col_data, na.rm = TRUE),
value = range(col_data, na.rm = TRUE)
)
} else {
selectInput(
ns("value_select"),
"Values",
choices = unique(col_data),
multiple = TRUE,
selected = unique(col_data)
)
}
})
# Return filtered data as a reactive
filtered <- eventReactive(input$apply, {
req(input$column)
col <- input$column
df <- data()
if (is.numeric(df[[col]])) {
req(input$value_range)
df[df[[col]] >= input$value_range[1] &
df[[col]] <= input$value_range[2], ]
} else {
req(input$value_select)
df[df[[col]] %in% input$value_select, ]
}
}, ignoreNULL = FALSE)
return(filtered)
})
}
关键规则:
<name>Server 约定idmoduleServer(id, function(input, output, session) { ... })session$ns预期结果: 处理输入并返回响应式输出的 server 函数。
失败处理: 如果响应式值不更新,检查动态 UI 的输入是否使用 session$ns(而非外部 ns)。如果模块返回 NULL,确保 return() 是 moduleServer() 内部的最后一个表达式。
# In app_ui.R or ui
ui <- page_sidebar(
title = "Analysis App",
sidebar = sidebar(
dataFilterUI("filter1")
),
card(
DT::dataTableOutput("table")
)
)
# In app_server.R or server
server <- function(input, output, session) {
# Raw data source
raw_data <- reactive({ mtcars })
# Call module — capture its return value
filtered_data <- dataFilterServer(
"filter1",
data = raw_data,
columns = c("cyl", "mpg", "hp", "wt")
)
# Use the module's returned reactive
output$table <- DT::renderDataTable({
filtered_data()
})
}
预期结果: 模块出现在 UI 中,其返回的响应式流入下游输出。
失败处理: 如果模块 UI 不渲染,验证 UI 和 server 调用之间的 id 字符串是否匹配。如果返回的响应式为 NULL,检查 server 函数是否确实返回了值。
对于包含其他模块的模块:
analysisUI <- function(id) {
ns <- NS(id)
tagList(
dataFilterUI(ns("filter")),
plotOutput(ns("plot"))
)
}
analysisServer <- function(id, data) {
moduleServer(id, function(input, output, session) {
# Call inner module with namespaced ID
filtered <- dataFilterServer("filter", data = data, columns = names(data()))
output$plot <- renderPlot({
req(filtered())
plot(filtered())
})
return(filtered)
})
}
关键规则:在 UI 中,使用 ns("inner_id") 嵌套。在 server 中,只用 "inner_id" 调用——moduleServer 处理命名空间链接。
预期结果: 内部模块在外部模块的命名空间内正确渲染。
失败处理: 如果内部模块的 UI 不出现,你可能忘记了在外部 UI 函数中用 ns() 包裹内部模块的 ID。如果 server 通信断开,检查内部模块 ID 是否匹配(server 调用中不用 ns())。
# Quick test app for the module
if (interactive()) {
shiny::shinyApp(
ui = fluidPage(
dataFilterUI("test"),
DT::dataTableOutput("result")
),
server = function(input, output, session) {
data <- reactive(iris)
filtered <- dataFilterServer("test", data, names(iris))
output$result <- DT::renderDataTable(filtered())
}
)
}
预期结果: 模块在最小测试应用中正常工作。
失败处理: 如果模块在隔离环境中失败但在完整应用中工作(或反之),检查是否存在对全局变量或父会话状态的隐式依赖。
id 作为第一个参数并使用 NS(id)ns() 包裹moduleServer(id, function(input, output, session) { ... })session$ns 作为 IDrenderUI() 中忘记 ns():在 server 内部创建的动态 UI 必须使用 session$ns——外部 ns 在 moduleServer() 内部不可用reactive(data) 而非 dataid 字符串必须与 server 调用中的 id 完全匹配return() 一个响应式。遗忘这一点是一个隐性 bugns("inner_id")。在 server 中:只用 "inner_id"。混淆这些会导致命名空间双重包裹或缺少前缀scaffold-shiny-app — 在添加模块之前设置应用结构test-shiny-app — 使用 testServer() 单元测试测试模块design-shiny-ui — 模块 UI 的 bslib 布局和主题设置optimize-shiny-performance — 模块内的缓存和异步模式