Deploying R shiny app as a stand

Deploying R shiny app as a stand

起源!

某天,我发现了Shiny这个东西,当时兴冲冲的尝试官网上各种各样的例子,最后发现这个东西似乎只能充当一个“玩具”。如果要在本地运行,它需要一个完整的R环境,这对相当一部分用户来说是极度不友好的。另外,Rstudio主张将Shiny部署在https://www.shinyapps.io/,但是看到这个价格以及资源限制以后进一步被劝退了。

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

毕竟很多科研工作者的出发点是将自己的研究过程和结果分享展示给他人,而不是出于商业的目的,部署在服务器上供他人使用需要持续投入计算资源和维护成本,这不是长久之计。


目的?

那么,如果我们实现了一个精妙的Shiny App,如何0成本的分享给别人,且别人能够方便的使用呢?为了达到这个目的,最好的结果是将R中的Shiny App转换为一个独立运行的exe文件,也就是一个这样的桌面应用:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

对,我实现了,过程中还是踩了一些坑的,现在就把具体的方法分享给大家。这是我自己思考的方法,因为本人也是刚开始研究,可能还有些地方理解的不是很清楚,如果您有更好的建议,恳请不吝斧正。

刚开始我是看了这个stone大神写的贴作为启蒙:https://zhuanlan.zhihu.com/p/121003243,但是我没能在自己电脑上实现,因为electricShine这个东西是一个写死的包,写死既被动,在调用npm的时候总会有小小的问题导致全盘失败。虽然没有成功实现,但是我肯定是不服的。后来我又看了某机构的博客:https://foretodata.com/how-to-make-a-standalone-desktop-application-with-shiny-and-electron-on-windows/,感觉上可行,尝试以后发现跑通了,确实可以。但是以上都不好作为最终的解决方案。

那么一个最为方便且易于实现的思路是这样的:

  • 安装R-Portable作为开发、部署、分发的R环境
  • 在上述环境中开发ShinyApp(推荐使用golem)
  • 通过electron-quick-start将R-Portable和ShinyApp打包成exe
    该方法基于Windows实现了打包exe,理论上可以在mac上实现打包dmg

怎么做?

0 准备工作

  • 熟悉R及Rstudio
  • 熟悉命令行操作
  • 了解Shiny App及其基本结构
  • 确定了解我们的目的
  • 新建一个工作目录C:\myShinyApp

1 下载安装R-portable

链接:https://sourceforge.net/projects/rportable/files/R-Portable/3.6.3/

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

强烈建议这个3.6.3版本,比较稳定,4.0.0编译暂时有问题。

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

安装比较简单,注意将路径设置为我们新建的工作目录,安装完成即可。

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

2 配置 Rstudio

现在我们要开启R-Portable作为R环境
打开Rstudio,鼠标点:Tools>Global Options>General>Change R version>Browse
定位我们刚才安装的R-Portable路径(C:\myShinyApp\R-Portable\App\R-Portable)
然后点选择文件夹,选择64位版本

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

一路点OK,最后重启Rstudio
.libPaths()里有我们刚才装好的R-Portable就好了:

 > .libPaths()
[1] "C:/Users/XXX/Documents/R/win-library/3.6"
[2] "C:/myShinyApp/R-Portable/App/R-Portable/library"

注意:这里出现了两个路径,[1]是我原来就有的,[2]是刚装的,ShinyApp中所有要用到的包必须装在[2]里。

3 搭建Shiny App

golem包是开发Shiny App的辅助开发工具,用它可以让开发过程更加方便。
先在Rstudio中安装这个包:

install.packages('golem',dependencies = T)

安装完成后,在Rstudio中点菜单:File>New Project>New Directory>Package for Shiny App using golem

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

将Directory name随意设置为shinyapptest,路径定位到我们的工作目录

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

创建完成后,我们就在Rstudio中开辟了一个新的Project和工作环境,且工作目录出现了一个类似于R包的结构:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

根据golem的Document,我们主要关注./dev中的三个脚本01_start.R02_dev.R03_deploy.R以及./R中的三个脚本app_ui.Rapp_server.Rrun_app.R

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

假如我们现在要实现文章开头例2提到的csv表格查看器。

3.1 添加模块

载入csv文件的按钮就是一个模块(按钮本身是模块的UI,读取csv文件是这个模块的功能),我们运行./dev/02_dev.R中的add_module添加一个模块

## Add modules ----
## Create a module infrastructure in R/
golem::add_module( name = "csv_file" ) # Name of the module

结果./R路径下生成了一个以mod_为前缀的模块文件,

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

