0Pricing
R Academy · Урок

Модули Shiny для повторного использования кода

Инкапсулируйте логику интерфейса и сервера в модули с пространствами имён, пригодные для повторного использования

«Модули Shiny для повторного использования кода» — бесплатный урок R Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения R Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс R Academy содержит 4 уроков всего.

Зачем нужны модули Shiny

По мере роста приложений Shiny хранить весь код пользовательского интерфейса и сервера в одном файле становится неудобно. Модули — это автономные части пользовательского интерфейса Shiny и серверной логики с пространствами имён для идентификаторов. Один и тот же модуль можно повторно использовать в одном приложении без конфликтов идентификаторов, а также тестировать независимо.

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

NS() — Функция пространства имён

Каждая функция пользовательского интерфейса модуля начинается с ns <- NS(id). Все идентификаторы элементов интерфейса, передаваемые в Shiny, должны быть обёрнуты в ns(). Эта функция добавляет 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. Один и тот же модуль можно вызывать несколько раз с разными идентификаторами, создавая независимые экземпляры.

# 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')

Передача реактивных значений INTO модуль

Серверные функции модулей могут принимать реактивные значения или выражения в качестве параметров. Это позволяет родительскому приложению передавать данные дочерним модулям. Внутри модуля вызывайте реактивное значение как функцию, чтобы получить его текущее значение.

# 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)
}

Возврат реактивных значений FROM модуля

Серверные функции модулей могут возвращать реактивные значения родительскому приложению. Это позволяет дочерним модулям передавать информацию вверх. Верните реактивное значение или список реактивных значений из 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
}

Быстрая проверка

Почему все идентификаторы элементов интерфейса в функции пользовательского интерфейса модуля должны быть обёрнуты в ns()?

Итоги изучения модулей Shiny

Основные выводы из темы «Модули Shiny для повторного использования кода»:

  • Модули предотвращают конфликты идентификаторов, добавляя пространство имён ко всем идентификаторам с помощью NS(id).
  • Пользовательский интерфейс модуля: обычная функция с ns <- NS(id); оборачивайте все идентификаторы в ns().
  • Сервер модуля: moduleServer(id, function(input, output, session) {...}).
  • Передавайте реактивные значения INTO модули в качестве параметров функций; RETURN реактивные значения для передачи данных вверх.
  • Вызывайте один и тот же модуль несколько раз с разными идентификаторами, создавая независимые экземпляры.
  • Организуйте модули в файлах 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 для повторного использования кода» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс R Academy, подпишись на CoddyKit PRO. Курс R Academy содержит 4 уроков всего.

Чему я научусь в уроке «Модули Shiny для повторного использования кода»?

Инкапсулируйте логику интерфейса и сервера в модули с пространствами имён, пригодные для повторного использования Ты практикуешь R Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать R Academy?

Предыдущий опыт не требуется. R Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Модули Shiny для повторного использования кода»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке R Academy?

Да. Каждый урок R Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Глубокое погружение в реактивное программирование
  2. Модули Shiny для повторного использования кода
  3. Динамический интерфейс с renderUI и insertUI
  4. Развёртывание приложений Shiny
← Назад к R Academy