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

起源!

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

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


目的?

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

例1
例2

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

刚开始我是看了这个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/

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

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

2 配置 Rstudio

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

一路点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

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

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

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

假如我们现在要实现文章开头例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_为前缀的模块文件,

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文件自己测试一下。

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
这是解压版,安装版也是同理的。下载完成后解压到指定目录,可以是我们的工作目录,解压完以后是这样的:

4.2 配置环境变量

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

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

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

编辑Path环境变量,新建这两个值:C:\myShinyApp\node-v12.16.2-win-x64C:\myShinyApp\node-v12.16.2-win-x64\node_global(忽略图中的大小写笔误)

image.png

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

# 出现以下信息说明成功
# + electron-packager@15.2.0
# 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
拉取或解压成功后:

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

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

# 出现以下信息就明名安装成功
# > electron@5.0.7 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路径:

?还记得吗?这个环境里有我们安装好的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文件夹下出现了一个新的目录:

双击exe文件:

成功!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容