不是说学一门语言学的不仅仅是他的语法,更重要的是他背后的思想么?R本身是个大杂烩,ggplot可以单独拎出来作为一门语言学,shiny其实也可以单独拎出来学一番。
只是简单的实现一个shiny app确实不难,就像官网上一进去看到的那个例子那样。基本上如果只是做一些比较简单的可控的dashboard,shiny的代码无非就是写的细致一点,谈不上什么架构之类的。
直到某一天...你发现这东西还可以玩的更深,然后就毅然跳入了下一个大坑——shiny reactivity。官网的开场白很直接:
It’s easy to build interactive applications with Shiny, but to get the most out of it, you’ll need to understand the reactive programming model used by Shiny.
然后就介绍三剑客:reactive sources, reactive conductors, and reactive endpoints。
最简单的情况:没有conductor,直接从source到endpoint。
继续拷贝官网图:
嗯,听起来不难...其实大部分情况下shiny app处理的都是这样的情况。基本上就是一个输入(input)和输出(output)的过程。最简单的,就是我们经常在shiny app里面需要处理的input对象,会读入各种用户操作带来的值,然后后面返回一个表格或者图或者文字什么的。回到官网的例子(继续拷贝官网图)
shinyServer(function(input, output) {
output$distPlot <- renderPlot({
hist(rnorm(input$obs))
})
})
大概就是这么一个简单的架构。在这种架构下,自然可以写很多if else之类的在server.R中加入各种各样输入输出的组合,然后在ui.R中排列一下输入的各种框框和输出的各种图标文字什么的。
考虑conductors的情况
shiny用的稍稍熟悉了,就开始想更多的控制这个东西。其实conductor说白了有点中间变量的感觉,就是他本身并不会最终显示出来,但是作为一个中间过程存在着。比如官网一个计算Fibonacci 序列的例子:
fib <- function(n) ifelse(n<3, 1, fib(n-1)+fib(n-2))
shinyServer(function(input, output) {
currentFib <- reactive({ fib(as.numeric(input$n)) })
output$nthValue <- renderText({ currentFib() })
output$nthValueInv <- renderText({ 1 / currentFib() })
})
这里无非就是多了一个新的对象currentFib(跟input和output都无关),暂时的存储了一下计算过程中的变量,然后基于这个东西,又衍生了两个最终显示出来的返回值。新的架构图就多了一个conductor这样。
再复杂一点:考虑Reactive expressions
按照官网的说法,这个东西包括:
- accessing a database
- reading data from a file
- downloading data over the network
- performing an expensive computation
基本就是从其他的数据源而不是用户的input那里读数据的情况。常见的场景比如,按照用户给定的一些条件(由input传入),返回一个符合条件的数据子集。如果数据全集是会不断更新的(比如每次都应该重新load("xxx.rdata")这样),那么这个这句话就必须写在reactive里面。同理,如果直接利用input生成一句SQL然后连接数据库读取数据,那么也要写在reactive里面。
再复杂一点:自动还是手动更新
shiny默认的情况下,如果没有放submit按钮,那么就是随便一个操作就trigger整个reactive的流程。比如官网首页上,我们拖拖鼠标那个直方图就跟着变化。如果不想让他直接变化,那么简单的可以放一个submit按钮,然后只有submit之后才会变化。
如果希望更好的控制一个app界面上各个部分呢?就要动用更高级一点的actionButton
。
这东西还要配合一个isolate()
,就是你要告诉shiny哪些东西跟着这个button变化,哪些不属于这个button的控制范围。比如官网的例子:
shinyServer(function(input, output) {
output$distPlot <- renderPlot({
# Take a dependency on input$goButton
input$goButton
# Use isolate() to avoid dependency on input$obs
dist <- isolate(rnorm(input$obs))
hist(dist)
})
})
这里只有goButton被点击的时候,才会执行下面的抽随机数并画图。actionButton
是随着每次点击而不断增加计数的,从0开始一直增加过去,所以可以在server.R里面利用这个button的返回值来进行流程控制。
篇外废话:
shiny有希望成长成一个可以替代php这样实现客户端和服务器端大部分交互请求的巨人么?从这点来看,Tableau还是限制太多了。