mod_csv_file.R这个文件的内容改成这样的:

#' csv_file UI Function
#' @description A shiny Module.
#' @param id,input,output,session Internal parameters for {shiny}.
#' @noRd
#' @importFrom shiny NS tagList
mod_csv_file_ui <- function(id, label = "CSV file"){
  ns <- NS(id)
  tagList(
    fileInput(ns("file"), label),
    checkboxInput(ns("heading"), "Has heading"),
    selectInput(ns("quote"), "Quote", c(
      "None" = "",
      "Double quote" = "\"",
      "Single quote" = "'"
    ))
  )
}

#' csv_file Server Function
#' @noRd
mod_csv_file_server  <- function(id, stringsAsFactors) {
  moduleServer(
    id,
    ## Below is the module function
    function(input, output, session) {
      # The selected file, if any
      userFile <- reactive({
        # If no file is selected, don't do anything
        validate(need(input$file, message = FALSE))
        input$file
      })
      # The user's data, parsed into a data frame
      dataframe <- reactive({
        read.csv(userFile()$datapath,
                 header = input$heading,
                 quote = input$quote,
                 stringsAsFactors = stringsAsFactors)
      })
      # We can run observers in here if we want to
      observe({
        msg <- sprintf("File %s was uploaded", userFile()$name)
        cat(msg, "\n")
      })
      # Return the reactive that yields the data frame
      return(dataframe)
    }
  )
}

模块的定义包含两个部分:mod_csv_file_ui 定义模块UI,mod_csv_file_server 定义模块功能,如果要使用这个模块只需在Shiny App的app_ui中调用前者,app_server中调用后者就可以了。

3.2 写AppUI和AppServer

我们将app_ui.R改为这样的:

#' The application User-Interface
#' @param request Internal parameter for `{shiny}`.
#'     DO NOT REMOVE.
#' @import shiny
#' @noRd
app_ui <- function(request) {
  tagList(
    # List the first level UI elements here
    fluidPage(
      sidebarLayout(
        sidebarPanel(
          mod_csv_file_ui("datafile", "User data (.csv format)") # 调用模块UI
        ),
        mainPanel(
          dataTableOutput("table")
        )
      )
    )
  )
}

为了节省空间我把golem导入外部资源的部分去除了。
然后将app_server.R改成这样的:

#' The application server-side
#' @param input,output,session Internal parameters for {shiny}.
#'     DO NOT REMOVE.
#' @import shiny
#' @noRd
app_server <- function(input, output, session) {
  datafile <- mod_csv_file_server("datafile", stringsAsFactors = FALSE) # 调用模块function
  output$table <- renderDataTable({
    datafile()
  })
}

3.3 测试App

改好这些文件以后我们在./dev/run_dev.R脚本中测试一下我们的Shiny App:

> # Detach all loaded packages and clean your environment
> golem::detach_all_attached()
错误: $ operator is invalid for atomic vectors
此外: Warning message:
In FUN(X[[i]], ...) :
  DESCRIPTION file of package 'shiny' is missing or broken

运行到上面这一条提示我们还没有装shiny这个包,那就装吧:

install.packages(pkgs = 'shiny',
                 lib = .libPaths()[length(.libPaths())], # 保证装到R-Portable的lib里
                 dependencies = T) # 保证同时安装依赖

再次运行这一条,发现成功了:

> # Detach all loaded packages and clean your environment
> golem::detach_all_attached()
>

最后运行run_app

# Run the application
library(golem)
library(shiny)
source('./R/app_server.R')
source('./R/app_ui.R')
source('./R/mod_csv_file.R')
source('./R/run_app.R')
run_app()

出现下面这个界面Shiny App基本上就成了,可以打开一个csv文件自己测试一下。

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

3.4 打包Shiny App

假如有一天,我们精妙的Shiny App终于大功告成了,那么可以将他打成package并安装到R-Portable中。
先准备一下devtools:

if(!requireNamespace("devtools")){
  install.packages("devtools")
  library(devtools)
}

然后打包shinyapp,路径为当时golem创建的项目路径:

devtools::build(path = "C:/myShinyApp/shinyapptest")
√  checking for file 'C:\myShinyApp\shinyapptest/DESCRIPTION' ...
-  preparing 'shinyapptest':
√  checking DESCRIPTION meta-information ...
-  checking for LF line-endings in source and make files and shell scripts
-  checking for empty or unneeded directories
-  building 'shinyapptest_0.0.0.9000.tar.gz'
[1] "C:/myShinyApp/shinyapptest/shinyapptest_0.0.0.9000.tar.gz"

