banner

Se você deseja apenas usar Vite como substituto do Webpacker integrado ao Rails (como era no Rails 6), basta usar Vite Ruby.

O objetivo deste artigo é diferente: ter 2 ambientes separados (Rails API e React frontend) usando o Vite, mas com algum grau de integração:

  • você pode usar o Puma em produção para servir o aplicativo React (ou seja, apenas um contêiner)
  • você pode usar node para automatizar qualquer outra tarefa no aplicativo Rails
  • iniciar ambos os servidores com um único comando

Requisitos

Estou assumindo que você já tenha:

Se você ainda não instalou nenhum, você pode tentar usar o nvm e o rvm ou dar uma olhada no meu post anterior sobre o asdf.

Configuração do Rails

Vamos chamar nosso aplicativo de blogger, então vamos começar criando a Rails API com:

rails new blogger --api
cd blogger
rails g scaffold Post title:string content:text
rails db:migrate

Seed do Banco de Dados

Crie um único post, adicionando no final do arquivo db/seeds.rb:

Post.create!(title: "Primeiro post", content: "Este é o primeiro post!")

Agora é só rodar rails db:seed.

CORS

Como estaremos executando dois servidores diferentes em portas diferentes, o CORS vai bloquear as solicitações do frontend para a API.

Vamos alterar isso pra permitir as chamadas vindas do frontend.

Abra o Gemfile e descomente (ou adicione ao final) a seguinte linha:

gem "rack-cors"

Agora instale a gem:

bundle install

Pra permitir que o frontend consulte a API, vamos editar o arquivo config/initializers/cors.rb adicionando no final a seguinte configuração:

# TODO: Add production URLs to the list
# Use '*' if you want expose the API to the world
accept = [
  'http://127.0.0.1:5173', # Dev Frontend
  'http://localhost:5173', # Dev Frontend
  'http://127.0.0.1:5174', # Dev Frontend
  'http://localhost:5174', # Dev Frontend
]

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins accept
    resource '*',
      headers: :any,
      methods: %i[get post put delete options patch],
      expose: %w[Authorization]
  end
end

Configuração do Foreman para o Backend

Mais adiante usaremos o Foreman para iniciar todos os servidores. Seguindo a documentação dele que recomenda que não seja incluído no Gemfile, apenas instale a gem com:

gem install foreman

Crie um arquivo chamado Procfile na pasta raiz do projeto com este conteúdo:

rails: bundle exec rails s -p 3000

Execute bundle install.

Pronto: o backend está configurado. Hora de instalar e configurar o frontend.

Bônus: todas os passos acima em uma tacada só:

rails new blogger --api
cd blogger
rails g scaffold Post title:string content:text
rails db:migrate
echo 'Post.create!(title: "First post", content: "This is the first post!")' >> db/seeds.rb
rails db:seed
bundle add rack-cors
bundle install
printf '\n' >> config/initializers/cors.rb
wget https://gist.githubusercontent.com/raelgc/799cadce1303bd604a38bc3808d272b4/raw/1b401713a728178ed4ab86e061bbe1b0cc4d5033/cors.rb -O ->> config/initializers/cors.rb
gem install foreman
echo 'rails: bundle exec rails s -p 3000' >> Procfile

Configuração do Vite

Inicie o Vite na pasta raiz do projeto usando o template para React + TypeScript:

npm create vite@latest . -- --template react-ts

Se este é o primeiro aplicativo Vite que você está criando, vai aparecer esse prompt (responda y):

Need to install the following packages:
  create-vite@5.1.0
Ok to proceed? (y)

Depois disso, o configurador vai perguntar o que fazer com os arquivos existentes na pasta do projeto (responda “Ignore files and continue”):

? Current directory is not empty. Please choose how to proceed: › - Use arrow-keys. Return to submit.
    Remove existing files and continue
    Cancel operation
❯   Ignore files and continue

Instale todos os pacotes configurados:

npm install

Movendo o Conteúdo do Frontend

Crie a pasta para o aplicativo React e mova os arquivos relacionados ao Vite:

mkdir react
mv src react/
mv public react/
mv index.html react/
mv vite.config.ts react/
mkdir public && touch public/.keep

Atualize o tsconfig.node.json para indicar o novo caminho substituindo a linha "include": ["vite.config.ts"] por:

"include": ["react/vite.config.ts", "react/src/**/*.d.ts"]

No arquivo package.json, atualize os comandos para apontar para a pasta react, substituindo a seção scripts pelo seguinte:

  "scripts": {
    "dev": "vite react",
    "build": "tsc && vite build react && touch public/.keep",
    "lint": "eslint react --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview react"
  },

Atualize react/vite.config.ts para fazer o build usando a pasta react/src e publicar o conteúdo gerado na pasta public do Rails (que sempre estará limpa, exceto pelo arquivo .keep), adicionando no final o seguinte dentro da função defineConfig:

  // Cole essas linhas dentro de defineConfig
  build: { 
    emptyOutDir: true,
    outDir: '../public' 
  },

Também atualize o arquivo tsconfig.json substituindo a linha "include": ["src"], por:

  "include": ["react/src"],

Configuração o Foreman para o Frontend

Como o Foreman já está instalado, vamos apenas incluir o script dev no Foreman (ou seja, adicione ao final do Procfile):

web: npm run dev

A configuração do frontend está pronta! Agora vamos fazer algumas requisições para o backend.

Requisições para a API

Instale o axios para fazermos requisições HTTP a nossa API:

npm install --save axios

Edite o arquivo react/src/main.tsx e configure a URL base padrão do axios, colocando o seguinte conteúdo acima da linha ReactDOM.createRoot:

// ... importações existentes acima
import axios from 'axios';

axios.defaults.baseURL = import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:3000';

Se você usar a API em uma URL diferente, defina uma variável de ambiente chamada VITE_APP_API_URL. Veja como o Vite funciona com as variáveis de ambiente.

Vamos atualizar nosso aplicativo React para listar todos os posts. Substitua o conteúdo inteiro de react/src/App.tsx por:

import { useState, useEffect } from 'react'
import axios from 'axios'
import './App.css'

type Post = {
  id: number,
  title: string,
  content: string,
}

function App() {
  const [posts, setPosts] = useState<Post[]>([])

  useEffect(() => {
    axios.get('/posts').then(response => setPosts(response.data)).catch(console.error)
  }, [])

  return (
    posts.length === 0 
      ? <p>No posts</p>
      : (
        <>
          <h1>Posts</h1>
          {posts.map(post => (
            <>
              <h2>{post.title}</h2>
              <p>{post.content}</p>
            </>
          ))}
        </>
      )
  )
}

export default App

Servindo Páginas Estáticas com o Servidor Rails (Puma)

Em um ambiente de produção, geralmente o frontend será servido pelo nginx. Mas é possível ter o Rails servindo tanto o backend quanto o frontend.

Adicione a gem rails-static-router ao Gemfile:

# Serve static index as catch-all route
gem "rails-static-router"

Instale-a:

bundle install

Adicione ao config/routes.rb:

  if ENV["RAILS_SERVE_STATIC_FILES"].present?
    get "*path", to: static("index.html")
    post "*path", to: static("index.html")
  end

Scripts

  • Iniciar o backend: rails s
  • Iniciar o frontend: npm run dev
  • Iniciar ambos (já que o foreman tá instalado): foreman start
  • Empacotar para produção: npm run build

Se você quiser testar usando o rails server como servidor web de produção (normalmente seria o nginx) para testar:

  • Execute primeiro o script “Compilar para produção
  • Em seguida, execute: RAILS_SERVE_STATIC_FILES=1 RAILS_ENV=production rails s