11 Golem
Neste capítulo, vamos apresentar o framework Golem para a construção de aplicativos Shiny prontos para serem colocados em produção.
11.1 Motivação
O destino final de aplicativos Shiny costuma ser um ambiente de produção diferente do ambiente de desenvolvimento. Seja um servidor próprio, uma máquina na nuvem ou o shinyapps.io, o nosso app precisa funcionar nesses ambientes, não apenas na nossa máquina.
Uma vez no ambiente de produção, aplicativos Shiny costumam ficar lá por um bom tempo, gerando a necessidade de manutenção períodica e/ou atualizações. A depender de como o app foi desenvolvido, essas tarefas podem ficar muito mais trabalhosas. Seria interessante, nesse sentido, ter um framework de desenvolvimento que facilitasse a organização e documentação do código e o controle das dependências. É para isso que o Golem foi criado.
O Golem é um framework para desenvolvimento de aplicativos Shiny prontos para serem colocados em produção. As vantagens são:
padroniza a organização dos scripts e demais arquivos do seu app;
integra com pacotes que aceleram o desenvolvimento do código;
motiva a documentação do código;
e facilita o compartilhamento e a reutilização de códigos em outros projetos e com outras pessoas.
Na próxima seção, abordaremos como usar o pacote golem
para obter essas vantagens.
11.2 Como usar?
Antes de mais nada, precisamos instalar o pacote.
install.packages("golem")
Para criar um app dentro do framework Golem, basta rodar o seguinte código:
::create_golem("~/Documents/meuapp") golem
Esse código vai criar uma pasta chamada meuapp/
dentro de ~/Documents/
(você pode especificar qualquer outra pasta no seu computador). Essa pasta vai conter diversos arquivos que lhe permitirão iniciar o desenvolvimento do seu app dentro do Golem.
Antes de falarmos dos arquivos dessa pasta, precisamos ter em mente que usar o pacote golem
diz muito mais respeito a seguir uma filosofia do que a aprender uma ferramenta. Como os próprios autores descrevem
Golem is an opinionated framework for building production-grade shiny applications.
Isto é, para usar o Golem precisamos construir nosso app de um jeito específico, que os autores consideram ser o melhor. Com relação ao pacote em si, criada a estrutura inicial com a função golem::create_golem()
, você poderia continuar o desenvolvimento do app dentro desse framework sem utilizar nenhuma outra função do golem
1. O que realmente importa é seguir as seguintes premissas:
um aplicativo Golem é construído como um pacote R;
sempre que conveniente, devemos dividir o nosso app em módulos;
e devemos documentar as funções que compõem o aplicativo.
Assim, como já falamos de módulos no capítulo anterior, para entender melhor o Golem, precisamos falar um pouco de pacotes.
11.3 Pacotes
Se você nunca construiu um pacote de R, recomendamos fortemente a leitura do livro R Packages, em especial os 12 primeiros capítulos. Também recomendamos que treine construindo alguns pacotes simples fora do Shiny/Golem antes de continuar (veja os exercícios deste capítulo). Esse passo atrás é importante para que você saiba diferenciar o que é um pacote de R, o que é o Shiny e o que é o Golem.
Dito isso, para aqueles que por imprudência ou falta de tempo continuarão este texto sem uma base sobre o tema “pacotes”, vamos apresentar os pontos necessários para o entendimento do Golem.
11.3.1 O que é um pacote?
Um pacote de R é uma forma específica de organizar código, seguindo o protocolo descrito pela R Foundation.
Pacotes são a unidade fundamental de código R reprodutível.
— Wickham & Bryan
Um pacote inclue funções em R, documentação sobre como usá-las, testes e dados de exemplo.
De maneira geral, as funções de um pacote tentam resolver bem um problema em específico. O pacote dplyr
, por exemplo, possui funções especializadas em manipular bases de dados, já o pacote ggplot2
possui funções para a construção de gráficos.
11.3.2 Estrutura básica do pacote
A seguir, apresentaremos a estrutura básica (arquivos e pastas) de qualquer pacote R.
DESCRIPTION
: define o nome, descrição, versão, licença, dependências e outras caracaterísticas do pacote. É um arquivo de metadados.LICENSE
: especifica os termos de uso e distribuição do seu pacote..Rbuildignore
: lista arquivos que não devem ser incluídos ao compilar o pacote R a partir do código-fonte, isto é, arquivos que são úteis apenas no desenvolvimento e não serão enviados para quem instalar o pacote.NAMESPACE
: este arquivo declara as funções que o pacote exporta (que ficam disponível quando alguém usalibrary()
) e as funções que seu pacote importa de outros pacotes. Ele é criado automaticamente a partir da documentação das funções do pacote. Não devemos editar este arquivo manualmente.R/
: pasta onde fica o código R das funções do pacote. Essa pasta não deve conter subdiretórios.
11.3.3 Criando pacotes
Uma maneira fácil de criarmos a estrutura básica de um pacote é usamos a função usethis::create_package()
. Você deve passar um caminho como ~/Documents/meupacote
e uma nova pasta chamada meupacote
será criada dentro da pasta Documents
. Essa pasta será tanto um projeto do RStudio quanto um pacote, ambos chamados meupacote
.
Não adicione acentos, caracteres especiais e espaços no nome do pacote, assim como nos arquivos que você criar dentro dele.
::create_package("~/Documents/meupacote") usethis
11.3.4 A pasta R/
Dentro de um pacote, a pasta R/
só pode ter scripts R com funções. Guardaremos nela todas as funções que farão parte do nosso pacote, mesmo que elas sejam apenas funções usadas internamente.
As funções colocadas dentro dessa pasta nunca devem ser rodadas diretamente. Se você quiser testá-las, deve fazer isso “carregando as funções”, isto é, usando a função devtools::load_all()
. Isso fará com que todas as funções dentro da pasta R/
fiquem disponíveis na sua sessão, algo equivalente a fazer library(meupacote)
, mas com a diferença de também carregar as funções não exportadas.
Podemos usar usethis::use_r("nome-do-arquivo")
para criar um arquivo script R dentro da pasta R/
.
11.3.5 Dependências
Sem os inúmeros pacotes criados pela comunidade, o R provavelmente já estaria no porão da Ciência de Dados. Por isso, a primeira coisa que escrevemos nos nossos scripts quase sempre é library(algumPacoteLegal)
. Quando estamos construindo um pacote, é comum querermos utilizar dentro dele outros pacotes que não apenas o R base. Esses pacotes são chamados de dependências.
Ao desenvolver um pacote, a função library()
nunca deve ser utilizada2, e todas as funções externas devem ter seus pacotes de origem explicitamente referenciados pelo operador ::
. Embora seja chato especificar todos os pacotes, isso traz uma vantagem: as dependências do código estarão sempre atualizadas, pois elas estarão sempre atreladas às próprias funções sendo utilizadas.
Sempre que você utilizar um pacote dentro do pacote que está desenvolvendo, você deve especificá-lo como dependência no arquivo DESCRIPTION
. Isso dirá ao R que, ao instalar o seu pacote, ele também precisa instalar todos os pacotes listados como dependência nesse arquivo. Você pode fazer isso facilmente utilizando usethis::use_package()
. O código abaixo registra o pacote dplyr
como dependência de um pacote sendo construído.
::use_package("dplyr") usethis
Se você está usando um pacote em desenvolvimento a partir de um repositório do GitHub, por exemplo, você pode usar a função usethis::use_dev_package()
para adicioná-lo como dependência.
::use_dev_package("dplyr", remote = "tidyverse/dplyr") usethis
Leia a documentação dessas funções para mais informações sobre como adicionar dependências ao arquivo DESCRIPTION
.
11.3.6 Dados
Se o seu pacote possuir bases de dados, como a dplyr::starwars
, ou qualquer outro tipo de objeto do R, como pi
ou letters
, você deve colocá-los dentro de uma pasta chamada data/
, na raiz do projeto, com a extensão .rda
3. Isso pode ser feito facilmente a partir da função usethis::use_data()
.
Ao rodar o código abaixo, por exemplo, vamos criar uma pasta data/
na raiz do pacote, caso ela não exista ainda, e salvar nela o vetor base nomes
no arquivo nomes.rda
.
<- c("Athos", "Bruna", "Caio")
nomes ::use_data(nomes) usethis
Fazendo isso, quando alguém carregar esse pacote, o objeto nomes
ficará disponível para ser utilizado (igual a base starwars
fica disponível quando carregamos o dplyr
).
11.3.7 Documentação de funções
Para documentar as funções do seu pacote (i.e., gerar aquele documento mostrado quando rodamos ?mean
, por exemplo), escrevemos comentários antes da definição da função nos scripts da pasta R/
. Fazemos isso usando um tipo de comentários especial, o #'
, e marcadores que indicam qual parte da documentação estamos escrevendo. A estrutura dos comentários deve ser a seguinte:
#' Título da função
#'
#' Descrição da função
#'
#' @param a Descrição do primeiro parâmetro.
#' @param b Descrição do segundo parâmetro.
#'
#' @return Descrição do resultado (valor que sai da função).
#'
#' @export
<- function(a, b) {
fun + b
a }
O marcador @export
indica que a função ficará disponível quando rodarmos library(meupacote)
. Se você não quer que a função fique disponível, basta não colocar esse marcador.
Após escrever a documentação das suas funções dessa maneira, você deve rodar devtools::document()
para que ela seja compilada e fique disponível no seu pacote (acessível pelo Help do R). Isso é feito por trás das cortinas pelo pacote roxygen2
.
Dica: o RStudio disponibiliza um atalho para criar a estrutura da documentação de uma função. No menu superior, clique em Code
-> Insert Roxygen Skeleton
.
Para saber mais sobre documentação de pacotes, leia este capítulo do R Packages.
11.3.8 Instalando e compartilhando o seu pacote
Para verificar se você não feriu alguma regra de desenvolvimento de pacotes R, você pode usar a função devtools::check()
. Essa função devolverá um relatório com possíveis problemas que o seu pacote pode ter, como erros de sintaxe, arquivos com extensões não permitidos, dependências não declaradas ou erros de documentação.
Para instalar o seu pacote localmente durante o desenvolvimento, rode a função devtools::install()
. Isso é equivalente a ter o pacote instalado via install.packages()
.
O jeito mais fácil de disponibilizar o seu pacote na internet é subi-lo para um repositório público no Github. Dessa maneira, qualquer pessoa pode instalá-lo com a função remotes::install_github()
.
Para subir um pacote para o CRAN, o processo é (bem) mais burocrático. Se você quiser saber mais, leia este capítulo do R Packages.
11.4 Estrutura de um Golem app
Agora que já sabemos o básico sobre pacotes R, podemos voltar a falar do Golem.
Uma pasta criada pela função golem::create_golem()
terá a seguinte estrutura:
#> ├── DESCRIPTION
#> ├── NAMESPACE
#> ├── R
#> │ ├── app_config.R
#> │ ├── app_server.R
#> │ ├── app_ui.R
#> │ └── run_app.R
#> ├── dev
#> │ ├── 01_start.R
#> │ ├── 02_dev.R
#> │ ├── 03_deploy.R
#> │ └── run_dev.R
#> ├── inst
#> │ ├── app
#> │ │ └── www
#> │ │ └── favicon.ico
#> │ └── golem-config.yml
#> └── man
#> └── run_app.Rd
Veja que ela possui, entre outras coisas, a estrutura básica de um pacote. Vamos descrever cada arquivo mais detalhadamente e discutir a importância dele no contexto do desenvolvimento de um Shiny app:
O arquivo
DESCRIPTION
: guarda os metadados do pacote. No desenvolvimento de um aplicativo Shiny, ele vai guardar o nome do aplicativo, o que ele faz, as dependências dele, a versão (importante em projetos em produção que recebem atualizações periódicas) e quem contatar quando alguma coisa der errada. Com relação às dependências, isso quer dizer que, para rodar o seu app, o R precisará instalar todos os pacotes listados nesse arquivo.O arquivo
NAMESPACE
: guarda metadados do pacote. Com esse arquivo, podemos carregar apenas funções específicas de um pacote dentro do nosso app4. O Golem faz isso com o pacoteshiny
nas funçõesapp_ui()
eapp_server()
para não precisarmos colocarshiny::
no início de cada função.A pasta
R/
: guarda as funções do pacote. Como o app será feito dentro de um pacote R, todo o seu código será escrito em funções nessa pasta. O Golem já cria os arquivos para construirmos a UI e o servidor. Os scripts contendo os módulos do aplicativo também devem ser colocados nessa pasta, assim como scripts com funções úteis utilizadas em vários lugares do app.O arquivo
R/app_config.R
: usado para especificar alguns mecanismos do Golem, como ler o arquivo de configuração localizado eminst/golem-config.yml
.O arquivo
R/app_server.R
: script com a funçãoapp_server()
, onde você vai desenvolver o servidor do seu aplicativo.O arquivo
R/app_ui.R
: script com a funçãoapp_ui()
, onde você vai desenvolver a UI do seu aplicativo, e a funçãogolem_add_external_resources()
, utilizada para dizer ao Shiny que a pastainst/app/www
será utilizada como uma fonte de recursos externos, acessada pelo caminhowww/nome_do_arquivo
5. Além disso, o Golem inclui no HTML do seu app a conexão com todo arquivo CSS e JS que você coloca nessa pasta, então não precisamos fazer isso manualmente.O arquivo
R/run_app.R
: script que contém a funçãorun_app()
, utilizada para rodar o app. Ela chama a funçãoshiny::shinyApp()
, que inicia o app localmente. A funcãoshiny::shinyApp()
está dentro da funçãogolem::with_golem_options()
, que recebe parâmetros passados para arun_app()
. Esses parâmetros podem ser recuperados dentro do app com a funçãogolem::get_golem_options()
, deixando a parametrização de um aplicativo Shiny muito mais simples6.dev/
: pasta com scripts dogolem
que podem ser utilizados ao longo do desenvolvimento do app. Eles contêm uma lista de funções úteis que ajudam a configurar diversos aspectos do aplicativo. O uso desses scripts é opcional.A pasta
inst/app/www
: local onde adicionaremos os recursos externos do aplicativo (imagens, arquivos CSS, fontes etc) que serão compartilhados com o navegador de quem estiver usando o app. A pastainst
é uma pasta especial no desenvolvimento de pacotes. Ela serve para adicionarmos arquivos que gostaríamos que fossem instalados com o pacote, como arquivos de teste, imagens etc. No contexto do Shiny, ela será utilizada para guardarmos arquivos auxiliares, como a própria pastaapp/www
, templates.Rmd
de relatórios que o app gera, arquivos.md
com textos que serão colocados no app, entre outros.A pasta
man/
: contém a documentação do pacote, a ser gerada peloroxygen2
. É muito importante documentarmos todas as funções do nosso app, pois é muito comum que o código precise de ajustes ou atualizações no futuro. Uma breve descrição do que a função espera e o que ela devolve pode ser suficiente para ajudar a pessoa que for mexer no app no futuro (que pode ser você mesma) a esconomizar horas de debug.
11.5 Principais funções
Além da função golem::create_golem()
que utilizamos para criar o nosso projeto com a estrutura do framework Golem, o pacote golem
possui diversas funções úteis para usarmos durante o desenvolvimento do pacote. Vamos listar a seguir algumas delas:
golem::set_golem_name()
: usada para mudar o nome do seu aplicativo. A mudança precisa ser feita tanto no arquivoDESCRIPTION
quanto dentro da funçãoapp_sys()
contida no arquivoR/app_config.R
, e essa função realiza essas tarefas.golem::add_module()
: cria um arquivo na pastaR/
com o template de um módulo do Shiny. O nome do módulo (utilizado também como nome do arquivo) é passado pelo argumentoname
.golem::add_css_file()
egolem::add_js_file()
: cria um arquivo vazio com extensão.css
ou.js
dentro da pastainst/app/www
do app. O nome desse arquivo pode ser passado pelo argumentoname
.golem::use_utils_ui()
: cria um arquivo chamadogolem_utils_ui.R
na pastaR/
com diversas funções úteis para serem utilizadas na UI de um Shiny app.golem::use_utils_server()
: cria um arquivo chamadogolem_utils_server.R
na pastaR/
com diversas funções úteis para serem utilizadas no servidor de um Shiny app.golem::add_shinyappsio_file()
: cria um arquivoapp.R
que pode ser utilizado para fazer o deploy do app para o shinyapps.io.
11.6 Deploy
Neste capítulo, vamos falar sobre como fazer o deploy do seu app feito em Golem em dois ambientes diferentes: no shinyapps.io e em qualquer máquina ou serviço que rode containers Docker.
11.6.1 Deploy para o shinyapps.io
Para subir um Shiny app para o shinyapps.io (veja a Seção 9.1 para mais detalhes), precisamos enviar o script primário do nosso app7, normalmente chamado de app.R
, junto de todos os arquivos dos quais ele depende (outros scripts, bases de dados, imagens etc). A partir do RStudio, podemos fazer isso com alguns cliques a partir do botão Publish, disponível quando abrimos o arquivo app.R
, ou a partir da função rsconnect::deployApp()
, que consegue identificar esse script primário caso você não especifique para ela no argumento appPrimaryDoc
.
No entanto, na estrutura do Golem que vimos até o momento não temos esse arquivo, já que todas as partes do app são construídas dentro de funções na pasta R/
. O script run_app.R
não pode ser considerado esse script primário pois a chamada da função shiny::shinyApp()
está dentro da definição da função run_app()
. O que fazer nesse caso?
A solução é criar um arquivo app.R
dentro dessa estrutura. Podemos fazer isso usando a função golem::add_shinyappsio_file()
. O arquivo será criado na pasta raiz do seu projeto e poderá ser utilizado como script primário do seu app, isto é, a partir dele você poderá usar o botão Publish
. Além do app.R
, você deverá subir para o shinyapps.io todos arquivos que constituem o seu app/pacote.
O arquivo app.R
criado vai conter a chamada das seguintes funções:
::load_all()
pkgloadoptions("golem.app.prod" = TRUE)
run_app()
pkgload::load_all()
vai carregar o as funções definidas na pastaR/
.options("golem.app.prod" = TRUE)
vai definir a variável de sistemagolem.app.prod
comoTRUE
. Isso é opcional e pode ser utilizado para criar versões diferentes de homologação ou produção para o seu app.run_app()
: roda o seu app, isto é, executa a funçãoshiny::shinyApp()
.
Com esse arquivo criado, você também poderá utilizar normalmente a função rsconnect::deployApp()
para fazer o deploy.
11.6.2 Deploy com Docker
O Docker é um software código aberto para o desenvolvimento e a implantação de aplicações embrulhadas em containers8. Ao colocar o seu app em um container, você garante que o ambiente onde ele ficará hospedado, seja ele qual for, terá todas as dependências que ele precisa para funcionar e estará devidamente configurado para que as pessoas possam acessá-lo por meio de uma URL.
Para dockerizar um app, precisamos criar um Dockerfile
. Esse arquivo conterá todas as instruções necessárias para criar um container possuindo todas as dependências e configurações para hospedar o seu app (incluindo o Shiny Server). O pacote golem
possui algumas funções para a criação desse arquivo:
golem::add_dockerfile()
: adiciona umDockerfile
genérico, sem configurações para ambientes específicos.golem::add_dockerfile_shinyproxy()
: adiciona umDockerfile
com configurações específicas para o ShinyProxy, uma solução código aberto para o deploy de aplicativos Shiny.golem::add_dockerfile_heroku()
: adiciona umDockerfile
com configurações específicas para o Heroku, um serviço para hospedagem de qualquer tipo de aplicação dockerizada.
O Dockerfile
será criado na pasta raiz do seu projeto. Para testá-lo localmente, caso você tenha o Docker instalado na sua máquina, basta rodar no Terminal:
docker build -t meuApp .
docker run -p 8080:80 meuApp
Se você precisar trocar a configuração padrão do Shiny Server, crie um novo shiny-server.conf
na pasta inst/app
e insira a seguinte linha no Dockerfile
antes da linha EXPOSE 80
:
COPY ./inst/app/shiny-server.conf /etc/shiny-server/shiny-server.conf
Além do ShinyProxy e do Heroku, você também pode subir o seu app dockerizado para plataformas como a AWS e o Google Cloud Engine.
11.7 Exercícios
Quais são as vantagens de se usar o Golem?
Qual a diferença entre usar Golem e usar apenas módulos?
Podemos seguir o framework Golem sem usar o pacote
golem
. Verdadeiro ou falso?Em qual arquivo na estrutura de um pacote R definimos as suas dependências?
Construa um pacote R que tenha as seguintes funções:
soma(x, y)
: realiza a somax + y
;subtração(x, y)
: realiza a subtraçãox - y
;mult(x, y)
: realiza a multiplicaçãox * y
;divisao(x, y)
: realiza a divisãox / y
.
- Construa um pacote R que tenha as seguintes funções:
read_csvs(arquivos, empilhar = FALSE)
: ela recebe um vetor com caminhos de arquivos.csv
contendo bases de dados e devolve uma lista de tibbles com cada uma das bases seempilhar = FALSE
e uma tibble com todas as bases empilhadas seempilhar = TRUE
.read_excels(arquivos, empilhar = FALSE)
: a mesma ideia, mas para arquivos.xlsx
.
- Aplique o framework Golem no app disponibilizado neste link. Ele utiliza a base de dados Pokemon, que pode ser baixada clicando aqui. Além disso,
Transforme o shinydashboard em Bs4Dash: construa ao menos a UI do zero.
Modularize o app (cada página do dashboard deve ser um módulo diferente).
Refaça os gráficos utilizando alguma biblioteca javascript (plotly, echarts, highcharts etc).
Faça o deploy do app para o shinyapps.io.
Na prática, poderíamos construir essa estrutura nós mesmos e usar o framework Golem sem usar o pacote
golem
.↩︎Imagine se, ao usarmos uma função de um pacote, ele carregasse um novo pacote na nossa sessão, possivelmente mascarando funções que estamos usando. Isso seria uma péssima prática.↩︎
Arquivos
.rda
são extremamente estáveis, compactos e podem ser carregados rapidamente pelo R, tornando este formato o principal meio de guardar dados de um pacote.↩︎Isso é feito automaticamente pelo
roxygen2
a partir da documentação das funções. Não edite esse arquivo na mão.↩︎Isto é, você não precisa incluir
inst/app/
no caminho do arquivo. Por exemplo, se você quiser colocar a imageminst/app/www/imagem.png
no seu app, basta usar o caminhowww/imagem.png
.↩︎Podemos criar versões diferentes do app que serão executadas a depender dos parâmetros passados na função
run_app()
.↩︎O arquivo com a chamada da função
shiny::shinyApp()
no final.↩︎Containers são uma unidade padrão de software que empacota um código e suas dependências de tal forma que a aplicação possa rodar de maneira confiável independentemente do ambiente computacional. Leia mais aqui.↩︎