安装这个打包成功的packageshinyapptest_0.0.0.9000.tar.gz

install.packages(
  pkgs = 'C:/myShinyApp/shinyapptest/shinyapptest_0.0.0.9000.tar.gz',
  lib = .libPaths()[length(.libPaths())],
  repos = NULL, # 这个参数一定要的
  dependencies = T
)

# 尝试用包直接运行app
shinyapptest::run_app()

shiny具体的开发文档还是要研究一下:https://shiny.rstudio.com/articles/。好了,R的工作完成了剩下的交给electron-quick-start。

4 安装并配置node.js

4.1 下载解压

去这个链接下载zip压缩文件:https://nodejs.org/download/release/v12.16.2/node-v12.16.2-win-x64.zip
我装的是v12.16.2版本,如果嫌下载慢的话,想想办法,这里我分享一个网盘给你们:
链接: https://pan.baidu.com/s/1QbLJcfhRqTsgUeQ10Wy7wA
提取码: 4gzh
这是解压版,安装版也是同理的。下载完成后解压到指定目录,可以是我们的工作目录,解压完以后是这样的:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

4.2 配置环境变量

在这个目录中新建两个文件夹node_globalnode_cache

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

新建一个系统变量,变量名是NODE_PATH,值是nodejs的解压或安装目录C:\myShinyApp\node-v12.16.2-win-x64

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

新建另一个关键的系统变量,变量名是NODE_TLS_REJECT_UNAUTHORIZED,值是0,我觉得这个变量很关键:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

编辑Path环境变量,新建这两个值:C:\myShinyApp\node-v12.16.2-win-x64C:\myShinyApp\node-v12.16.2-win-x64\node_global(忽略图中的大小写笔误)
将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

4.3 配置npm参数

现在,以管理员身份打开优秀的Windows Powershell,检查node和npm是否安装正常:

> node -v
v12.16.2
> npm -v
6.14.4

配置一些必要的npm参数:

> npm config set prefix "C:\myShinyApp\node-v12.16.2-win-x64\node_global"
> npm config set cache "C:\myShinyApp\node-v12.16.2-win-x64\node_cahce"
> npm config set strict-ssl false
> npm config set registry http://registry.npm.taobao.org/

4.4 安装 electron-packager

以上配置就是为了能够成功安装这个包

> npm install electron-packager -g

# 出现以下信息说明成功
# + [email protected]
# added 18 packages from 9 contributors, removed 10 packages and updated 8 packages in 4.188s

5 使用electron-quick-start模板

如果方便在命令行用git的话(我一般是用WSL+Cmder),就先cdC:\myShinyApp\electron-quick-start,然后clone项目:

$ git clone https://github.com/listen2099/electron-quick-start.git

如果不方便用git,就直接下载连接中的zip文件解压到C:\myShinyApp\electron-quick-starthttps://github.com/listen2099/electron-quick-start/archive/master.zip
拉取或解压成功后:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

再次以管理员身份打开优秀的Windows Powershell:

> cd C:\myShinyApp\electron-quick-start
> npm install

# 出现以下信息就明名安装成功
# > [email protected] postinstall C:\myShinyApp\electron-quick-start\node_modules\electron
# > node install.js
# added 148 packages from 139 contributors in 4.326s

接下来是关键的一步:
将R-Portable路径C:\myShinyApp\R-Portable\App\R-Portable下的所有文件复制并替换C:\myShinyApp\electron-quick-start\R-Portable-Win路径:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

?还记得吗?这个环境里有我们安装好的R环境、写好的ShinyApp以及依赖的R包(其实,ShinyApp也作为包安装在这个R环境了,依稀记得包名叫shinyapptest)。

回到C:\myShinyApp\electron-quick-start,编辑这个目录下的app.R文件,这个文件是程序的入口,那么你猜这个文件应该写什么?要不就试试写这一行内容保存:

# app.R
shinyapptest::run_app()

最后一次打开优秀的Windows Powershell,完成最后的打包

> cd C:\myShinyApp\electron-quick-start
> npm run package-win

# 出现以下信息就说明成功了
# Packaging app for platform win32 ia32 using electron v5.0.7
# Wrote new app to ElectronShinyAppWindows\electron-quick-start-win32-ia32

6 完成

C:\myShinyApp\electron-quick-start文件夹下出现了一个新的目录:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

双击exe文件:

将Shiny APP搭建为独立的桌面可执行程序 -  Deploying R shiny app as a standalone application-LMLPHP

成功!

12-06 09:48