使用 Shiny 模块复用代码
将用户界面和服务器逻辑封装为带命名空间且可复用的模块
使用 Shiny 模块复用代码 是 CoddyKit 上的免费 R Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 R Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 R Academy 课程共包含 4 节课。
为什么使用 Shiny 模块
随着 Shiny 应用不断增大,将所有用户界面和服务器代码放在一个文件中会变得难以管理。模块是自包含的 Shiny 用户界面和服务器逻辑单元,并使用命名空间化的 ID。您可以在同一个应用中多次复用同一模块而不会发生 ID 冲突,还可以独立测试模块。
# Problem: without modules, ID conflicts arise
# ui <- fluidPage(
# selectInput('dataset', ...), # used by plot1 AND plot2!
# selectInput('dataset', ...) # duplicate ID — BROKEN
# )
# With modules: each instance has its own namespaced IDs
# plotModule('plot1', ...) -> input$plot1-dataset
# plotModule('plot2', ...) -> input$plot2-datasetNS() — 命名空间函数
每个模块用户界面函数都以 ns <- NS(id) 开始。传递给 Shiny 的所有用户界面元素 ID 都必须用 ns() 包装。它会将 id- 添加到每个 ID 前面,从而创建命名空间,避免不同模块实例之间发生冲突。
# Module UI function
filter_plot_ui <- function(id) {
ns <- NS(id) # create the namespace function
tagList(
selectInput(ns('dataset'), 'Choose Dataset:',
choices = c('mtcars', 'iris', 'airquality')),
sliderInput(ns('n_rows'), 'Rows to show:', 1, 50, 20),
plotOutput(ns('scatter_plot'))
)
}moduleServer() — 服务器函数
moduleServer(id, function(input, output, session) {...}) 是定义模块服务器逻辑的现代方式(Shiny 1.5 及更高版本)。在该函数内部,input、output 和 session 会自动使用命名空间;您访问的是 input$dataset,而不是 input$plot1-dataset。
# Module server function
filter_plot_server <- function(id) {
moduleServer(id, function(input, output, session) {
data <- reactive({
# input$dataset is already namespaced to this instance
head(get(input$dataset), input$n_rows)
})
output$scatter_plot <- renderPlot({
df <- data()
plot(df[[1]], df[[2]],
xlab = names(df)[1], ylab = names(df)[2])
})
})
}在应用中使用模块
在 ui 中调用模块用户界面函数,并在 server 中调用模块服务器函数,两者使用相同的 id 字符串。您可以使用不同的 ID 多次调用同一模块,以创建彼此独立的实例。
# Main app using the module twice
ui <- fluidPage(
h2('Plot 1'),
filter_plot_ui('plot1'), # instance 1
hr(),
h2('Plot 2'),
filter_plot_ui('plot2') # instance 2 — no ID conflicts!
)
server <- function(input, output, session) {
filter_plot_server('plot1') # wire up instance 1
filter_plot_server('plot2') # wire up instance 2
}
shinyApp(ui, server)向模块用户界面传递参数
模块用户界面函数只是普通的 R 函数。除了 id 之外,您还可以添加其他参数,以便在创建每个实例时自定义其外观或行为。这些参数会在构建用户界面时计算一次。
# Module UI with extra parameters
summary_table_ui <- function(id, title = 'Summary', height = '300px') {
ns <- NS(id)
tagList(
h4(title),
div(
style = paste0('height:', height, '; overflow-y: auto;'),
DTOutput(ns('tbl'))
)
)
}
# Use with custom titles
summary_table_ui('train_tbl', title = 'Training Data', height = '400px')
summary_table_ui('test_tbl', title = 'Test Data', height = '200px')将响应式值传入模块
模块服务器函数可以将响应式值或表达式作为参数接收。这样,父应用就可以将数据传递给子模块。在模块内部,像调用函数一样调用该响应式值即可获取其当前值。
# Module that accepts a reactive as input
chart_module_server <- function(id, data_reactive) {
moduleServer(id, function(input, output, session) {
output$chart <- renderPlot({
df <- data_reactive() # call the reactive passed in
ggplot2::ggplot(df, ggplot2::aes(x = x, y = y)) +
ggplot2::geom_point(colour = input$colour)
})
})
}
# In main server:
server <- function(input, output, session) {
shared_data <- reactive({ load_data(input$source) })
chart_module_server('chart1', data_reactive = shared_data)
chart_module_server('chart2', data_reactive = shared_data)
}从模块返回响应式值
模块服务器函数可以向父应用返回响应式值。这样,子模块就能向上层传递信息。请从 moduleServer() 返回一个响应式值或响应式值列表,并在父服务器函数中接收它。
# Module that returns a reactive to the parent
filter_module_server <- function(id, all_data) {
moduleServer(id, function(input, output, session) {
# Return the filtered data reactive
filtered <- reactive({
all_data[all_data$group == input$group_filter, ]
})
return(filtered) # parent can use this reactive
})
}
# In main server:
server <- function(input, output, session) {
raw_data <- reactive({ read.csv('data.csv') })
# filtered_data is a reactive returned from the module
filtered_data <- filter_module_server('filter1', raw_data)
output$main_plot <- renderPlot({ plot(filtered_data()) })
}模块文件组织
对于大型应用,请将每个模块放在 R/ 文件夹中的独立文件内。应用加载时,Shiny 会自动加载 R/ 中的所有文件。这样可以让每个模块保持自包含,并使用 shinytest2 软件包独立进行测试。
# Recommended project structure:
# myapp/
# app.R # main app: source modules + wire up
# R/
# mod_filter_plot.R # filter_plot_ui() + filter_plot_server()
# mod_summary_table.R # summary_table_ui() + summary_table_server()
# mod_download.R # download_ui() + download_server()
# tests/
# testthat/test-mod_filter_plot.R
# In app.R:
library(shiny)
# source('R/mod_filter_plot.R') # not needed if in R/ folder
ui <- fluidPage(filter_plot_ui('p1'))
server <- function(input, output, session) { filter_plot_server('p1') }
shinyApp(ui, server)嵌套模块
模块可以包含其他模块。父模块通过 session 参数,将自身的 session 命名空间传递给对子模块的调用。每嵌套一层,就会增加一个命名空间前缀:outer-inner-element_id。
# Outer module uses an inner module
outer_server <- function(id) {
moduleServer(id, function(input, output, session) {
# Call an inner module using this module's session
inner_result <- inner_module_server(
id = 'inner',
session = session # passes the namespaced session
)
output$combined <- renderText({
paste('Inner result:', inner_result())
})
})
}
# ID chain: outer -> inner
# Full ID: outer-inner-element使用 shinytest2 测试模块
shinytest2 软件包可以将模块包装在最小应用中,从而为模块编写自动化测试。使用 AppDriver 驱动浏览器、设置输入并断言输出值,整个过程都不需要真实的浏览器会话。
library(shinytest2)
# Wrap the module in a testable app
test_that('filter_plot module filters correctly', {
test_app <- shinyApp(
ui = fluidPage(filter_plot_ui('test')),
server = function(input, output, session) {
filter_plot_server('test')
}
)
app <- AppDriver$new(test_app)
app$set_inputs('test-dataset' = 'iris') # namespaced input
app$wait_for_idle()
# Assert plot was rendered
expect_true(!is.null(app$get_value(output = 'test-scatter_plot')))
})模块通信模式
模块与父应用之间的通信模式总结如下:
- 父应用到模块:将响应式值作为参数传递给模块服务器。
- 模块到父应用:从 moduleServer() 返回响应式值。
- 同级模块之间:由父应用持有共享状态(reactiveValues),并将其传递给各个模块。
- 全局状态:使用在父应用中定义的
reactiveValues,并向下传递其引用。
# Sibling module communication via parent state
server <- function(input, output, session) {
shared <- reactiveValues(selected_row = NULL)
# Table module sets the selection
table_module_server('tbl', shared_state = shared)
# Detail module reads the selection
detail_module_server('detail', shared_state = shared)
# Both modules communicate through 'shared' reactiveValues
# Table sets shared$selected_row; Detail reads it
}快速检查
为什么模块用户界面函数中的所有用户界面元素 ID 都必须用 ns() 包装?
Shiny 模块回顾
用于代码复用的 Shiny 模块的要点:
- 模块通过使用
NS(id)为所有 ID 添加命名空间来避免 ID 冲突。 - 模块用户界面:使用
ns <- NS(id)的普通函数;用ns()包装所有 ID。 - 模块服务器:
moduleServer(id, function(input, output, session) {...})。 - 将响应式值作为函数参数传入模块;返回响应式值以实现向上通信。
- 使用不同的 ID 多次调用同一模块,以创建彼此独立的实例。
- 将模块组织在
R/mod_*.R文件中;Shiny 会自动加载R/文件夹。 - 使用
shinytest2::AppDriver测试模块。
# Complete module example
my_module_ui <- function(id) {
ns <- NS(id)
tagList(selectInput(ns('var'), 'Variable:', choices = names(mtcars)),
plotOutput(ns('hist')))
}
my_module_server <- function(id, data) {
moduleServer(id, function(input, output, session) {
output$hist <- renderPlot(hist(data()[[input$var]]))
})
}
# Use it:
ui <- fluidPage(my_module_ui('m1'), my_module_ui('m2'))
server <- function(input, output, session) {
d <- reactive(mtcars)
my_module_server('m1', d)
my_module_server('m2', d)
}
shinyApp(ui, server)常见问题解答
「使用 Shiny 模块复用代码」课时是免费的吗?
是的 — 「使用 Shiny 模块复用代码」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 R Academy 课程的其余内容,请升级到 CoddyKit PRO。 R Academy 课程共包含 4 节课。
「使用 Shiny 模块复用代码」这节课中我会学到什么?
将用户界面和服务器逻辑封装为带命名空间且可复用的模块 你通过在浏览器中直接运行的动手代码来练习 R Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 R Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 R Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「使用 Shiny 模块复用代码」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 R Academy 课中编写并运行代码吗?
能。每节 R Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 深入理解响应式编程
- 使用 Shiny 模块复用代码
- 使用 renderUI 和 insertUI 创建动态用户界面
- 部署 Shiny 应用