执行上下文

概述

Shiny 交互式文档可以包含在渲染时执行的代码以及在服务器上执行的代码,以响应用户操作和输入值的变化。对这些执行上下文的深入理解对于在开发过程中建立正确的思维模型以及优化文档性能都至关重要。

渲染与服务器上下文

为了更清晰地分解这一点,让我们回顾一下在交互式文档介绍中开始的 “Hello, Shiny” 文档:

---
title: "Old Faithful"
format: html
server: shiny
---

```{r}
sliderInput("bins", "Number of bins:", 
            min = 1, max = 50, value = 30)
plotOutput("distPlot")
```

```{r}
#| context: server
output$distPlot <- renderPlot({
  x <- faithful[, 2]  # Old Faithful Geyser data
  bins <- seq(min(x), max(x), length.out = input$bins + 1)
  hist(x, breaks = bins, col = 'darkgray', border = 'white')
})
```

以下是该文档执行的分解方式:

  1. 包含 sliderInput()plotOutput() 调用的第一个代码块将在渲染文档时执行(例如 quarto render old-faithful.qmd)。

  2. 带有 context: server 选项的第二个代码块不会在渲染时执行,而是仅在文档被服务时执行。

理解这两个代码块在完全独立的 R 会话中运行至关重要。这意味着你不能在第二个块中访问第一个块中创建的变量,反之亦然。这类似于构成大多数正常 Shiny 应用程序的 ui.Rserver.R 脚本。

当然,能够在不同上下文中重用代码是非常有用的,我们将在下面的 共享代码 部分介绍一些实现方法。

为了使交互式文档的代码易于理解和使用,我们强烈建议将服务器上下文(可以有多个)放在文档的底部。这使得在文档源代码的流程中,不同的执行环境更加清晰。

server.R

如果你更喜欢更强的分离,还有另一种选择。你可以将 .qmd 文件限制为仅在渲染时执行的代码,然后将服务器代码分离到单独的 server.R 文件中。

以这种方式重写我们的示例如下:

old-faithful.qmd
---
title: "Old Faithful"
format: html
server: shiny
---

```{r}
sliderInput("bins", "Number of bins:", 
            min = 1, max = 50, value = 30)
plotOutput("distPlot")
```
server.R
function(input, output, session) {
  output$distPlot <- renderPlot({
    x <- faithful[, 2]  # Old Faithful Geyser data
    bins <- seq(min(x), max(x), length.out = input$bins + 1)
    hist(x, breaks = bins, col = 'darkgray', border = 'white')
  })
}

这可能不太方便,但确实更好地符合传统 Shiny 应用程序中存在的 ui.R / server.R 分离。

共享代码

在渲染上下文之间共享代码的工作方式略有不同,具体取决于你的代码是否在单个 .qmd 文件中,或者是否使用 server.R。我们将在下面介绍这两种场景。

单个文件

context: setup

要在渲染和服务上下文中执行代码,请创建一个带有 context: setup 的代码块。例如:

```{r}
#| context: setup
#| include: false

# 加载库
library(dplyr)

# 加载数据
dataset <- import_data("data.csv")
dataset <- sample_n(dataset, 1000)
```

此代码将在渲染时以及为每个新用户会话创建服务器时执行。请注意,我们还指定了 include: false,以确保该块的代码、警告和输出不会包含在渲染的文档中。

context: data

数据的加载和操作通常主导 Shiny 应用程序的启动时间。由于交互式文档在两个阶段执行(初始渲染然后向用户提供文档),我们可以在渲染期间执行昂贵的数据操作,然后在启动应用程序时简单地加载数据。

你可以通过将 context: data 选项添加到 R 代码块来定义预渲染数据。该块将在渲染时执行,它创建的任何 R 对象都将保存到 .RData 文件中,然后在 Shiny 服务器启动时加载。例如,我们可以将上面所示的设置块中的数据加载分解到自己的块中:

```{r}
#| context: data
#| include: false

dataset <- import_data("data.csv")
dataset <- sample_n(dataset, 1000)
```

请注意,在 context: data 代码块中创建的 R 对象对 UI 渲染和服务器上下文都可用。

Knitr 缓存

你可以通过在数据代码块中添加 cache: true 选项来进一步提高数据渲染的性能。这将使得代码块仅在需要时重新执行。例如:

```{r}
#| context: data
#| include: false
#| cache: true
#| cache.extra: !expr file.info("data.csv")$mtime

dataset <- import_data("data.csv")
dataset <- sample_n(dataset, 1000)
```

在这个例子中,如果代码块中的 R 代码发生变化或 “data.csv” 文件的修改时间发生变化,缓存将会失效(这是通过 cache.extra 选项实现的)。

你也可以通过删除交互式文档旁边的 _cache 目录来使现有缓存失效。

context: server-start

还有一个额外的执行上下文,使你能够在多个用户会话之间共享代码和数据。具有 context: server-start 的代码块在 Shiny 文档首次运行时执行一次,并且不会为文档的每个新用户重新执行。使用 context: server-start 适用于以下几种场景:

  1. 建立与远程服务器的共享连接(例如数据库、Spark 上下文等)。

  2. 创建旨在在会话之间共享的反应值(例如使用 reactivePollreactiveFileReader)。

例如:

```{r}
#| context: server-start

library(DBI)
db <- dbConnect(...)
```

多个文件

如果你的交互式文档使用 .qmd 文件定义用户界面,并使用 server.R 文件作为服务器,你可以将共享代码放在名为 global.R 的文件中。在 global.R 中定义的函数和变量在渲染期间和服务器执行期间都可用。

在这种情况下,你的交互式文档由三个源文件组成:

文件 描述
doc.qmd Markdown 内容以及 Shiny 输入和输出(例如 sliderInput()plotOutput() 等)
server.R 主服务器函数,包含反应表达式、输出赋值等。
global.R doc.qmdserver.R 之间共享的代码。